Compare commits

..

256 Commits

Author SHA1 Message Date
Cody Henthorne
09b9349f6c Bump version to 5.51.1 2022-09-28 15:49:31 -04:00
Nicholas
aeb5a9cf57 Better handling of Bluetooth connections/disconnections during calls. 2022-09-28 15:44:38 -04:00
Greyson Parrelli
aaf8bf3280 Fix crash with delayed foreground service. 2022-09-28 15:16:52 -04:00
Greyson Parrelli
b6d7271858 Fix a PNP-related contact merge scenario. 2022-09-28 14:40:44 -04:00
Alex Hart
9498a34293 Add onWillBeDestroyed callback to ViewBinderDelegate 2022-09-28 14:45:27 -03:00
Cody Henthorne
0cae15b7fd Bump version to 5.51.0 2022-09-28 11:41:10 -04:00
Cody Henthorne
11e4fd7f34 Updated language translations. 2022-09-28 11:34:13 -04:00
Cody Henthorne
31f31534ce Round out sms/mms export process. 2022-09-28 11:34:13 -04:00
Greyson Parrelli
0e4bec3977 Clean up some unused feature flags. 2022-09-28 11:34:13 -04:00
Greyson Parrelli
7fef1b060f Add proxy support for CDSv2. 2022-09-28 11:34:13 -04:00
Alex Hart
0312dfcfcd Allow autofocus of name field. 2022-09-28 11:34:13 -04:00
Alex Hart
b05f4430f6 Ensure my story is always at the top of the list. 2022-09-28 11:34:13 -04:00
Alex Hart
8703707d62 Add registration check for Stories flag check. 2022-09-28 11:34:13 -04:00
Alex Hart
04eeb434c9 Add ability to hide contacts behind a feature flag. 2022-09-28 11:34:12 -04:00
Alex Hart
a8a773db43 Fix StoryLinkPreviewView touch targets. 2022-09-28 11:33:36 -04:00
Alex Hart
daf78b31b5 Fix hide dialog dismissal. 2022-09-28 11:33:36 -04:00
Alex Hart
20ce3e68f8 Move snackbar positioning. 2022-09-28 11:33:36 -04:00
Nicholas
92d065050f Fix Headset Switching (Especially Bluetooth) on Android 12+. 2022-09-28 11:33:36 -04:00
Nicholas
1b53f09687 Force LTR formatting for the phone number in AppSettingsFragment. 2022-09-28 11:33:36 -04:00
Alex Hart
f4d0bf900c Add polish to story crossfader when exiting viewer. 2022-09-28 11:33:36 -04:00
Sgn-32
c652d83f81 Use MaterialAlertDialogBuilder in EditProxyFragment.
Closes #12479
2022-09-28 11:33:36 -04:00
Nicholas
7167ad331f Hide megaphone view in archived list. 2022-09-28 11:33:36 -04:00
Greyson Parrelli
9bb089d198 Add interfaces for tables that reference RecipientIds or thread IDs. 2022-09-28 11:33:36 -04:00
Cody Henthorne
866853ff99 Fix qr scanner for camerax blacklisted devices. 2022-09-28 11:33:36 -04:00
Alex Hart
931b9f8831 Update stories jump logic to match spec. 2022-09-28 11:33:36 -04:00
Alex Hart
e8c10cd550 Add basic story search support. 2022-09-28 11:33:35 -04:00
Alex Hart
1049f8bd2f Update to Material Design 1.6.1 2022-09-28 11:33:35 -04:00
Jim Gustafson
9929e6549e Update to RingRTC v2.21.1 2022-09-28 11:33:35 -04:00
Cody Henthorne
ff28ff0e6b Fix too many pending intents crashes. 2022-09-28 11:33:35 -04:00
Alex Hart
2a82db2b02 Update bad calculation of content size for stories collection. 2022-09-28 11:33:35 -04:00
Greyson Parrelli
457c3c0526 Don't start disallowed foreground service on API 31+. 2022-09-28 11:33:35 -04:00
Cody Henthorne
4f803c695b Fix crash when unable to decode notification image preview. 2022-09-28 11:33:35 -04:00
Alex Hart
bdbdcccaff Fix potential crash when searching contacts in forward sheet. 2022-09-28 11:33:35 -04:00
Cody Henthorne
8d7393e4b5 Fix controlls showing in call PIP. 2022-09-28 11:33:35 -04:00
Greyson Parrelli
533dcfb828 Improve handling of SSLExceptions.
Current theory is that some Samsung devices a doing something funky with SSLExceptions, causing them to not be caught as IOExceptions.
2022-09-28 11:33:35 -04:00
Isira Seneviratne
e67ac95890 Use AlarmManagerCompat.
Fixes #12468
2022-09-28 11:33:35 -04:00
Alex Hart
1b63ed0b20 Remove redundant text from story landing screen empty state. 2022-09-28 11:33:35 -04:00
Alex Hart
07d9e29e7c Update new story text to be a small button. 2022-09-28 11:33:35 -04:00
Alex Hart
c47a724654 Add support for new group story display states. 2022-09-28 11:33:35 -04:00
Greyson Parrelli
8ca94eb3d5 Fix issue where link previews wouldn't finish if we couldn't fetch the thumbnail. 2022-09-28 11:33:35 -04:00
Greyson Parrelli
11b1c9655c Fix image banding that can sometimes show in high-res images. 2022-09-28 11:33:35 -04:00
Nicholas
cf3dd70600 Prevent Chats icon from animating when returning from other activity. 2022-09-28 11:33:35 -04:00
Alex Hart
0bf5f15cf9 Enqueue downloads for stories we view on other devices. 2022-09-28 11:33:35 -04:00
Alex Hart
ea3fb774f8 Display failure state in story info and other places. 2022-09-28 11:33:35 -04:00
Alex Hart
25c0dc801f Display group story notifications if user has reacted or replied. 2022-09-28 11:33:35 -04:00
Alex Hart
c29922a575 Add check to load thumbnail if it comes in late. 2022-09-28 11:33:35 -04:00
Varsha
e676f324f1 Add new handling to encourage the user to save their wallet recovery phrase.
This only effects those who have opted in to payments and have a non-zero balance.
2022-09-28 11:33:35 -04:00
Nicholas
c6bfdeb4b0 Track tab buttons' selected state in the ViewModel. 2022-09-28 11:33:35 -04:00
Greyson Parrelli
80a6e0f781 Show a chat event when two threads are merged.
* Add internal button to split contacts for debugging.
* Show a chat event when two threads are merged.
2022-09-28 11:33:35 -04:00
Varsha
bc7b0b40b0 Update payment keyboard insets and colors. 2022-09-28 11:33:35 -04:00
Alex Hart
1cea615675 Reimplement contact search collection to support group access predicate. 2022-09-28 11:33:35 -04:00
Alex Hart
9dd96148d1 Add story boolean to envelope proto. 2022-09-28 11:33:35 -04:00
Alex Hart
9e094dfc2b Add internal prefs page for launching stories dialogs. 2022-09-28 11:33:35 -04:00
Alex Hart
a39b09c314 Add correct tinting to send button in multiforward activity. 2022-09-28 11:33:35 -04:00
Alex Hart
6c4c299b28 Support enabling stories access by country. 2022-09-28 11:33:35 -04:00
Nicholas
a98cc5706f Use ViewCompat to get window insets on Android 5.0+.
On devices running API 20 and below, getRootWindowInsets() always returns null.
2022-09-28 11:33:35 -04:00
Nicholas Tinsley
7451ee1403 Update to targetSdkVersion 32. 2022-09-28 11:33:35 -04:00
Nicholas Tinsley
b9f4dc3fe9 Specify exported status and PendingIntent mutability.
Also reduce shake sampling frequency, add coarse location permission.
Random things for targetSdk 32.
2022-09-28 11:33:35 -04:00
Alex Hart
2566d6f61f Fix story unit test compilation. 2022-09-28 11:33:35 -04:00
Alex Hart
8eebdaf451 Set max story video duration to 30999ms. 2022-09-28 11:33:35 -04:00
Alex Hart
b1dacf4acd Fix story reply synchronization. 2022-09-28 11:33:35 -04:00
Alex Hart
9326c1726a Increase stories caption limit to 1500 grapheme clusters. 2022-09-28 11:33:35 -04:00
Alex Hart
654b602cef Fix bounds clipping in pinch-to-zoom story gesture. 2022-09-28 11:33:35 -04:00
Alex Hart
a642876bda Fix issue where crossfader has wrong story on shared element animation start. 2022-09-28 11:33:35 -04:00
Nicholas
2b8041d779 Make VerificationCodeView lay out properly on tiny screens.
Chain together the views inside VerificationCodeView so that they don't get collapsed to 0dp width.
2022-09-28 11:33:35 -04:00
Alex Hart
8141b53c15 Display dialog to confirm hiding story in story viewer. 2022-09-28 11:33:35 -04:00
Greyson Parrelli
115d1fcf63 Improve handling of unregistered users in storage service. 2022-09-28 11:33:31 -04:00
Alex Hart
ffa249885e Add scale gesture to stories. 2022-09-23 14:30:58 -04:00
Alex Hart
9a21f5abca Add stories link treatment for devices with link previews disabled. 2022-09-23 14:30:58 -04:00
Alex Hart
552592db39 Fix unread story nav. 2022-09-23 14:30:58 -04:00
Alex Hart
75af1b69e8 Update payment toolbars to match M3 specification. 2022-09-23 14:30:58 -04:00
Alex Hart
c96fec9537 Update username to use . as delimiter. 2022-09-23 14:30:58 -04:00
Greyson Parrelli
a457d1f569 Bump version to 5.50.4 2022-09-23 14:19:01 -04:00
Greyson Parrelli
87f206fdc4 Ensure websockets are restarted after changing proxy. 2022-09-23 14:18:05 -04:00
Greyson Parrelli
e845860c7c Bump version to 5.50.3 2022-09-22 12:46:38 -04:00
Greyson Parrelli
e351c74ddb Fix issue with bioauth on API 29. 2022-09-22 12:44:49 -04:00
Greyson Parrelli
aeeaef567f Bump version to 5.50.2 2022-09-19 15:25:02 -04:00
Greyson Parrelli
78a9206898 Updated language translations. 2022-09-19 15:24:45 -04:00
Greyson Parrelli
aab8bd1261 Filter badly-formatted numbers from one-off CDS requests. 2022-09-19 11:18:54 -04:00
Greyson Parrelli
db16155b0d Use proper log tag. 2022-09-19 11:17:42 -04:00
Greyson Parrelli
1b254ca185 Bump version to 5.50.1 2022-09-14 16:42:40 -04:00
Greyson Parrelli
9a6ed9bcb3 Update Dockerfile to build with compileSdk 32. 2022-09-14 16:42:11 -04:00
Greyson Parrelli
c8f0bd7b82 Fix lint. 2022-09-14 16:41:45 -04:00
Greyson Parrelli
f6b7b9e913 Bump version to 5.50.0 2022-09-14 15:31:42 -04:00
Greyson Parrelli
840a56cbb4 Updated language translations. 2022-09-14 15:30:44 -04:00
Nicholas
aa268fc3ba Only show "Note To Self" as Voice Memo author if both sender and receiver are self. 2022-09-14 15:30:44 -04:00
Alex Hart
889d1183b2 Allow the STORIES feature flag to be hot-swappable. 2022-09-14 15:30:44 -04:00
Alex Hart
a8706f65d5 Clean out witness verification metadata. 2022-09-14 15:30:44 -04:00
Alex Hart
26bebb9811 Upgrade several AndroidX Libraries.
AppCompat 1.2.0 to 1.5.1
Lifecycle 2.3.1 to 2.5.1
Navigation 2.3.5 to 2.5.2
Fragment 1.3.5 to 1.5.2
Annotations 1.2.0 to 1.4.0
Window 1.0.0-alpha09 to 1.0.0
AAPT2 to 7.0.4
Fragment-Testing 1.3.5 to 1.5.2 (matching Fragment)
2022-09-14 15:30:43 -04:00
Alex Hart
9331e9ce89 Add deprecation notice to SingleLiveEvent. 2022-09-14 15:30:43 -04:00
Greyson Parrelli
6417f5cce0 Improve logging around attachment compression failures. 2022-09-14 15:30:43 -04:00
Alex Hart
a340ebf74a Add espresso test for usernames. 2022-09-14 15:30:43 -04:00
Alex Hart
4882a4d11c Add new story-based AccountRecord fields and wiring. 2022-09-13 13:07:42 -04:00
Greyson Parrelli
b5300c877c Fix issue with contact share editing.
Fixes #12446
2022-09-13 13:07:42 -04:00
Nicholas Tinsley
c2b94274b0 Cancel Send if we return to fragment.
This plugs a lifecycle hole: previously if you leave this fragment (SelectionConfirmed), you get stuck in that state even if you return.
2022-09-13 13:07:42 -04:00
Nicholas Tinsley
46ec45b985 Update ReminderView to Material Design 3. 2022-09-13 13:07:42 -04:00
Cody Henthorne
beee3b7dc3 Add PNP linked device initialization job.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2022-09-13 13:07:42 -04:00
Cody Henthorne
e2a7ed86e4 Promote ongoing call notification to high priority. 2022-09-13 13:07:42 -04:00
Alex Hart
95b0639ab4 Fix issue where video player is not released by preview fragment. 2022-09-13 13:07:42 -04:00
Nicholas Tinsley
d7f9582bc4 Update Megaphone text styling to match Material Design 3. 2022-09-13 13:07:42 -04:00
Alex Hart
176a705079 Add PNP Listing Wiring. 2022-09-13 13:07:42 -04:00
Greyson Parrelli
8e9f311fca Refresh your own profile when the stories flag changes. 2022-09-13 13:07:42 -04:00
Alex Hart
977af2c2f3 Add capability to request username creation during registration. 2022-09-13 13:07:42 -04:00
Alex Hart
7e45fc4a3e Update URL formatting for username links. 2022-09-13 13:07:42 -04:00
Nicholas Tinsley
58489bab61 Update Basic Megaphone to Material Design 3. 2022-09-13 13:07:42 -04:00
Cody Henthorne
0685cf4e51 Add signal.me username support. 2022-09-13 13:07:42 -04:00
Alex Hart
9b9453734c Implement new API endpoints for Usernames. 2022-09-13 13:07:42 -04:00
Cody Henthorne
ca0e52e141 Fix bug with stale linked devices when changing number. 2022-09-13 13:07:42 -04:00
Alex Hart
24b7593178 Update camera layout for better support across different screen sizes. 2022-09-13 13:07:42 -04:00
Alex Hart
993e49db48 Username search UI tweak. 2022-09-13 13:07:42 -04:00
Nicholas Tinsley
d458ddba55 Schedule TrimThreadsByDateManager on app startup.
If enabled, this reschedules the alarm on every startup to make sure that the system never loses track of it.
2022-09-13 13:07:42 -04:00
Cody Henthorne
bd5747b7f6 Add more logging around failed backups. 2022-09-13 13:07:42 -04:00
Nicholas
a335130ad4 Clear Selection on ACTION_UP if longClickCopySpan is not found. 2022-09-13 13:07:42 -04:00
Cody Henthorne
9558513190 Prevent empty call screen after missed calls. 2022-09-13 13:07:42 -04:00
Alex Hart
27a3015d4f Set reply icon size to 20dp. 2022-09-13 13:07:42 -04:00
Alex Hart
f751f9afa8 Add support for new story gradient fields and fallback. 2022-09-13 13:07:42 -04:00
Alex Hart
2e2b31aa79 Start call after granting permissions.
Fixes #12419
2022-09-13 13:07:42 -04:00
Greyson Parrelli
135d002f02 Fix possible crash with CDSv2 compat. 2022-09-13 13:07:42 -04:00
Alex Hart
a45ede9348 Update AudioView in Attachment keyboard stub. 2022-09-13 13:07:42 -04:00
Greyson Parrelli
e4b2e5022f Remove some outdated internal settings. 2022-09-13 13:07:42 -04:00
Alex Hart
286010ce90 Fix clickable area around link previews. 2022-09-13 13:07:41 -04:00
Alex Hart
13eb89746b Add unit testing to story download enqueuer. 2022-09-13 13:07:41 -04:00
Cody Henthorne
d2f639c57f Bump version to 5.49.3 2022-09-13 10:52:16 -04:00
Cody Henthorne
ad587606b7 Updated language translations. 2022-09-13 10:42:51 -04:00
Cody Henthorne
9fd5e2057d Fix reply messages for android auto. 2022-09-13 10:38:31 -04:00
Cody Henthorne
8f63b850fc Bump version to 5.49.2 2022-09-07 14:33:54 -04:00
Cody Henthorne
199d04b663 Updated language translations. 2022-09-07 14:28:53 -04:00
Greyson Parrelli
658741be52 Fix token mismatch issues when using CDSv2. 2022-09-07 14:25:03 -04:00
Alex Hart
f1bcc756d3 Remove animation from flash helper. 2022-09-06 10:26:45 -03:00
Alex Hart
cdcb1de3d4 Bump version to 5.49.1 2022-09-01 17:17:03 -03:00
Alex Hart
7d11a6207a Updated language translations. 2022-09-01 17:16:26 -03:00
Alex Hart
e608ad24c2 Hide keyboard when closing the bubble activity. 2022-09-01 17:06:51 -03:00
Alex Hart
4fe382398e Adjust alpha and duration of selfie flash animation. 2022-09-01 17:06:51 -03:00
Alex Hart
b6546f3ae3 Fix single tap on video previews. 2022-09-01 17:06:51 -03:00
Alex Hart
4620eade58 Implement better state management and recoverability for donation badge jobs. 2022-09-01 17:06:51 -03:00
Alex Hart
23a328f12d Add screen to set Signal as default SMS. 2022-09-01 13:17:53 -03:00
Alex Hart
83905dd6a6 Bump version to 5.49.0 2022-08-31 15:58:41 -04:00
Alex Hart
3eb4eb3c09 Updated language translations. 2022-08-31 15:58:41 -04:00
Greyson Parrelli
2eba9a8d72 Add support for doing normal CDS queries on CDSv2. 2022-08-31 15:58:41 -04:00
Alex Hart
9b17e7a7e2 Fix story launching from settings. 2022-08-31 15:58:41 -04:00
Alex Hart
3eb9e4a035 Upgrade Glide to 4.13.2 and upgrade ExifInterface to 1.3.3 2022-08-31 15:58:41 -04:00
Alex Hart
3edc97eb38 Fix NPE when the attachment for a link preview is null. 2022-08-31 15:58:41 -04:00
Jim Gustafson
cb0208af4d Update to RingRTC v2.21.0 2022-08-31 15:58:41 -04:00
Greyson Parrelli
cdd311f741 Fix for possible issue in search. 2022-08-31 15:58:41 -04:00
Greyson Parrelli
8543325d59 Update database migrations to be in their own files. 2022-08-31 15:58:41 -04:00
Greyson Parrelli
a1a677a3e2 Apply network interceptors to CDSv2 websocket client. 2022-08-31 15:58:41 -04:00
Alex Hart
3705465ef2 Update translation strings for story privacy modes. 2022-08-31 15:58:41 -04:00
Alex Voloshyn
c80999839b Use AccountSnapshot to avoid unnecessary network calls. 2022-08-31 15:58:41 -04:00
Alex Hart
936212e684 Add initial sms exporter integration behind a feature flag. 2022-08-31 15:58:41 -04:00
Alex Hart
1cc39fb89b Fix launching of story from chat ring. 2022-08-31 15:58:41 -04:00
Alex Hart
37d3a953c8 Do not display icons in my stories row. 2022-08-31 15:58:41 -04:00
Alex Hart
5a1a23d9ac Fix view-based selfie flash. 2022-08-31 15:58:41 -04:00
Alex Hart
6cb359b2d0 Prevent header decoration from passing NO_POSITION to getHeaderId. 2022-08-31 15:58:41 -04:00
Alex Hart
8bd89d1e63 Fix camera zoom issue on some devices. 2022-08-31 15:58:40 -04:00
gram-signal
f111ac7cf2 Return empty from CDSv2 refresh if current recipient list is empty.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2022-08-31 15:58:40 -04:00
Greyson Parrelli
f6e000ab97 Fix some PNI-related issues around change number. 2022-08-31 15:58:40 -04:00
Alex Hart
29869c93b2 Adjust placement of elements in first time nav. 2022-08-31 15:58:40 -04:00
Alex Hart
3aae5ce1de Fix hide text header. 2022-08-29 14:23:28 -03:00
Alex Hart
e379cf6127 Bump version to 5.48.3 2022-08-29 14:08:10 -03:00
Alex Hart
0c23cb5ca8 Updated language translations. 2022-08-29 14:07:38 -03:00
Greyson Parrelli
d26ba27069 Look at new v2 remote announcement manifest. 2022-08-29 12:38:38 -04:00
Greyson Parrelli
e918178694 Update launcher assets. 2022-08-29 11:16:49 -04:00
Alex Hart
3d075bdd65 Check for EXTRA_TEXT if we cannot parse EXTRA_STREAM. 2022-08-29 10:47:47 -03:00
Alex Hart
4a3b8af6af Utilize proper control color for text cursor in gift badge message. 2022-08-29 10:35:30 -03:00
Alex Hart
2743492076 Fix ISE when utilizing the ear piece for voice notes. 2022-08-29 10:09:28 -03:00
Alex Hart
6ebc453e4b Bump version to 5.48.2 2022-08-26 15:20:26 -03:00
Alex Hart
75bd950b9b Updated language translations. 2022-08-26 15:20:26 -03:00
Alex Hart
0b0c4eb8c0 Utilize themed colors in fallback resource photos. 2022-08-26 15:20:26 -03:00
Alex Hart
e7dbc874bb Utilize lock icon instead of group icon for distribution lists. 2022-08-26 15:20:26 -03:00
Alex Hart
17426f1dbb Add long-press action to copy sent timestamp to clipboard. 2022-08-26 15:20:26 -03:00
Alex Hart
e00ce48517 Add proper title to text story sender. 2022-08-26 15:20:26 -03:00
Alex Hart
cba1caa5be Add audio focus handling to voice note playback. 2022-08-26 15:20:26 -03:00
Alex Hart
5f6b073cb6 Do not invoke reveal animation when editing a group. 2022-08-26 15:20:26 -03:00
Alex Hart
51647a5017 Enable both use-cases if available. 2022-08-26 15:20:26 -03:00
Johan
fae2ceab39 Set correct variable for password timeout.
Fixes #12415
2022-08-26 15:20:23 -03:00
Greyson Parrelli
553346629a Update libphonenumber to 8.12.54 2022-08-25 16:30:05 -04:00
Greyson Parrelli
726f48bc33 Clear search toolbar upon opening. 2022-08-25 16:27:42 -04:00
Greyson Parrelli
397793064d Bump version to 5.48.1 2022-08-25 14:26:51 -04:00
Greyson Parrelli
534af3c1a0 Updated language translations. 2022-08-25 14:26:27 -04:00
Greyson Parrelli
f551a700fe Fix crash when searching groups for a large number of members. 2022-08-25 14:26:27 -04:00
Greyson Parrelli
d0c737779a Update profile strings. 2022-08-25 13:41:49 -04:00
Greyson Parrelli
497b38ddbf Improve the ordering of conversation search results. 2022-08-25 12:15:02 -04:00
Greyson Parrelli
cdad45096b Fix bug with back navigation during payment lock. 2022-08-25 08:51:31 -04:00
Greyson Parrelli
f8aedca08e Improve navigation to fingerprint settings. 2022-08-25 08:26:09 -04:00
Greyson Parrelli
490ca1d74c Bump version to 5.48.0 2022-08-24 18:29:19 -04:00
Greyson Parrelli
cf9ddf3960 Updated language translations. 2022-08-24 18:26:34 -04:00
Greyson Parrelli
61498037f3 Add support for PniSignatureMessages. 2022-08-24 18:16:42 -04:00
Cody Henthorne
1e499fd12f Refactor notification thumbnails to reduce chances for ANR. 2022-08-24 17:09:01 -04:00
Cody Henthorne
a9fc5622cd Add search by group membership. 2022-08-24 17:09:01 -04:00
Alex Hart
777a91abc7 SMS Exporter unit testing. 2022-08-24 17:09:01 -04:00
Varsha
372f939a67 Add support for biometric auth for payments. 2022-08-24 17:09:01 -04:00
Greyson Parrelli
716229719a Add migration to new KBS enclave. 2022-08-24 17:09:01 -04:00
Cody Henthorne
b57b160660 Add error toasts to multiforward sheet. 2022-08-24 17:09:01 -04:00
Cody Henthorne
40c52a31c9 Fix race condition when joining a group call. 2022-08-24 17:09:01 -04:00
Cody Henthorne
05c16e4c70 Retry backup verify and rename with delay. 2022-08-24 17:09:01 -04:00
Greyson Parrelli
7a7c4c28c2 Update verification-metadata to remove outdated entry. 2022-08-24 17:09:01 -04:00
Greyson Parrelli
8e2ab40b4c Update string for profile creation. 2022-08-24 17:09:01 -04:00
Greyson Parrelli
bcef73c2e0 Update some donation error strings. 2022-08-24 17:09:01 -04:00
Cody Henthorne
f0a109245b Only fallback to unidentified socket when a auth error occurs.
Fixes #12395
2022-08-24 17:09:01 -04:00
Cody Henthorne
c6c30f25a2 Attempt automated SMS verification in change number flow. 2022-08-24 17:09:01 -04:00
Cody Henthorne
8036aaa985 Reduce verbosity of phone number parse errors. 2022-08-24 17:09:01 -04:00
Greyson Parrelli
a56dd5ca87 Avoid a false positive in DeadlockDetector. 2022-08-24 17:09:01 -04:00
Greyson Parrelli
40ac0f4e89 Log media quality setting. 2022-08-24 17:09:01 -04:00
Greyson Parrelli
1aa9aa97ac Only include the first photo in quoteAttachments.
Otherwise you spend a bunch of time compressing stuff people will never
see.
2022-08-24 17:09:01 -04:00
Greyson Parrelli
96a75a7f7f Always use inferred PIN state.
Saving the PIN state could lead to it being stale or mismanaged, and tbh
we were using the inferred state to _set_ the value anyway.
2022-08-24 17:09:01 -04:00
Greyson Parrelli
5009bd4e6a Prevent usage of null itemAnimator in chat list.
Fixes #12393
2022-08-24 17:09:01 -04:00
Greyson Parrelli
62ea82a2ba Do not include pending downloads in storage usage.
Fixes #12231
2022-08-24 17:09:01 -04:00
Greyson Parrelli
fa55062510 Update ExoPlayer to 2.18.1 2022-08-24 17:09:01 -04:00
Greyson Parrelli
4b195c67cb Bump version to 5.47.3 2022-08-24 16:10:53 -04:00
Greyson Parrelli
f36aa09a81 Revert "Ensure main database is updated before opening secondary ones."
This reverts commit e0e3f7dfec.
2022-08-24 16:08:38 -04:00
Greyson Parrelli
e0f16548cf Bump version to 5.47.2 2022-08-23 14:46:30 -04:00
Greyson Parrelli
577971c7a9 Updated language translations. 2022-08-23 14:46:11 -04:00
Greyson Parrelli
6bbd941158 Fix back navigation issues when creating an initial profile. 2022-08-23 13:49:21 -04:00
Victor Ding
b92dd19a4c Use StandardCharsets in OkHttpUtil.
okhttp3.internal.Util.UTF_8 was never meant to be used outside of
okhttp3 library; and it has been deleted in later versions.
Signal should use java.nio.charset.StandardCharsets instead.
No functional change.

Closes #12413
2022-08-23 10:56:01 -04:00
Greyson Parrelli
13f3a8cf8a Fix navigation bug when deactivating payments. 2022-08-23 10:45:04 -04:00
Greyson Parrelli
60da8116be Update MobileCoin SDK to 1.2.2.2 2022-08-23 10:27:28 -04:00
Greyson Parrelli
1b7c873ea5 Bump version to 5.47.1 2022-08-22 21:02:15 -04:00
Greyson Parrelli
b18ecfdffd Updated language translations. 2022-08-22 21:01:27 -04:00
Greyson Parrelli
da286329f7 Update libsignal-client to 0.20.0 2022-08-22 20:55:24 -04:00
Greyson Parrelli
db69603b5d Fix CDS flag name. 2022-08-22 19:18:37 -04:00
Cody Henthorne
a2b73bf979 Make single badge appear selected. 2022-08-22 12:03:43 -04:00
Cody Henthorne
dc503e3406 Prevent video thumbnail creation from crashing the app. 2022-08-22 11:52:37 -04:00
Cody Henthorne
ab3e0b87c6 Bump version to 5.47.0 2022-08-18 16:14:08 -04:00
Cody Henthorne
7751ce3ae0 Updated language translations. 2022-08-18 16:05:30 -04:00
Alex Hart
796e98be10 Utilize proper intent creation when launching profile creator from PassphraseRequiredActivity. 2022-08-18 16:01:05 -04:00
Greyson Parrelli
9c266e7995 Remove legacy fields from the Envelope. 2022-08-18 16:01:05 -04:00
Alex Hart
b4ae13fe8a Catch IAE when video thumbnail extractor cannot instantiate a decoder. 2022-08-18 16:01:05 -04:00
Alex Hart
8ffad4cc6f Upgrade gradle version to v7.5.1
Fixes #12399

Co-Authored-By: Patryk Miś <foss@patrykmis.com>
2022-08-18 16:01:05 -04:00
Alex Hart
f341e02fb7 Story privacy screen updates. 2022-08-18 16:01:05 -04:00
Greyson Parrelli
15e52a8b88 Add ability to do unused reads from CDSv2 to test server load. 2022-08-18 16:01:05 -04:00
Cody Henthorne
84717b95f7 Add logging around how call activity is started. 2022-08-18 16:01:05 -04:00
Cody Henthorne
b1d1e92dbb Fix group call remote video not rendering. 2022-08-18 16:01:05 -04:00
Cody Henthorne
cca35ec687 Dust off remote megaphone for upcoming donate megaphone. 2022-08-18 16:01:05 -04:00
Greyson Parrelli
95fc9d6c3c Add support for PNIs in storage service. 2022-08-18 16:01:05 -04:00
Alex Hart
cb057968ee Move gift header into recycler. 2022-08-18 16:01:05 -04:00
Thilo
deed8ac6c9 Add monochrome entry to support Themed App Icons.
Fixes #12385
2022-08-18 16:01:05 -04:00
Alex Hart
fe44f8e369 Add hard-code of colors in numeric keyboard to light mode. 2022-08-18 16:01:05 -04:00
Alex Hart
e517232172 Sort "new group story" entries by recency. 2022-08-18 16:01:05 -04:00
Alex Hart
28310a88f5 Username UX refresh. 2022-08-18 16:01:05 -04:00
Cody Henthorne
3252871ed5 Replace prekey jobs with one overall sync job. 2022-08-18 16:01:05 -04:00
Cody Henthorne
2740b5e300 Fix emoji completion over newlines bug. 2022-08-18 09:27:20 -03:00
Alex Hart
a46faebb67 Add check before trying to launch contact add intent. 2022-08-18 09:27:20 -03:00
Alex Hart
16a4c321c4 Add additional logging for diagnosing shares with null EXTRA_STREAM. 2022-08-18 09:27:20 -03:00
Alex Hart
056ef84817 Change initial my story privacy fragment peek size to 1. 2022-08-18 09:27:20 -03:00
Alex Hart
820d76990a Add click handler to prevent tap propagation. 2022-08-18 09:27:20 -03:00
Alex Hart
01e4a7fd79 Add My Story row polish. 2022-08-18 09:27:20 -03:00
Alex Hart
8d4f87641d Update stories send preview scroll mode to none. 2022-08-18 09:27:20 -03:00
Alex Hart
afb248c57c Set link preview max width to 280dp. 2022-08-18 09:27:20 -03:00
Greyson Parrelli
62871c1bdd Update keepalive interval to ping every 30sec. 2022-08-18 09:27:20 -03:00
Greyson Parrelli
c6be427883 Add support for resending badly-encrypted stories. 2022-08-18 09:27:20 -03:00
Cody Henthorne
7873ec2b67 Bump version to 5.46.6 2022-08-17 20:28:08 -04:00
Cody Henthorne
64e6b492ab Updated language translations. 2022-08-17 20:14:03 -04:00
Alex Hart
c1cd893a4a Bump version to 5.46.5 2022-08-16 15:27:33 -03:00
Alex Hart
8a1e033efa Updated language translations. 2022-08-16 15:27:33 -03:00
Alex Hart
b2c974a684 Add distinctUntilChanged operator to security info flowable. 2022-08-16 15:27:33 -03:00
Cody Henthorne
57e476988e Fix release channel donation bug. 2022-08-16 12:18:18 -04:00
786 changed files with 367156 additions and 291138 deletions

View File

@@ -57,8 +57,8 @@ ktlint {
version = "0.43.2" version = "0.43.2"
} }
def canonicalVersionCode = 1109 def canonicalVersionCode = 1130
def canonicalVersionName = "5.46.4" def canonicalVersionName = "5.51.1"
def postFixSize = 100 def postFixSize = 100
def abiPostFix = ['universal' : 0, def abiPostFix = ['universal' : 0,
@@ -201,11 +201,13 @@ android {
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDS_MRENCLAVE", "\"74778bb0f93ae1f78c26e67152bab0bbeb693cd56d1bb9b4e9244157acc58081\"" buildConfigField "String", "CDS_MRENCLAVE", "\"74778bb0f93ae1f78c26e67152bab0bbeb693cd56d1bb9b4e9244157acc58081\""
buildConfigField "String", "CDSI_MRENCLAVE", "\"7b75dd6e862decef9b37132d54be082441917a7790e82fe44f9cf653de03a75f\"" buildConfigField "String", "CDSI_MRENCLAVE", "\"ef4787a56a154ac6d009138cac17155acd23cfe4329281252365dd7c252e7fbf\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " + buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " + "\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")" "\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]" buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\"" buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
@@ -354,10 +356,12 @@ android {
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"" buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"74778bb0f93ae1f78c26e67152bab0bbeb693cd56d1bb9b4e9244157acc58081\"" buildConfigField "String", "CDS_MRENCLAVE", "\"74778bb0f93ae1f78c26e67152bab0bbeb693cd56d1bb9b4e9244157acc58081\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " + buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " + "\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")" "\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]" buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] { new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\") }"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\"" buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\""
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"" buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
@@ -413,10 +417,11 @@ dependencies {
implementation (libs.androidx.appcompat) { implementation (libs.androidx.appcompat) {
version { version {
strictly '1.2.0' strictly '1.5.1'
} }
} }
implementation libs.androidx.window implementation libs.androidx.window.window
implementation libs.androidx.window.java
implementation libs.androidx.recyclerview implementation libs.androidx.recyclerview
implementation libs.material.material implementation libs.material.material
implementation libs.androidx.legacy.support implementation libs.androidx.legacy.support
@@ -429,7 +434,9 @@ dependencies {
implementation libs.androidx.multidex implementation libs.androidx.multidex
implementation libs.androidx.navigation.fragment.ktx implementation libs.androidx.navigation.fragment.ktx
implementation libs.androidx.navigation.ui.ktx implementation libs.androidx.navigation.ui.ktx
implementation libs.androidx.lifecycle.extensions implementation libs.androidx.lifecycle.viewmodel.ktx
implementation libs.androidx.lifecycle.livedata.ktx
implementation libs.androidx.lifecycle.process
implementation libs.androidx.lifecycle.viewmodel.savedstate implementation libs.androidx.lifecycle.viewmodel.savedstate
implementation libs.androidx.lifecycle.common.java8 implementation libs.androidx.lifecycle.common.java8
implementation libs.androidx.lifecycle.reactivestreams.ktx implementation libs.androidx.lifecycle.reactivestreams.ktx
@@ -466,6 +473,7 @@ dependencies {
implementation project(':donations') implementation project(':donations')
implementation project(':contacts') implementation project(':contacts')
implementation project(':qr') implementation project(':qr')
implementation project(':sms-exporter')
implementation libs.libsignal.android implementation libs.libsignal.android
implementation libs.google.protobuf.javalite implementation libs.google.protobuf.javalite
@@ -556,6 +564,10 @@ dependencies {
androidTestImplementation testLibs.mockito.kotlin androidTestImplementation testLibs.mockito.kotlin
androidTestImplementation testLibs.square.okhttp.mockserver androidTestImplementation testLibs.square.okhttp.mockserver
instrumentationImplementation (libs.androidx.fragment.testing) {
exclude group: 'androidx.test', module: 'core'
}
testImplementation testLibs.espresso.core testImplementation testLibs.espresso.core
implementation libs.kotlin.stdlib.jdk8 implementation libs.kotlin.stdlib.jdk8
@@ -566,7 +578,7 @@ dependencies {
implementation libs.rxjava3.rxkotlin implementation libs.rxjava3.rxkotlin
implementation libs.rxdogtag implementation libs.rxdogtag
androidTestUtil 'androidx.test:orchestrator:1.4.1' androidTestUtil testLibs.androidx.test.orchestrator
} }
def getLastCommitTimestamp() { def getLastCommitTimestamp() {

View File

@@ -34,6 +34,7 @@ import org.thoughtcrime.securesms.testing.success
import org.thoughtcrime.securesms.testing.timeout import org.thoughtcrime.securesms.testing.timeout
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.MismatchedDevices
import org.whispersystems.signalservice.internal.push.PreKeyState import org.whispersystems.signalservice.internal.push.PreKeyState
import java.util.UUID import java.util.UUID
@@ -249,6 +250,109 @@ class ChangeNumberViewModelTest {
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest) assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
} }
@Test
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = ServiceId.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.deviceMessages.isEmpty()) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2)
extraDevices = emptyList()
}
)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Get("/v2/keys/$aci/2") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = ServiceId.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
MockProvider.mockGetRegistrationLockStringFlow(kbsRepository)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
} else if (changeNumberRequest.deviceMessages.isEmpty()) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2)
extraDevices = emptyList()
}
)
} else if (changeNumberRequest.deviceMessages.size == 1) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2, 3)
extraDevices = emptyList()
}
)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Get("/v2/keys/$aci/2") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
},
Get("/v2/keys/$aci/3") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 3))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
processor.registrationLock() assertIs true
Recipient.self().requirePni() assertIsNot newPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) { private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) {
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni() val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore = SignalStore.account().pniPreKeys val pniMetadataStore = SignalStore.account().pniPreKeys

View File

@@ -6,6 +6,7 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.DistributionListId
@@ -116,6 +117,7 @@ class MmsDatabaseTest_stories {
assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0) assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0)
} }
@Ignore
@Test @Test
fun given5ViewedStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectLatestViewedFirst() { fun given5ViewedStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectLatestViewedFirst() {
// GIVEN // GIVEN
@@ -257,12 +259,13 @@ class MmsDatabaseTest_stories {
) )
// WHEN // WHEN
val result = mms.hasSelfReplyInGroupStory(groupStoryId) val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
// THEN // THEN
assertFalse(result) assertFalse(result)
} }
@Ignore
@Test @Test
fun givenAGroupStoryWithAReplyFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectTrue() { fun givenAGroupStoryWithAReplyFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectTrue() {
// GIVEN // GIVEN
@@ -281,7 +284,7 @@ class MmsDatabaseTest_stories {
) )
// WHEN // WHEN
val result = mms.hasSelfReplyInGroupStory(groupStoryId) val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
// THEN // THEN
assertTrue(result) assertTrue(result)
@@ -306,7 +309,7 @@ class MmsDatabaseTest_stories {
) )
// WHEN // WHEN
val result = mms.hasSelfReplyInGroupStory(groupStoryId) val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
// THEN // THEN
assertFalse(result) assertFalse(result)
@@ -334,7 +337,7 @@ class MmsDatabaseTest_stories {
) )
// WHEN // WHEN
val result = mms.hasSelfReplyInGroupStory(groupStoryId) val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
// THEN // THEN
assertFalse(result) assertFalse(result)

View File

@@ -18,6 +18,77 @@ class RecipientDatabaseTest {
@get:Rule @get:Rule
val harness = SignalActivityRule() val harness = SignalActivityRule()
@Test
fun givenAHiddenRecipient_whenIQueryAllContacts_thenIDoNotExpectHiddenToBeReturned() {
val hiddenRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results = SignalDatabase.recipients.queryAllContacts("Hidden")!!
assertEquals(0, results.count)
}
@Test
fun givenAHiddenRecipient_whenIGetSignalContacts_thenIDoNotExpectHiddenToBeReturned() {
val hiddenRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(false)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
}
ids
}!!
assertNotEquals(0, results.size)
assertFalse(hiddenRecipient in results)
}
@Test
fun givenAHiddenRecipient_whenIQuerySignalContacts_thenIDoNotExpectHiddenToBeReturned() {
val hiddenRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results = SignalDatabase.recipients.querySignalContacts("Hidden", false)!!
assertEquals(0, results.count)
}
@Test
fun givenAHiddenRecipient_whenIQueryNonGroupContacts_thenIDoNotExpectHiddenToBeReturned() {
val hiddenRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results = SignalDatabase.recipients.queryNonGroupContacts("Hidden", false)!!
assertEquals(0, results.count)
}
@Test
fun givenAHiddenRecipient_whenIGetNonGroupContacts_thenIDoNotExpectHiddenToBeReturned() {
val hiddenRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(false)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientDatabase.ID)))
}
ids
}!!
assertNotEquals(0, results.size)
assertFalse(hiddenRecipient in results)
}
@Test @Test
fun givenABlockedRecipient_whenIQueryAllContacts_thenIDoNotExpectBlockedToBeReturned() { fun givenABlockedRecipient_whenIQueryAllContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0] val blockedRecipient = harness.others[0]

View File

@@ -60,13 +60,6 @@ class RecipientDatabaseTest_processPnpTuple {
} }
} }
@Test(expected = IllegalStateException::class)
fun noMatch_pniOnly() {
test {
process(null, PNI_A, null)
}
}
@Test(expected = IllegalStateException::class) @Test(expected = IllegalStateException::class)
fun noMatch_noData() { fun noMatch_noData() {
test { test {
@@ -417,7 +410,7 @@ class RecipientDatabaseTest_processPnpTuple {
fun process(e164: String?, pni: PNI?, aci: ACI?) { fun process(e164: String?, pni: PNI?, aci: ACI?) {
SignalDatabase.rawDatabase.beginTransaction() SignalDatabase.rawDatabase.beginTransaction()
try { try {
generatedIds += recipientDatabase.processPnpTuple(e164, pni, aci, pniVerified = false, pnpEnabled = true).finalId generatedIds += recipientDatabase.processPnpTuple(e164, pni, aci, pniVerified = false).finalId
SignalDatabase.rawDatabase.setTransactionSuccessful() SignalDatabase.rawDatabase.setTransactionSuccessful()
} finally { } finally {
SignalDatabase.rawDatabase.endTransaction() SignalDatabase.rawDatabase.endTransaction()

View File

@@ -67,11 +67,6 @@ class RecipientDatabaseTest_processPnpTupleToChangeSet {
) )
} }
@Test(expected = IllegalStateException::class)
fun noMatch_pniOnly() {
db.processPnpTupleToChangeSet(null, PNI_A, null, pniVerified = false)
}
@Test(expected = IllegalStateException::class) @Test(expected = IllegalStateException::class)
fun noMatch_noData() { fun noMatch_noData() {
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false) db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)

View File

@@ -109,7 +109,7 @@ class MyStoryMigrationTest {
} }
private fun runMigration() { private fun runMigration() {
MyStoryMigration.migrate( V151_MyStoryMigration.migrate(
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application, InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
SignalDatabase.rawDatabase, SignalDatabase.rawDatabase,
0, 0,

View File

@@ -77,6 +77,7 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
serviceNetworkAccessMock = mock { serviceNetworkAccessMock = mock {
on { getConfiguration() } doReturn uncensoredConfiguration on { getConfiguration() } doReturn uncensoredConfiguration
on { getConfiguration(any()) } doReturn uncensoredConfiguration on { getConfiguration(any()) } doReturn uncensoredConfiguration
on { uncensoredConfiguration } doReturn uncensoredConfiguration
} }
keyBackupService = mock() keyBackupService = mock()

View File

@@ -0,0 +1,212 @@
package org.thoughtcrime.securesms.jobs
import androidx.test.ext.junit.runners.AndroidJUnit4
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.libsignal.protocol.ecc.Curve
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.Put
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.testing.parsedRequestBody
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
import org.whispersystems.signalservice.internal.push.PreKeyState
import org.whispersystems.signalservice.internal.push.PreKeyStatus
@RunWith(AndroidJUnit4::class)
class PreKeysSyncJobTest {
@get:Rule
val harness = SignalActivityRule()
private val aciPreKeyMeta: PreKeyMetadataStore
get() = SignalStore.account().aciPreKeys
private val pniPreKeyMeta: PreKeyMetadataStore
get() = SignalStore.account().pniPreKeys
private lateinit var job: PreKeysSyncJob
@Before
fun setUp() {
job = PreKeysSyncJob()
}
@After
fun tearDown() {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
/**
* Create signed prekeys for both identities when both do not have registered prekeys according
* to our local state.
*/
@Test
fun runWithoutRegisteredKeysForBothIdentities() {
// GIVEN
aciPreKeyMeta.isSignedPreKeyRegistered = false
pniPreKeyMeta.isSignedPreKeyRegistered = false
lateinit var aciSignedPreKey: SignedPreKeyEntity
lateinit var pniSignedPreKey: SignedPreKeyEntity
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v2/keys/signed?identity=aci") { r ->
aciSignedPreKey = r.parsedRequestBody()
MockResponse().success()
},
Put("/v2/keys/signed?identity=pni") { r ->
pniSignedPreKey = r.parsedRequestBody()
MockResponse().success()
},
)
// WHEN
val result: Job.Result = job.run()
// THEN
result.isSuccess assertIs true
aciPreKeyMeta.isSignedPreKeyRegistered assertIs true
pniPreKeyMeta.isSignedPreKeyRegistered assertIs true
val aciVerifySignatureResult = Curve.verifySignature(
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.publicKey,
aciSignedPreKey.publicKey.serialize(),
aciSignedPreKey.signature
)
aciVerifySignatureResult assertIs true
val pniVerifySignatureResult = Curve.verifySignature(
ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.publicKey,
pniSignedPreKey.publicKey.serialize(),
pniSignedPreKey.signature
)
pniVerifySignatureResult assertIs true
}
/**
* With 100 prekeys registered for each identity, do nothing.
*/
@Test
fun runWithRegisteredKeysForBothIdentities() {
// GIVEN
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(100)) },
)
// WHEN
val result: Job.Result = job.run()
// THEN
result.isSuccess assertIs true
aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
pniPreKeyMeta.activeSignedPreKeyId assertIs currentPniKeyId
}
/**
* With 100 prekeys registered for ACI, but no PNI prekeys registered according to local state,
* do nothing for ACI but create PNI prekeys and update local state.
*/
@Test
fun runWithRegisteredKeysForAciIdentityOnly() {
// GIVEN
pniPreKeyMeta.isSignedPreKeyRegistered = false
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
Put("/v2/keys/signed?identity=pni") { MockResponse().success() },
)
// WHEN
val result: Job.Result = job.run()
// THEN
result.isSuccess assertIs true
pniPreKeyMeta.isSignedPreKeyRegistered assertIs true
aciPreKeyMeta.activeSignedPreKeyId assertIs currentAciKeyId
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
}
/**
* With <10 prekeys registered for each identity, upload new.
*/
@Test
fun runWithLowNumberOfRegisteredKeysForBothIdentities() {
// GIVEN
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
val currentNextAciPreKeyId = aciPreKeyMeta.nextOneTimePreKeyId
val currentNextPniPreKeyId = pniPreKeyMeta.nextOneTimePreKeyId
lateinit var aciPreKeyStateRequest: PreKeyState
lateinit var pniPreKeyStateRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(5)) },
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(5)) },
Put("/v2/keys/?identity=aci") { r ->
aciPreKeyStateRequest = r.parsedRequestBody()
MockResponse().success()
},
Put("/v2/keys/?identity=pni") { r ->
pniPreKeyStateRequest = r.parsedRequestBody()
MockResponse().success()
},
)
// WHEN
val result: Job.Result = job.run()
// THEN
result.isSuccess assertIs true
aciPreKeyMeta.activeSignedPreKeyId assertIsNot currentAciKeyId
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
aciPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextAciPreKeyId
pniPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextPniPreKeyId
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.let { aciIdentityKey ->
aciPreKeyStateRequest.identityKey assertIs aciIdentityKey
val verifySignatureResult = Curve.verifySignature(
aciIdentityKey.publicKey,
aciPreKeyStateRequest.signedPreKey.publicKey.serialize(),
aciPreKeyStateRequest.signedPreKey.signature
)
verifySignatureResult assertIs true
}
ApplicationDependencies.getProtocolStore().pni().identityKeyPair.publicKey.let { pniIdentityKey ->
pniPreKeyStateRequest.identityKey assertIs pniIdentityKey
val verifySignatureResult = Curve.verifySignature(
pniIdentityKey.publicKey,
pniPreKeyStateRequest.signedPreKey.publicKey.serialize(),
pniPreKeyStateRequest.signedPreKey.signature
)
verifySignatureResult assertIs true
}
}
}

View File

@@ -0,0 +1,138 @@
package org.thoughtcrime.securesms.profiles.manage
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.testing.FragmentScenario
import androidx.fragment.app.testing.launchFragmentInContainer
import androidx.lifecycle.Lifecycle
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isEnabled
import androidx.test.espresso.matcher.ViewMatchers.isNotEnabled
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.reactivex.rxjava3.schedulers.TestScheduler
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.testing.Put
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsNull
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
import java.util.concurrent.TimeUnit
@RunWith(AndroidJUnit4::class)
class UsernameEditFragmentTest {
@get:Rule
val harness = SignalActivityRule(othersCount = 10)
private val ioScheduler = TestScheduler()
private val computationScheduler = TestScheduler()
@get:Rule
val testSchedulerRule = RxTestSchedulerRule(
ioTestScheduler = ioScheduler,
computationTestScheduler = computationScheduler
)
@After
fun tearDown() {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@Test
fun testUsernameCreationInRegistration() {
val scenario = createScenario(true)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
noViewFoundException.assertIsNull()
val toolbar = view as Toolbar
toolbar.navigationIcon.assertIsNull()
}
onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed()))
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Test
fun testUsernameCreationOutsideOfRegistration() {
val scenario = createScenario()
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
noViewFoundException.assertIsNull()
val toolbar = view as Toolbar
toolbar.navigationIcon.assertIsNotNull()
}
onView(withText(R.string.UsernameEditFragment_username)).check(matches(isDisplayed()))
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Test
fun testNicknameUpdateHappyPath() {
val nickname = "Spiderman"
val discriminator = "4578"
val username = "$nickname${UsernameState.DELIMITER}$discriminator"
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/accounts/username/reserved") {
MockResponse().success(ReserveUsernameResponse(username, "reservationToken"))
},
Put("/v1/accounts/username/confirm") {
MockResponse().success()
}
)
val scenario = createScenario(isInRegistration = true)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.username_text)).perform(typeText(nickname))
computationScheduler.advanceTimeBy(501, TimeUnit.MILLISECONDS)
computationScheduler.triggerActions()
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
ioScheduler.triggerActions()
computationScheduler.triggerActions()
onView(withId(R.id.username_text)).perform(closeSoftKeyboard())
onView(withId(R.id.username_done_button)).check(matches(isDisplayed()))
onView(withId(R.id.username_done_button)).check(matches(isEnabled()))
onView(withText(username)).check(matches(isDisplayed()))
onView(withId(R.id.username_done_button)).perform(click())
computationScheduler.triggerActions()
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
}
private fun createScenario(isInRegistration: Boolean = false): FragmentScenario<UsernameEditFragment> {
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle()
return launchFragmentInContainer(
fragmentArgs = fragmentArgs,
themeResId = R.style.Signal_DayNight_NoActionBar
)
}
}

View File

@@ -6,7 +6,14 @@ import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock import org.mockito.kotlin.mock
import org.mockito.kotlin.stub import org.mockito.kotlin.stub
import org.signal.core.util.Hex import org.signal.core.util.Hex
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.protocol.util.Medium
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository import org.thoughtcrime.securesms.pin.KbsRepository
import org.thoughtcrime.securesms.pin.TokenData import org.thoughtcrime.securesms.pin.TokenData
import org.thoughtcrime.securesms.test.BuildConfig import org.thoughtcrime.securesms.test.BuildConfig
@@ -16,10 +23,14 @@ import org.whispersystems.signalservice.api.kbs.HashedPin
import org.whispersystems.signalservice.api.kbs.MasterKey import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
import org.whispersystems.signalservice.internal.ServiceResponse import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse import org.whispersystems.signalservice.internal.contacts.entities.TokenResponse
import org.whispersystems.signalservice.internal.push.AuthCredentials import org.whispersystems.signalservice.internal.push.AuthCredentials
import org.whispersystems.signalservice.internal.push.DeviceInfoList import org.whispersystems.signalservice.internal.push.DeviceInfoList
import org.whispersystems.signalservice.internal.push.PreKeyEntity
import org.whispersystems.signalservice.internal.push.PreKeyResponse
import org.whispersystems.signalservice.internal.push.PreKeyResponseItem
import org.whispersystems.signalservice.internal.push.PushServiceSocket import org.whispersystems.signalservice.internal.push.PushServiceSocket
import org.whispersystems.signalservice.internal.push.SenderCertificate import org.whispersystems.signalservice.internal.push.SenderCertificate
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
@@ -83,4 +94,21 @@ object MockProvider {
on { newRegistrationSession(any(), any()) } doReturn session on { newRegistrationSession(any(), any()) } doReturn session
} }
} }
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account().aciIdentityKey, deviceId: Int): PreKeyResponse {
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())
val device = PreKeyResponseItem().apply {
this.deviceId = deviceId
registrationId = KeyHelper.generateRegistrationId(false)
signedPreKey = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
preKey = PreKeyEntity(oneTimePreKey.id, oneTimePreKey.keyPair.publicKey)
}
return PreKeyResponse().apply {
identityKey = identity.publicKey
devices = listOf(device)
}
}
} }

View File

@@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.testing
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.TestScheduler
import org.junit.rules.ExternalResource
/**
* JUnit Rule which initialises Rx thread schedulers. If a specific
* scheduler is not specified, it defaults to the `defaultTestScheduler`
*/
class RxTestSchedulerRule(
val defaultTestScheduler: TestScheduler = TestScheduler(),
val ioTestScheduler: TestScheduler = defaultTestScheduler,
val computationTestScheduler: TestScheduler = defaultTestScheduler,
val singleTestScheduler: TestScheduler = defaultTestScheduler,
val newThreadTestScheduler: TestScheduler = defaultTestScheduler,
) : ExternalResource() {
override fun before() {
RxJavaPlugins.setInitIoSchedulerHandler { ioTestScheduler }
RxJavaPlugins.setIoSchedulerHandler { ioTestScheduler }
RxJavaPlugins.setInitComputationSchedulerHandler { computationTestScheduler }
RxJavaPlugins.setComputationSchedulerHandler { computationTestScheduler }
RxJavaPlugins.setInitSingleSchedulerHandler { singleTestScheduler }
RxJavaPlugins.setSingleSchedulerHandler { singleTestScheduler }
RxJavaPlugins.setInitNewThreadSchedulerHandler { newThreadTestScheduler }
RxJavaPlugins.setNewThreadSchedulerHandler { newThreadTestScheduler }
}
override fun after() {
RxJavaPlugins.reset()
}
}

View File

@@ -108,7 +108,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i))) val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i")) SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew()) SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true)) SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true) SignalDatabase.recipients.setProfileSharing(recipientId, true)
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey) ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey)
others += recipientId others += recipientId

View File

@@ -155,7 +155,8 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceProvisioningActivity" <activity android:name=".DeviceProvisioningActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -181,10 +182,10 @@
<activity android:name=".sharing.v2.ShareActivity" <activity android:name=".sharing.v2.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="true"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
@@ -212,13 +213,14 @@
</activity> </activity>
<activity android:name=".stickers.StickerPackPreviewActivity" <activity android:name=".stickers.StickerPackPreviewActivity"
android:exported="true"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask" android:launchMode="singleTask"
android:noHistory="true" android:noHistory="true"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" android:exported="true" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="sgnl" <data android:scheme="sgnl"
@@ -255,6 +257,7 @@
</activity-alias> </activity-alias>
<activity android:name=".deeplinks.DeepLinkEntryActivity" <activity android:name=".deeplinks.DeepLinkEntryActivity"
android:exported="true"
android:noHistory="true" android:noHistory="true"
android:theme="@style/Signal.Transparent"> android:theme="@style/Signal.Transparent">
@@ -386,10 +389,12 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".verify.VerifyIdentityActivity" <activity android:name=".verify.VerifyIdentityActivity"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar" android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".components.settings.app.AppSettingsActivity" <activity android:name=".components.settings.app.AppSettingsActivity"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar" android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
@@ -517,10 +522,11 @@
android:finishOnTaskLaunch="true" /> android:finishOnTaskLaunch="true" />
<activity android:name=".PlayServicesProblemActivity" <activity android:name=".PlayServicesProblemActivity"
android:exported="false"
android:theme="@style/TextSecure.DialogActivity" android:theme="@style/TextSecure.DialogActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".SmsSendtoActivity"> <activity android:name=".SmsSendtoActivity" android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SENDTO" /> <action android:name="android.intent.action.SENDTO" />
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@@ -539,6 +545,7 @@
</activity> </activity>
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare" <activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
android:exported="true"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:permission="android.permission.CALL_PHONE" android:permission="android.permission.CALL_PHONE"
android:theme="@style/NoAnimation.Theme.BlackScreen" android:theme="@style/NoAnimation.Theme.BlackScreen"
@@ -554,7 +561,7 @@
</activity> </activity>
<activity android:name=".mediasend.AvatarSelectionActivity" <activity android:name=".mediasend.AvatarSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia" android:theme="@style/TextSecure.DarkNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".blocked.BlockedUsersActivity" <activity android:name=".blocked.BlockedUsersActivity"
@@ -570,6 +577,10 @@
android:theme="@style/TextSecure.LightRegistrationTheme" android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateVisible|adjustResize" /> android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".profiles.username.AddAUsernameActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".profiles.manage.ManageProfileActivity" <activity android:name=".profiles.manage.ManageProfileActivity"
android:theme="@style/TextSecure.LightTheme" android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize" /> android:windowSoftInputMode="stateVisible|adjustResize" />
@@ -659,7 +670,7 @@
<activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity" <activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.FullScreenMedia" /> android:theme="@style/TextSecure.DarkNoActionBar" />
<activity android:name=".wallpaper.crop.WallpaperCropActivity" <activity android:name=".wallpaper.crop.WallpaperCropActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -670,6 +681,12 @@
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".exporter.flow.SmsExportActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<service android:enabled="true" android:name=".exporter.SignalSmsExportService" android:foregroundServiceType="dataSync" />
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService" android:foregroundServiceType="camera|microphone"/> <service android:enabled="true" android:name=".service.webrtc.WebRtcCallService" android:foregroundServiceType="camera|microphone"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/> <service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/> <service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
@@ -682,13 +699,13 @@
</intent-filter> </intent-filter>
</service> </service>
<service android:name=".components.voice.VoiceNotePlaybackService"> <service android:name=".components.voice.VoiceNotePlaybackService" android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.media.browse.MediaBrowserService" /> <action android:name="android.media.browse.MediaBrowserService" />
</intent-filter> </intent-filter>
</service> </service>
<receiver android:name="androidx.media.session.MediaButtonReceiver" > <receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" /> <action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter> </intent-filter>
@@ -728,7 +745,7 @@
<service android:name=".gcm.FcmFetchForegroundService" /> <service android:name=".gcm.FcmFetchForegroundService" />
<service android:name=".gcm.FcmReceiveService"> <service android:name=".gcm.FcmReceiveService" android:exported="true">
<intent-filter> <intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" /> <action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter> </intent-filter>
@@ -819,51 +836,51 @@
</provider> </provider>
<receiver android:name=".service.BootReceiver"> <receiver android:name=".service.BootReceiver" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="org.thoughtcrime.securesms.RESTART"/> <action android:name="org.thoughtcrime.securesms.RESTART"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".service.DirectoryRefreshListener"> <receiver android:name=".service.DirectoryRefreshListener" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".service.RotateSignedPreKeyListener"> <receiver android:name=".service.RotateSignedPreKeyListener" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".service.RotateSenderCertificateListener"> <receiver android:name=".service.RotateSenderCertificateListener" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".messageprocessingalarm.MessageProcessReceiver"> <receiver android:name=".messageprocessingalarm.MessageProcessReceiver" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="org.thoughtcrime.securesms.action.PROCESS_MESSAGES" /> <action android:name="org.thoughtcrime.securesms.action.PROCESS_MESSAGES" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".service.LocalBackupListener"> <receiver android:name=".service.LocalBackupListener" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".service.PersistentConnectionBootListener"> <receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name=".notifications.LocaleChangedReceiver"> <receiver android:name=".notifications.LocaleChangedReceiver" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED"/> <action android:name="android.intent.action.LOCALE_CHANGED"/>
</intent-filter> </intent-filter>
@@ -871,7 +888,7 @@
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver"/> <receiver android:name=".notifications.MessageNotifier$ReminderReceiver"/>
<receiver android:name=".notifications.DeleteNotificationReceiver"> <receiver android:name=".notifications.DeleteNotificationReceiver" android:exported="false">
<intent-filter> <intent-filter>
<action android:name="org.thoughtcrime.securesms.DELETE_NOTIFICATION"/> <action android:name="org.thoughtcrime.securesms.DELETE_NOTIFICATION"/>
</intent-filter> </intent-filter>
@@ -899,16 +916,19 @@
<service <service
android:name=".jobmanager.KeepAliveService" android:name=".jobmanager.KeepAliveService"
android:enabled="@bool/enable_alarm_manager" /> android:enabled="@bool/enable_alarm_manager"
android:exported="false"/>
<receiver <receiver
android:name=".jobmanager.AlarmManagerScheduler$RetryReceiver" android:name=".jobmanager.AlarmManagerScheduler$RetryReceiver"
android:enabled="@bool/enable_alarm_manager" /> android:enabled="@bool/enable_alarm_manager"
android:exported="false"/>
<!-- Probably don't need this one --> <!-- Probably don't need this one -->
<receiver <receiver
android:name=".jobmanager.BootReceiver" android:name=".jobmanager.BootReceiver"
android:enabled="true"> android:enabled="true"
android:exported="false">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>

View File

@@ -0,0 +1,41 @@
package androidx.documentfile.provider;
import android.content.Context;
import android.net.Uri;
import android.provider.DocumentsContract;
import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
/**
* Located in androidx package as {@link TreeDocumentFile} is package protected.
*/
public class DocumentFileHelper {
private static final String TAG = Log.tag(DocumentFileHelper.class);
/**
* System implementation swallows the exception and we are having problems with the rename. This inlines the
* same call and logs the exception. Note this implementation does not update the passed in document file like
* the system implementation. Do not use the provided document file after calling this method.
*
* @return true if rename successful
*/
@RequiresApi(21)
public static boolean renameTo(Context context, DocumentFile documentFile, String displayName) {
if (documentFile instanceof TreeDocumentFile) {
Log.d(TAG, "Renaming document directly");
try {
final Uri result = DocumentsContract.renameDocument(context.getContentResolver(), documentFile.getUri(), displayName);
return result != null;
} catch (Exception e) {
Log.w(TAG, "Unable to rename document file", e);
return false;
}
} else {
Log.d(TAG, "Letting OS rename document: " + documentFile.getClass().getSimpleName());
return documentFile.renameTo(displayName);
}
}
}

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.AccountAttributes;
@@ -20,6 +21,6 @@ public final class AppCapabilities {
* asking if the user has set a Signal PIN or not. * asking if the user has set a Signal PIN or not.
*/ */
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) { public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories(), FeatureFlags.giftBadgeReceiveSupport()); return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, Stories.isFeatureFlagEnabled(), FeatureFlags.giftBadgeReceiveSupport(), FeatureFlags.phoneNumberPrivacy());
} }
} }

View File

@@ -50,20 +50,21 @@ import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji; import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmJobService; import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob; import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob; import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob; import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.FontDownloaderJob; import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob; import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob; import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob; import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob; import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob; import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.PersistentLogger;
@@ -180,13 +181,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(this::initializeRevealableMessageManager) .addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager) .addNonBlocking(this::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeFcmCheck) .addNonBlocking(this::initializeFcmCheck)
.addNonBlocking(CreateSignedPreKeyJob::enqueueIfNeeded) .addNonBlocking(PreKeysSyncJob::enqueueIfNeeded)
.addNonBlocking(this::initializePeriodicTasks) .addNonBlocking(this::initializePeriodicTasks)
.addNonBlocking(this::initializeCircumvention) .addNonBlocking(this::initializeCircumvention)
.addNonBlocking(this::initializePendingMessages) .addNonBlocking(this::initializePendingMessages)
.addNonBlocking(this::initializeCleanup) .addNonBlocking(this::initializeCleanup)
.addNonBlocking(this::initializeGlideCodecs) .addNonBlocking(this::initializeGlideCodecs)
.addNonBlocking(RefreshPreKeysJob::scheduleIfNecessary)
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync) .addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop()) .addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
.addNonBlocking(EmojiSource::refresh) .addNonBlocking(EmojiSource::refresh)
@@ -196,6 +196,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this)) .addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager) .addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this))) .addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
.addPostRender(this::initializeTrimThreadsByDateManager)
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this)) .addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary) .addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge())) .addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
@@ -206,6 +207,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary) .addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary) .addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded) .addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
.execute(); .execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -385,6 +387,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary(); ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
} }
private void initializeTrimThreadsByDateManager() {
KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration();
if (keepMessagesDuration != KeepMessagesDuration.FOREVER) {
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
}
}
private void initializePeriodicTasks() { private void initializePeriodicTasks() {
RotateSignedPreKeyListener.schedule(this); RotateSignedPreKeyListener.schedule(this);
DirectoryRefreshListener.schedule(this); DirectoryRefreshListener.schedule(this);

View File

@@ -0,0 +1,80 @@
package org.thoughtcrime.securesms
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.activity.result.contract.ActivityResultContract
import androidx.annotation.RequiresApi
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.PromptInfo
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.util.ServiceUtil
/**
* Authentication using phone biometric (face, fingerprint recognition) or device lock (pattern, pin or passphrase).
*/
class BiometricDeviceAuthentication(
private val biometricManager: BiometricManager,
private val biometricPrompt: BiometricPrompt,
private val biometricPromptInfo: PromptInfo
) {
companion object {
const val AUTHENTICATED = 1
const val NOT_AUTHENTICATED = -1
const val TAG: String = "BiometricDeviceAuth"
const val BIOMETRIC_AUTHENTICATORS = BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.BIOMETRIC_WEAK
const val ALLOWED_AUTHENTICATORS = BIOMETRIC_AUTHENTICATORS or BiometricManager.Authenticators.DEVICE_CREDENTIAL
}
fun authenticate(context: Context, force: Boolean, showConfirmDeviceCredentialIntent: () -> Unit): Boolean {
val isKeyGuardSecure = ServiceUtil.getKeyguardManager(context).isKeyguardSecure
if (!isKeyGuardSecure) {
Log.w(TAG, "Keyguard not secure...")
return false
}
return if (Build.VERSION.SDK_INT != 29 && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS) {
if (force) {
Log.i(TAG, "Listening for biometric authentication...")
biometricPrompt.authenticate(biometricPromptInfo)
} else {
Log.i(TAG, "Skipping show system biometric or device lock dialog unless forced")
}
true
} else if (Build.VERSION.SDK_INT >= 21) {
if (force) {
Log.i(TAG, "firing intent...")
showConfirmDeviceCredentialIntent()
} else {
Log.i(TAG, "Skipping firing intent unless forced")
}
true
} else {
Log.w(TAG, "Not compatible...")
false
}
}
fun cancelAuthentication() {
biometricPrompt.cancelAuthentication()
}
}
class BiometricDeviceLockContract : ActivityResultContract<String, Int>() {
@RequiresApi(api = 21)
override fun createIntent(context: Context, input: String): Intent {
val keyguardManager = ServiceUtil.getKeyguardManager(context)
return keyguardManager.createConfirmDeviceCredentialIntent(input, "")
}
override fun parseResult(resultCode: Int, intent: Intent?) =
if (resultCode != Activity.RESULT_OK) {
BiometricDeviceAuthentication.NOT_AUTHENTICATED
} else {
BiometricDeviceAuthentication.AUTHENTICATED
}
}

View File

@@ -144,20 +144,20 @@ public final class ContactSelectionListFragment extends LoggingFragment
private MappingAdapter contactChipAdapter; private MappingAdapter contactChipAdapter;
private ContactChipViewModel contactChipViewModel; private ContactChipViewModel contactChipViewModel;
private LifecycleDisposable lifecycleDisposable; private LifecycleDisposable lifecycleDisposable;
private HeaderActionProvider headerActionProvider; private HeaderActionProvider headerActionProvider;
private TextView headerActionView; private TextView headerActionView;
@Nullable private FixedViewsAdapter headerAdapter; @Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter; @Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback; @Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback; @Nullable private ScrollCallback scrollCallback;
private GlideRequests glideRequests; @Nullable private OnItemLongClickListener onItemLongClickListener;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS; private GlideRequests glideRequests;
private Set<RecipientId> currentSelection; private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private boolean isMulti; private Set<RecipientId> currentSelection;
private boolean hideCount; private boolean isMulti;
private boolean canSelectSelf; private boolean hideCount;
private boolean canSelectSelf;
@Override @Override
public void onAttach(@NonNull Context context) { public void onAttach(@NonNull Context context) {
@@ -206,6 +206,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (getParentFragment() instanceof HeaderActionProvider) { if (getParentFragment() instanceof HeaderActionProvider) {
headerActionProvider = (HeaderActionProvider) getParentFragment(); headerActionProvider = (HeaderActionProvider) getParentFragment();
} }
if (context instanceof OnItemLongClickListener) {
onItemLongClickListener = (OnItemLongClickListener) context;
}
if (getParentFragment() instanceof OnItemLongClickListener) {
onItemLongClickListener = (OnItemLongClickListener) getParentFragment();
}
} }
@Override @Override
@@ -720,6 +728,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
} }
} }
@Override
public boolean onItemLongClick(ContactSelectionListItem item) {
if (onItemLongClickListener != null) {
return onItemLongClickListener.onLongClick(item);
} else {
return false;
}
}
} }
private boolean selectionHardLimitReached() { private boolean selectionHardLimitReached() {
@@ -850,6 +867,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
@NonNull HeaderAction getHeaderAction(); @NonNull HeaderAction getHeaderAction();
} }
public interface OnItemLongClickListener {
boolean onLongClick(ContactSelectionListItem contactSelectionListItem);
}
public interface AbstractContactsCursorLoaderFactoryProvider { public interface AbstractContactsCursorLoaderFactoryProvider {
@NonNull AbstractContactsCursorLoader.Factory get(); @NonNull AbstractContactsCursorLoader.Factory get();
} }

View File

@@ -18,6 +18,7 @@ import androidx.core.view.ViewCompat;
import org.signal.qr.QrScannerView; import org.signal.qr.QrScannerView;
import org.signal.qr.kitkat.ScanListener; import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
@@ -57,7 +58,7 @@ public class DeviceAddFragment extends LoggingFragment {
}); });
} }
scannerView.start(getViewLifecycleOwner()); scannerView.start(getViewLifecycleOwner(), CameraXModelBlocklist.isBlocklisted());
lifecycleDisposable.bindTo(getViewLifecycleOwner()); lifecycleDisposable.bindTo(getViewLifecycleOwner());

View File

@@ -44,7 +44,7 @@ import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter; import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager; import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader; import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
@@ -168,7 +168,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
setSupportActionBar(findViewById(R.id.toolbar)); setSupportActionBar(findViewById(R.id.toolbar));
voiceNoteMediaController = new VoiceNoteMediaController(this); voiceNoteMediaController = new VoiceNoteMediaController(this);
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class); viewModel = new ViewModelProvider(this).get(MediaPreviewViewModel.class);
fullscreenHelper = new FullscreenHelper(this); fullscreenHelper = new FullscreenHelper(this);

View File

@@ -20,11 +20,27 @@ import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.ViewGroup;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository;
import org.thoughtcrime.securesms.contacts.management.ContactsManagementViewModel;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery; import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
@@ -33,32 +49,57 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/** /**
* Activity container for starting a new conversation. * Activity container for starting a new conversation.
* *
* @author Moxie Marlinspike * @author Moxie Marlinspike
*
*/ */
public class NewConversationActivity extends ContactSelectionActivity public class NewConversationActivity extends ContactSelectionActivity
implements ContactSelectionListFragment.ListCallback implements ContactSelectionListFragment.ListCallback, ContactSelectionListFragment.OnItemLongClickListener
{ {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = Log.tag(NewConversationActivity.class); private static final String TAG = Log.tag(NewConversationActivity.class);
private ContactsManagementViewModel viewModel;
private ActivityResultLauncher<Intent> contactLauncher;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@Override @Override
public void onCreate(Bundle bundle, boolean ready) { public void onCreate(Bundle bundle, boolean ready) {
super.onCreate(bundle, ready); super.onCreate(bundle, ready);
assert getSupportActionBar() != null; assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.NewConversationActivity__new_message); getSupportActionBar().setTitle(R.string.NewConversationActivity__new_message);
disposables.bindTo(this);
ContactsManagementRepository repository = new ContactsManagementRepository(this);
ContactsManagementViewModel.Factory factory = new ContactsManagementViewModel.Factory(repository);
contactLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), activityResult -> {
if (activityResult.getResultCode() == RESULT_OK) {
handleManualRefresh();
}
});
viewModel = new ViewModelProvider(this, factory).get(ContactsManagementViewModel.class);
} }
@Override @Override
@@ -120,10 +161,18 @@ public class NewConversationActivity extends ContactSelectionActivity
super.onOptionsItemSelected(item); super.onOptionsItemSelected(item);
switch (item.getItemId()) { switch (item.getItemId()) {
case android.R.id.home: super.onBackPressed(); return true; case android.R.id.home:
case R.id.menu_refresh: handleManualRefresh(); return true; super.onBackPressed();
case R.id.menu_new_group: handleCreateGroup(); return true; return true;
case R.id.menu_invite: handleInvite(); return true; case R.id.menu_refresh:
handleManualRefresh();
return true;
case R.id.menu_new_group:
handleCreateGroup();
return true;
case R.id.menu_invite:
handleInvite();
return true;
} }
return false; return false;
@@ -162,4 +211,143 @@ public class NewConversationActivity extends ContactSelectionActivity
handleCreateGroup(); handleCreateGroup();
finish(); finish();
} }
@Override
public boolean onLongClick(ContactSelectionListItem contactSelectionListItem) {
RecipientId recipientId = contactSelectionListItem.getRecipientId().orElse(null);
if (recipientId == null) {
return false;
}
List<ActionItem> actions = generateContextualActionsForRecipient(recipientId);
if (actions.isEmpty()) {
return false;
}
new SignalContextMenu.Builder(contactSelectionListItem, (ViewGroup) contactSelectionListItem.getRootView())
.preferredVerticalPosition(SignalContextMenu.VerticalPosition.BELOW)
.preferredHorizontalPosition(SignalContextMenu.HorizontalPosition.START)
.offsetX((int) DimensionUnit.DP.toPixels(12))
.offsetY((int) DimensionUnit.DP.toPixels(12))
.show(actions);
return true;
}
private @NonNull List<ActionItem> generateContextualActionsForRecipient(@NonNull RecipientId recipientId) {
Recipient recipient = Recipient.resolved(recipientId);
return Stream.of(
createMessageActionItem(recipient),
createAudioCallActionItem(recipient),
createVideoCallActionItem(recipient),
createRemoveActionItem(recipient),
createBlockActionItem(recipient)
).filter(Objects::nonNull).collect(Collectors.toList());
}
private @NonNull ActionItem createMessageActionItem(@NonNull Recipient recipient) {
return new ActionItem(
R.drawable.ic_message_24,
getString(R.string.NewConversationActivity__message),
R.color.signal_colorOnSurface,
() -> startActivity(ConversationIntents.createBuilder(this, recipient.getId(), -1L).build())
);
}
private @Nullable ActionItem createAudioCallActionItem(@NonNull Recipient recipient) {
if (recipient.isSelf() || recipient.isGroup()) {
return null;
}
return new ActionItem(
R.drawable.ic_phone_right_24,
getString(R.string.NewConversationActivity__audio_call),
R.color.signal_colorOnSurface,
() -> CommunicationActions.startVoiceCall(this, recipient)
);
}
private @Nullable ActionItem createVideoCallActionItem(@NonNull Recipient recipient) {
if (recipient.isSelf() || recipient.isMmsGroup()) {
return null;
}
return new ActionItem(
R.drawable.ic_video_call_24,
getString(R.string.NewConversationActivity__video_call),
R.color.signal_colorOnSurface,
() -> CommunicationActions.startVideoCall(this, recipient)
);
}
private @Nullable ActionItem createRemoveActionItem(@NonNull Recipient recipient) {
if (!FeatureFlags.hideContacts() || recipient.isSelf() || recipient.isGroup()) {
return null;
}
return new ActionItem(
R.drawable.ic_minus_circle_20, // TODO [alex] -- correct asset
getString(R.string.NewConversationActivity__remove),
R.color.signal_colorOnSurface,
() -> {
if (recipient.isSystemContact()) {
displayIsInSystemContactsDialog(recipient);
} else {
displayRemovalDialog(recipient);
}
}
);
}
@SuppressWarnings("CodeBlock2Expr")
private @Nullable ActionItem createBlockActionItem(@NonNull Recipient recipient) {
if (recipient.isSelf()) {
return null;
}
return new ActionItem(
R.drawable.ic_block_tinted_24,
getString(R.string.NewConversationActivity__block),
R.color.signal_colorError,
() -> BlockUnblockDialog.showBlockFor(this,
this.getLifecycle(),
recipient,
() -> {
disposables.add(viewModel.blockContact(recipient).subscribe(() -> {
displaySnackbar(R.string.NewConversationActivity__s_has_been_removed);
}));
})
);
}
private void displayIsInSystemContactsDialog(@NonNull Recipient recipient) {
new MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.NewConversationActivity__unable_to_remove_s, recipient.getShortDisplayName(this)))
.setMessage(R.string.NewConversationActivity__this_person_is_saved_to_your)
.setPositiveButton(R.string.NewConversationActivity__view_contact,
(dialog, which) -> contactLauncher.launch(new Intent(Intent.ACTION_VIEW, recipient.getContactUri()))
)
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void displayRemovalDialog(@NonNull Recipient recipient) {
new MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.NewConversationActivity__remove_s, recipient.getShortDisplayName(this)))
.setMessage(R.string.NewConversationActivity__you_wont_see_this_person)
.setPositiveButton(R.string.NewConversationActivity__remove,
(dialog, which) -> {
disposables.add(viewModel.hideContact(recipient).subscribe(() -> {
displaySnackbar(R.string.NewConversationActivity__s_has_been_removed);
}));
}
)
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void displaySnackbar(@StringRes int message) {
Snackbar.make(findViewById(android.R.id.content), message, Snackbar.LENGTH_SHORT).show();
}
} }

View File

@@ -47,7 +47,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.biometric.BiometricManager; import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricManager.Authenticators;
import androidx.biometric.BiometricPrompt; import androidx.biometric.BiometricPrompt;
import org.signal.core.util.ThreadUtil; import org.signal.core.util.ThreadUtil;
@@ -64,6 +63,8 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.SupportEmailUtil; import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import kotlin.Unit;
/** /**
* Activity that prompts for a user's passphrase. * Activity that prompts for a user's passphrase.
* *
@@ -72,8 +73,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class PassphrasePromptActivity extends PassphraseActivity { public class PassphrasePromptActivity extends PassphraseActivity {
private static final String TAG = Log.tag(PassphrasePromptActivity.class); private static final String TAG = Log.tag(PassphrasePromptActivity.class);
private static final int BIOMETRIC_AUTHENTICATORS = Authenticators.BIOMETRIC_STRONG | Authenticators.BIOMETRIC_WEAK;
private static final int ALLOWED_AUTHENTICATORS = BIOMETRIC_AUTHENTICATORS | Authenticators.DEVICE_CREDENTIAL;
private static final short AUTHENTICATE_REQUEST_CODE = 1007; private static final short AUTHENTICATE_REQUEST_CODE = 1007;
private static final String BUNDLE_ALREADY_SHOWN = "bundle_already_shown"; private static final String BUNDLE_ALREADY_SHOWN = "bundle_already_shown";
public static final String FROM_FOREGROUND = "from_foreground"; public static final String FROM_FOREGROUND = "from_foreground";
@@ -90,9 +89,9 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private ImageButton hideButton; private ImageButton hideButton;
private AnimatingToggle visibilityToggle; private AnimatingToggle visibilityToggle;
private BiometricManager biometricManager; private BiometricManager biometricManager;
private BiometricPrompt biometricPrompt; private BiometricPrompt biometricPrompt;
private BiometricPrompt.PromptInfo biometricPromptInfo; private BiometricDeviceAuthentication biometricAuth;
private boolean authenticated; private boolean authenticated;
private boolean hadFailure; private boolean hadFailure;
@@ -249,12 +248,12 @@ public class PassphrasePromptActivity extends PassphraseActivity {
lockScreenButton = findViewById(R.id.lock_screen_auth_container); lockScreenButton = findViewById(R.id.lock_screen_auth_container);
biometricManager = BiometricManager.from(this); biometricManager = BiometricManager.from(this);
biometricPrompt = new BiometricPrompt(this, new BiometricAuthenticationListener()); biometricPrompt = new BiometricPrompt(this, new BiometricAuthenticationListener());
biometricPromptInfo = new BiometricPrompt.PromptInfo BiometricPrompt.PromptInfo biometricPromptInfo = new BiometricPrompt.PromptInfo
.Builder() .Builder()
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS) .setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS)
.setTitle(getString(R.string.PassphrasePromptActivity_unlock_signal)) .setTitle(getString(R.string.PassphrasePromptActivity_unlock_signal))
.build(); .build();
biometricAuth = new BiometricDeviceAuthentication(biometricManager, biometricPrompt, biometricPromptInfo);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
getSupportActionBar().setTitle(""); getSupportActionBar().setTitle("");
@@ -279,7 +278,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private void setLockTypeVisibility() { private void setLockTypeVisibility() {
if (TextSecurePreferences.isScreenLockEnabled(this)) { if (TextSecurePreferences.isScreenLockEnabled(this)) {
passphraseAuthContainer.setVisibility(View.GONE); passphraseAuthContainer.setVisibility(View.GONE);
fingerprintPrompt.setVisibility(biometricManager.canAuthenticate(BIOMETRIC_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS ? View.VISIBLE fingerprintPrompt.setVisibility(biometricManager.canAuthenticate(BiometricDeviceAuthentication.BIOMETRIC_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS ? View.VISIBLE
: View.GONE); : View.GONE);
lockScreenButton.setVisibility(View.VISIBLE); lockScreenButton.setVisibility(View.VISIBLE);
} else { } else {
@@ -290,33 +289,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
} }
private void resumeScreenLock(boolean force) { private void resumeScreenLock(boolean force) {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); if (!biometricAuth.authenticate(getApplicationContext(), force, this::showConfirmDeviceCredentialIntent)) {
assert keyguardManager != null;
if (!keyguardManager.isKeyguardSecure()) {
Log.w(TAG ,"Keyguard not secure...");
handleAuthenticated();
return;
}
if (Build.VERSION.SDK_INT != 29 && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS) {
if (force) {
Log.i(TAG, "Listening for biometric authentication...");
biometricPrompt.authenticate(biometricPromptInfo);
} else {
Log.i(TAG, "Skipping show system biometric dialog unless forced");
}
} else if (Build.VERSION.SDK_INT >= 21) {
if (force) {
Log.i(TAG, "firing intent...");
Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
startActivityForResult(intent, AUTHENTICATE_REQUEST_CODE);
} else {
Log.i(TAG, "Skipping firing intent unless forced");
}
} else {
Log.w(TAG, "Not compatible...");
handleAuthenticated(); handleAuthenticated();
} }
} }
@@ -332,6 +305,16 @@ public class PassphrasePromptActivity extends PassphraseActivity {
body); body);
} }
public Unit showConfirmDeviceCredentialIntent() {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
Intent intent = null;
if (Build.VERSION.SDK_INT >= 21) {
intent = keyguardManager.createConfirmDeviceCredentialIntent(getString(R.string.PassphrasePromptActivity_unlock_signal), "");
}
startActivityForResult(intent, AUTHENTICATE_REQUEST_CODE);
return Unit.INSTANCE;
}
private class PassphraseActionListener implements TextView.OnEditorActionListener { private class PassphraseActionListener implements TextView.OnEditorActionListener {
@Override @Override
public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent keyEvent) { public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent keyEvent) {

View File

@@ -20,17 +20,20 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity; import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity; import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity; import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.pin.PinRestoreActivity; import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity; import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.username.AddAUsernameActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity; import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale; import java.util.Locale;
@@ -52,6 +55,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_TRANSFER_ONGOING = 8; private static final int STATE_TRANSFER_ONGOING = 8;
private static final int STATE_TRANSFER_LOCKED = 9; private static final int STATE_TRANSFER_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10; private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private static final int STATE_CREATE_USERNAME = 11;
private SignalServiceNetworkAccess networkAccess; private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver; private BroadcastReceiver clearKeyReceiver;
@@ -156,6 +160,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent(); case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent(); case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent(); case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
case STATE_CREATE_USERNAME: return getCreateUsernameIntent();
default: return null; default: return null;
} }
} }
@@ -175,6 +180,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_CREATE_SIGNAL_PIN; return STATE_CREATE_SIGNAL_PIN;
} else if (userMustSetProfileName()) { } else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME; return STATE_CREATE_PROFILE_NAME;
} else if (shouldAskUserToCreateUsername()) {
return STATE_CREATE_USERNAME;
} else if (userMustCreateSignalPin()) { } else if (userMustCreateSignalPin()) {
return STATE_CREATE_SIGNAL_PIN; return STATE_CREATE_SIGNAL_PIN;
} else if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null && getClass() != OldDeviceTransferActivity.class) { } else if (EventBus.getDefault().getStickyEvent(TransferStatus.class) != null && getClass() != OldDeviceTransferActivity.class) {
@@ -200,6 +207,13 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty(); return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
} }
private boolean shouldAskUserToCreateUsername() {
return FeatureFlags.usernames() &&
FeatureFlags.phoneNumberPrivacy() &&
!SignalStore.uiHints().hasSetOrSkippedUsernameCreation() &&
SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode() == PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED;
}
private Intent getCreatePassphraseIntent() { private Intent getCreatePassphraseIntent() {
return getRoutedIntent(PassphraseCreateActivity.class, getIntent()); return getRoutedIntent(PassphraseCreateActivity.class, getIntent());
} }
@@ -238,7 +252,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
} }
private Intent getCreateProfileNameIntent() { private Intent getCreateProfileNameIntent() {
return getRoutedIntent(EditProfileActivity.class, getIntent()); Intent intent = EditProfileActivity.getIntentForUserProfile(this);
return getRoutedIntent(intent, getIntent());
} }
private Intent getOldDeviceTransferIntent() { private Intent getOldDeviceTransferIntent() {
@@ -258,6 +273,15 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return ChangeNumberLockActivity.createIntent(this); return ChangeNumberLockActivity.createIntent(this);
} }
private Intent getCreateUsernameIntent() {
return getRoutedIntent(AddAUsernameActivity.class, getIntent());
}
private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) {
if (nextIntent != null) destination.putExtra("next_intent", nextIntent);
return destination;
}
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) { private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
final Intent intent = new Intent(this, destination); final Intent intent = new Intent(this, destination);
if (nextIntent != null) intent.putExtra("next_intent", nextIntent); if (nextIntent != null) intent.putExtra("next_intent", nextIntent);

View File

@@ -24,7 +24,6 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Rect; import android.graphics.Rect;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build; import android.os.Build;
@@ -40,15 +39,18 @@ import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer; import androidx.core.util.Consumer;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.window.DisplayFeature; import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter;
import androidx.window.FoldingFeature; import androidx.window.layout.DisplayFeature;
import androidx.window.WindowLayoutInfo; import androidx.window.layout.FoldingFeature;
import androidx.window.layout.WindowInfoTracker;
import androidx.window.layout.WindowLayoutInfo;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey; import org.signal.libsignal.protocol.IdentityKey;
@@ -107,20 +109,21 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION"; public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION";
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE"; public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN";
private CallParticipantsListUpdatePopupWindow participantUpdateWindow; private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
private WifiToCellularPopupWindow wifiToCellularPopupWindow; private WifiToCellularPopupWindow wifiToCellularPopupWindow;
private DeviceOrientationMonitor deviceOrientationMonitor; private DeviceOrientationMonitor deviceOrientationMonitor;
private FullscreenHelper fullscreenHelper; private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen; private WebRtcCallView callScreen;
private TooltipPopup videoTooltip; private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel; private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable; private boolean enableVideoIfAvailable;
private boolean hasWarnedAboutBluetooth; private boolean hasWarnedAboutBluetooth;
private androidx.window.WindowManager windowManager; private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
private WindowLayoutInfoConsumer windowLayoutInfoConsumer; private WindowInfoTrackerCallbackAdapter windowInfoTrackerCallbackAdapter;
private ThrottledDebouncer requestNewSizesThrottle; private ThrottledDebouncer requestNewSizesThrottle;
private Disposable ephemeralStateDisposable = Disposable.empty(); private Disposable ephemeralStateDisposable = Disposable.empty();
@@ -133,7 +136,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate()"); Log.i(TAG, "onCreate(" + getIntent().getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")");
getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED); getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
@@ -158,10 +161,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false); enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE); getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
windowManager = new androidx.window.WindowManager(this);
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer(); windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
windowManager.registerLayoutChangeCallback(SignalExecutors.BOUNDED, windowLayoutInfoConsumer); windowInfoTrackerCallbackAdapter = new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
windowInfoTrackerCallbackAdapter.addWindowLayoutInfoListener(this, SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1)); requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
} }
@@ -185,11 +188,25 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
if (!EventBus.getDefault().isRegistered(this)) { if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this); EventBus.getDefault().register(this);
} }
WebRtcViewModel rtcViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
if (rtcViewModel == null) {
Log.w(TAG, "Activity resumed without service event, perform delay destroy");
ThreadUtil.runOnMainDelayed(() -> {
WebRtcViewModel delayRtcViewModel = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
if (delayRtcViewModel == null) {
Log.w(TAG, "Activity still without service event, finishing activity");
finish();
} else {
Log.i(TAG, "Event found after delay");
}
}, TimeUnit.SECONDS.toMillis(1));
}
} }
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
Log.i(TAG, "onNewIntent"); Log.i(TAG, "onNewIntent(" + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")");
super.onNewIntent(intent); super.onNewIntent(intent);
processIntent(intent); processIntent(intent);
} }
@@ -234,7 +251,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
windowManager.unregisterLayoutChangeCallback(windowLayoutInfoConsumer); windowInfoTrackerCallbackAdapter.removeWindowLayoutInfoListener(windowLayoutInfoConsumer);
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
} }
@@ -256,12 +273,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
} }
} }
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
viewModel.setIsInPipMode(isInPictureInPictureMode);
participantUpdateWindow.setEnabled(!isInPictureInPictureMode);
}
private boolean enterPipModeIfPossible() { private boolean enterPipModeIfPossible() {
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) { if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder() PictureInPictureParams params = new PictureInPictureParams.Builder()
@@ -341,6 +352,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees())); viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
viewModel.getControlsRotation().observe(this, callScreen::rotateControls); viewModel.getControlsRotation().observe(this, callScreen::rotateControls);
addOnPictureInPictureModeChangedListener(info -> {
viewModel.setIsInPipMode(info.isInPictureInPictureMode());
participantUpdateWindow.setEnabled(!info.isInPictureInPictureMode());
});
} }
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) { private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {

View File

@@ -10,6 +10,7 @@ import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import org.signal.core.util.PendingIntentFlags;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper; import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper;
@@ -40,7 +41,7 @@ public enum BackupFileIOError {
} }
public void postNotification(@NonNull Context context) { public void postNotification(@NonNull Context context) {
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), 0); PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), PendingIntentFlags.mutable());
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES) Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
.setSmallIcon(R.drawable.ic_signal_backup) .setSmallIcon(R.drawable.ic_signal_backup)
.setContentTitle(context.getString(titleId)) .setContentTitle(context.getString(titleId))

View File

@@ -413,7 +413,7 @@ public class FullBackupExporter extends FullBackupBase {
return count; return count;
} }
private static long calculateVeryOldStreamLength(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException { private static long calculateVeryOldStreamLength(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) {
long result = 0; long result = 0;
try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) { try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) {
@@ -425,6 +425,10 @@ public class FullBackupExporter extends FullBackupBase {
} }
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.w(TAG, "Missing attachment: " + e.getMessage()); Log.w(TAG, "Missing attachment: " + e.getMessage());
return 0;
} catch (IOException e) {
Log.w(TAG, "Failed to determine stream length", e);
return 0;
} }
return result; return result;
@@ -708,6 +712,7 @@ public class FullBackupExporter extends FullBackupBase {
public void close() throws IOException { public void close() throws IOException {
outputStream.flush();
outputStream.close(); outputStream.close();
} }
} }

View File

@@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.components.InputAwareLayout
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
@@ -36,6 +35,7 @@ import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
import org.thoughtcrime.securesms.util.Debouncer import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.fragments.requireListener
/** /**
@@ -75,7 +75,7 @@ class GiftFlowConfirmationFragment :
private val eventPublisher = PublishSubject.create<TextInput.TextInputEvent>() private val eventPublisher = PublishSubject.create<TextInput.TextInputEvent>()
private val debouncer = Debouncer(100L) private val debouncer = Debouncer(100L)
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
RecipientPreference.register(adapter) RecipientPreference.register(adapter)
GiftRowItem.register(adapter) GiftRowItem.register(adapter)

View File

@@ -4,17 +4,20 @@ import android.view.View
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
import org.thoughtcrime.securesms.components.settings.models.SplashImage
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -32,11 +35,12 @@ class GiftFlowStartFragment : DSLSettingsFragment(
private val lifecycleDisposable = LifecycleDisposable() private val lifecycleDisposable = LifecycleDisposable()
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
CurrencySelection.register(adapter) CurrencySelection.register(adapter)
GiftRowItem.register(adapter) GiftRowItem.register(adapter)
NetworkFailure.register(adapter) NetworkFailure.register(adapter)
IndeterminateLoadingCircle.register(adapter) IndeterminateLoadingCircle.register(adapter)
SplashImage.register(adapter)
val next = requireView().findViewById<View>(R.id.next) val next = requireView().findViewById<View>(R.id.next)
next.setOnClickListener { next.setOnClickListener {
@@ -58,6 +62,28 @@ class GiftFlowStartFragment : DSLSettingsFragment(
private fun getConfiguration(state: GiftFlowState): DSLConfiguration { private fun getConfiguration(state: GiftFlowState): DSLConfiguration {
return configure { return configure {
customPref(
SplashImage.Model(
R.drawable.ic_gift_chat
)
)
noPadTextPref(
title = DSLSettingsText.from(
R.string.GiftFlowStartFragment__gift_a_badge,
DSLSettingsText.CenterModifier,
DSLSettingsText.TextAppearanceModifier(R.style.Signal_Text_Headline)
)
)
space(DimensionUnit.DP.toPixels(16f).toInt())
noPadTextPref(
title = DSLSettingsText.from(R.string.GiftFlowStartFragment__gift_someone_a_badge, DSLSettingsText.CenterModifier)
)
space(DimensionUnit.DP.toPixels(16f).toInt())
customPref( customPref(
CurrencySelection.Model( CurrencySelection.Model(
selectedCurrency = state.currency, selectedCurrency = state.currency,

View File

@@ -36,6 +36,10 @@ object GiftRowItem {
private val taglineView = itemView.findViewById<TextView>(R.id.tagline) private val taglineView = itemView.findViewById<TextView>(R.id.tagline)
private val priceView = itemView.findViewById<TextView>(R.id.price) private val priceView = itemView.findViewById<TextView>(R.id.price)
init {
itemView.isSelected = true
}
override fun bind(model: Model) { override fun bind(model: Model) {
checkView.visible = false checkView.visible = false
badgeView.setBadge(model.giftBadge) badgeView.setBadge(model.giftBadge)

View File

@@ -13,11 +13,11 @@ import org.thoughtcrime.securesms.badges.Badges.displayBadges
import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.BadgePreview import org.thoughtcrime.securesms.badges.models.BadgePreview
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.Material3OnScrollHelper import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
/** /**
* Fragment which allows user to select one of their badges to be their "Featured" badge. * Fragment which allows user to select one of their badges to be their "Featured" badge.
@@ -50,7 +50,7 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment(
return Material3OnScrollHelper(requireActivity(), scrollShadow) return Material3OnScrollHelper(requireActivity(), scrollShadow)
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
Badge.register(adapter) { badge, isSelected, _ -> Badge.register(adapter) { badge, isSelected, _ ->
if (!isSelected) { if (!isSelected) {
viewModel.setSelectedBadge(badge) viewModel.setSelectedBadge(badge)

View File

@@ -10,7 +10,6 @@ import org.thoughtcrime.securesms.badges.Badges.displayBadges
import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
@@ -18,6 +17,7 @@ import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
/** /**
@@ -35,7 +35,7 @@ class BadgesOverviewFragment : DSLSettingsFragment(
} }
) )
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
Badge.register(adapter) { badge, _, isFaded -> Badge.register(adapter) { badge, _, isFaded ->
if (badge.isExpired() || isFaded) { if (badge.isExpired() || isFaded) {
findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge, null, null)) findNavController().safeNavigate(BadgesOverviewFragmentDirections.actionBadgeManageFragmentToExpiredBadgeDialog(badge, null, null))

View File

@@ -23,7 +23,6 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.PlayServicesUtil import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -68,10 +67,7 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
action.setOnClickListener { action.setOnClickListener {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url)) CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
} }
} else if ( } else if (Recipient.self().badges.none { it.category == Badge.Category.Donor && !it.isBoost() && !it.isExpired() }) {
FeatureFlags.donorBadges() &&
Recipient.self().badges.none { it.category == Badge.Category.Donor && !it.isBoost() && !it.isExpired() }
) {
action.setOnClickListener { action.setOnClickListener {
startActivity(AppSettingsActivity.subscriptions(requireContext())) startActivity(AppSettingsActivity.subscriptions(requireContext()))
} }
@@ -143,10 +139,6 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
recipientId: RecipientId, recipientId: RecipientId,
startBadge: Badge? = null startBadge: Badge? = null
) { ) {
if (!FeatureFlags.displayDonorBadges() && recipientId != Recipient.self().id) {
return
}
ViewBadgeBottomSheetDialogFragment().apply { ViewBadgeBottomSheetDialogFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putParcelable(ARG_START_BADGE, startBadge) putParcelable(ARG_START_BADGE, startBadge)

View File

@@ -10,7 +10,7 @@ import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
@@ -48,7 +48,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
BlockedUsersRepository repository = new BlockedUsersRepository(this); BlockedUsersRepository repository = new BlockedUsersRepository(this);
BlockedUsersViewModel.Factory factory = new BlockedUsersViewModel.Factory(repository); BlockedUsersViewModel.Factory factory = new BlockedUsersViewModel.Factory(repository);
viewModel = ViewModelProviders.of(this, factory).get(BlockedUsersViewModel.class); viewModel = new ViewModelProvider(this, factory).get(BlockedUsersViewModel.class);
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
ContactFilterView contactFilterView = findViewById(R.id.contact_filter_edit_text); ContactFilterView contactFilterView = findViewById(R.id.contact_filter_edit_text);

View File

@@ -9,7 +9,7 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.BlockUnblockDialog; import org.thoughtcrime.securesms.BlockUnblockDialog;
@@ -59,7 +59,7 @@ public class BlockedUsersFragment extends Fragment {
} }
}); });
viewModel = ViewModelProviders.of(requireActivity()).get(BlockedUsersViewModel.class); viewModel = new ViewModelProvider(requireActivity()).get(BlockedUsersViewModel.class);
viewModel.getRecipients().observe(getViewLifecycleOwner(), list -> { viewModel.getRecipients().observe(getViewLifecycleOwner(), list -> {
if (list.isEmpty()) { if (list.isEmpty()) {
empty.setVisibility(View.VISIBLE); empty.setVisibility(View.VISIBLE);

View File

@@ -17,6 +17,7 @@ import android.widget.TextView;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import com.airbnb.lottie.LottieAnimationView; import com.airbnb.lottie.LottieAnimationView;
@@ -126,6 +127,11 @@ public final class AudioView extends FrameLayout {
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE)); setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE));
int backgroundTintColor = typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.TRANSPARENT);
if (getBackground() != null && backgroundTintColor != Color.TRANSPARENT) {
DrawableCompat.setTint(getBackground(), backgroundTintColor);
}
this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE); this.waveFormPlayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformPlayedBarsColor, Color.WHITE);
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE); this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
this.waveFormThumbTint = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE); this.waveFormThumbTint = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE);

View File

@@ -64,6 +64,7 @@ public final class AvatarImageView extends AppCompatImageView {
private @Nullable RecipientContactPhoto recipientContactPhoto; private @Nullable RecipientContactPhoto recipientContactPhoto;
private @NonNull Drawable unknownRecipientDrawable; private @NonNull Drawable unknownRecipientDrawable;
private @Nullable AvatarColor fallbackPhotoColor;
public AvatarImageView(Context context) { public AvatarImageView(Context context) {
super(context); super(context);
@@ -105,6 +106,10 @@ public final class AvatarImageView extends AppCompatImageView {
this.fallbackPhotoProvider = fallbackPhotoProvider; this.fallbackPhotoProvider = fallbackPhotoProvider;
} }
public void setFallbackPhotoColor(@Nullable AvatarColor fallbackPhotoColor) {
this.fallbackPhotoColor = fallbackPhotoColor;
}
/** /**
* Shows self as the actual profile picture. * Shows self as the actual profile picture.
*/ */
@@ -213,7 +218,7 @@ public final class AvatarImageView extends AppCompatImageView {
requestManager.clear(this); requestManager.clear(this);
if (fallbackPhotoProvider != null) { if (fallbackPhotoProvider != null) {
setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName() setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName()
.asDrawable(getContext(), AvatarColor.UNKNOWN, inverted)); .asDrawable(getContext(), Util.firstNonNull(fallbackPhotoColor, AvatarColor.UNKNOWN), inverted));
} else { } else {
setImageDrawable(unknownRecipientDrawable); setImageDrawable(unknownRecipientDrawable);
} }

View File

@@ -426,7 +426,7 @@ public class ComposeText extends EmojiEditText {
} }
int delimiterSearchIndex = inputCursorPosition - 1; int delimiterSearchIndex = inputCursorPosition - 1;
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != starter && text.charAt(delimiterSearchIndex) != ' ')) { while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != starter && !Character.isWhitespace(text.charAt(delimiterSearchIndex)))) {
delimiterSearchIndex--; delimiterSearchIndex--;
} }

View File

@@ -80,6 +80,10 @@ class Material3SearchToolbar @JvmOverloads constructor(
} }
} }
fun clearText() {
input.setText("")
}
interface Listener { interface Listener {
fun onSearchTextChange(text: String) fun onSearchTextChange(text: String)
fun onSearchClosed() fun onSearchClosed()

View File

@@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import com.google.android.material.card.MaterialCardView
import org.thoughtcrime.securesms.R
/**
* A small card with a circular progress indicator in it. Usable in place
* of a ProgressDialog, which is deprecated.
*
* Remember to add this as the last UI element in your XML hierarchy so it'll
* draw over top of other elements.
*/
class ProgressCard @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : MaterialCardView(context, attrs) {
init {
inflate(context, R.layout.progress_card, this)
}
}

View File

@@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.components
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.viewbinding.ViewBinding
import kotlin.reflect.KProperty
/**
* ViewBinderDelegate which enforces the "best practices" for maintaining a reference to a view binding given by
* Android official documentation.
*/
open class ViewBinderDelegate<T : ViewBinding>(
private val bindingFactory: (View) -> T,
private val onBindingWillBeDestroyed: (T) -> Unit = {}
) : DefaultLifecycleObserver {
private var binding: T? = null
private var isBindingDestroyed = false
operator fun getValue(thisRef: Fragment, property: KProperty<*>): T {
if (isBindingDestroyed) {
error("Binding has been destroyed.")
}
if (binding == null) {
thisRef.viewLifecycleOwner.lifecycle.addObserver(this@ViewBinderDelegate)
binding = bindingFactory(thisRef.requireView())
}
return binding!!
}
override fun onDestroy(owner: LifecycleOwner) {
if (binding != null) {
onBindingWillBeDestroyed(binding!!)
}
binding = null
isBindingDestroyed = true
}
}

View File

@@ -35,7 +35,9 @@ final class ReminderActionsAdapter extends RecyclerView.Adapter<ReminderActionsA
TextView button = ((TextView) LayoutInflater.from(context).inflate(R.layout.reminder_action_button, parent, false)); TextView button = ((TextView) LayoutInflater.from(context).inflate(R.layout.reminder_action_button, parent, false));
if (importance == Reminder.Importance.NORMAL) { if (importance == Reminder.Importance.NORMAL) {
button.setTextColor(ContextCompat.getColor(context, R.color.signal_accent_primary)); button.setTextColor(ContextCompat.getColor(context, R.color.signal_colorPrimary));
} else if (importance == Reminder.Importance.ERROR || importance == Reminder.Importance.TERMINAL) {
button.setTextColor(ContextCompat.getColor(context, R.color.signal_light_colorOnSurface));
} }
return new ActionViewHolder(button); return new ActionViewHolder(button);

View File

@@ -17,6 +17,8 @@ import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.card.MaterialCardView;
import org.signal.core.util.DimensionUnit; import org.signal.core.util.DimensionUnit;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
@@ -28,8 +30,7 @@ import java.util.List;
public final class ReminderView extends FrameLayout { public final class ReminderView extends FrameLayout {
private ProgressBar progressBar; private ProgressBar progressBar;
private TextView progressText; private TextView progressText;
private ViewGroup container; private MaterialCardView container;
private View background;
private ImageButton closeButton; private ImageButton closeButton;
private TextView title; private TextView title;
private TextView text; private TextView text;
@@ -58,7 +59,6 @@ public final class ReminderView extends FrameLayout {
progressBar = findViewById(R.id.reminder_progress); progressBar = findViewById(R.id.reminder_progress);
progressText = findViewById(R.id.reminder_progress_text); progressText = findViewById(R.id.reminder_progress_text);
container = findViewById(R.id.container); container = findViewById(R.id.container);
background = findViewById(R.id.background);
closeButton = findViewById(R.id.cancel); closeButton = findViewById(R.id.cancel);
title = findViewById(R.id.reminder_title); title = findViewById(R.id.reminder_title);
text = findViewById(R.id.reminder_text); text = findViewById(R.id.reminder_text);
@@ -75,6 +75,7 @@ public final class ReminderView extends FrameLayout {
title.setText(""); title.setText("");
title.setVisibility(GONE); title.setVisibility(GONE);
space.setVisibility(VISIBLE); space.setVisibility(VISIBLE);
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurface));
} }
if (!reminder.isDismissable()) { if (!reminder.isDismissable()) {
@@ -82,22 +83,17 @@ public final class ReminderView extends FrameLayout {
} }
text.setText(reminder.getText()); text.setText(reminder.getText());
switch (reminder.getImportance()) { switch (reminder.getImportance()) {
case NORMAL: case NORMAL:
background.setBackgroundResource(R.drawable.reminder_background_normal); title.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurface));
title.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary)); text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurfaceVariant));
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary));
break; break;
case ERROR: case ERROR:
background.setBackgroundResource(R.drawable.reminder_background_error);
title.setTextColor(ContextCompat.getColor(getContext(), R.color.core_black));
text.setTextColor(ContextCompat.getColor(getContext(), R.color.core_black));
break;
case TERMINAL: case TERMINAL:
background.setBackgroundResource(R.drawable.reminder_background_terminal); container.setStrokeWidth(0);
title.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_button_primary_text)); container.setCardBackgroundColor(ContextCompat.getColor(getContext(), R.color.reminder_background));
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_button_primary_text)); title.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_light_colorOnSurface));
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_light_colorOnSurface));
break; break;
default: default:
throw new IllegalStateException(); throw new IllegalStateException();
@@ -118,7 +114,7 @@ public final class ReminderView extends FrameLayout {
}); });
if (reminder.getImportance() == Reminder.Importance.NORMAL) { if (reminder.getImportance() == Reminder.Importance.NORMAL) {
closeButton.setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_text_primary)); closeButton.setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurfaceVariant));
} }
int progress = reminder.getProgress(); int progress = reminder.getProgress();

View File

@@ -12,10 +12,13 @@ import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.Material3OnScrollHelper import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import java.lang.UnsupportedOperationException
abstract class DSLSettingsFragment( abstract class DSLSettingsFragment(
@StringRes private val titleId: Int = -1, @StringRes private val titleId: Int = -1,
@@ -27,9 +30,11 @@ abstract class DSLSettingsFragment(
protected var recyclerView: RecyclerView? = null protected var recyclerView: RecyclerView? = null
private set private set
private var toolbar: Toolbar? = null
@CallSuper @CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar? = view.findViewById(R.id.toolbar) toolbar = view.findViewById(R.id.toolbar)
if (titleId != -1) { if (titleId != -1) {
toolbar?.setTitle(titleId) toolbar?.setTitle(titleId)
@@ -44,7 +49,13 @@ abstract class DSLSettingsFragment(
toolbar?.setOnMenuItemClickListener { onOptionsItemSelected(it) } toolbar?.setOnMenuItemClickListener { onOptionsItemSelected(it) }
} }
val settingsAdapter = DSLSettingsAdapter() val config = ConcatAdapter.Config.Builder().setIsolateViewTypes(false).build()
val settingsAdapters = createAdapters()
val settingsAdapter: RecyclerView.Adapter<out RecyclerView.ViewHolder> = when {
settingsAdapters.size > 1 -> ConcatAdapter(config, *settingsAdapters)
settingsAdapters.size == 1 -> settingsAdapters.first()
else -> error("Require one or more settings adapters.")
}
recyclerView = view.findViewById<RecyclerView>(R.id.recycler).apply { recyclerView = view.findViewById<RecyclerView>(R.id.recycler).apply {
edgeEffectFactory = EdgeEffectFactory() edgeEffectFactory = EdgeEffectFactory()
@@ -56,7 +67,11 @@ abstract class DSLSettingsFragment(
} }
} }
bindAdapter(settingsAdapter) when (settingsAdapter) {
is ConcatAdapter -> bindAdapters(settingsAdapter)
is MappingAdapter -> bindAdapter(settingsAdapter)
else -> error("Illegal adapter subtype: ${settingsAdapter.javaClass.simpleName}")
}
} }
open fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper? { open fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper? {
@@ -76,7 +91,25 @@ abstract class DSLSettingsFragment(
recyclerView = null recyclerView = null
} }
abstract fun bindAdapter(adapter: DSLSettingsAdapter) fun setTitle(@StringRes resId: Int) {
toolbar?.setTitle(resId)
}
fun setTitle(title: CharSequence) {
toolbar?.title = title
}
open fun createAdapters(): Array<MappingAdapter> {
return arrayOf(DSLSettingsAdapter())
}
open fun bindAdapter(adapter: MappingAdapter) {
throw UnsupportedOperationException("This method is not implemented.")
}
open fun bindAdapters(adapter: ConcatAdapter) {
throw UnsupportedOperationException("This method is not implemented.")
}
private class EdgeEffectFactory : RecyclerView.EdgeEffectFactory() { private class EdgeEffectFactory : RecyclerView.EdgeEffectFactory() {
override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect { override fun createEdgeEffect(view: RecyclerView, direction: Int): EdgeEffect {

View File

@@ -9,7 +9,6 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
@@ -23,6 +22,7 @@ import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.PlayServicesUtil import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -30,7 +30,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
private val viewModel: AppSettingsViewModel by viewModels() private val viewModel: AppSettingsViewModel by viewModels()
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
adapter.registerFactory(BioPreference::class.java, LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item)) adapter.registerFactory(BioPreference::class.java, LayoutFactory(::BioPreferenceViewHolder, R.layout.bio_preference_item))
adapter.registerFactory(PaymentsPreference::class.java, LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference)) adapter.registerFactory(PaymentsPreference::class.java, LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
adapter.registerFactory(SubscriptionPreference::class.java, LayoutFactory(::SubscriptionPreferenceViewHolder, R.layout.dsl_preference_item)) adapter.registerFactory(SubscriptionPreference::class.java, LayoutFactory(::SubscriptionPreferenceViewHolder, R.layout.dsl_preference_item))
@@ -70,8 +70,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
} }
) )
if (FeatureFlags.donorBadges() && PlayServicesUtil.getPlayServicesStatus(requireContext()) == PlayServicesUtil.PlayServicesStatus.SUCCESS) { if (PlayServicesUtil.getPlayServicesStatus(requireContext()) == PlayServicesUtil.PlayServicesStatus.SUCCESS) {
clickPref( clickPref(
title = DSLSettingsText.from(R.string.preferences__donate_to_signal), title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
icon = DSLSettingsIcon.from(R.drawable.ic_heart_24), icon = DSLSettingsIcon.from(R.drawable.ic_heart_24),

View File

@@ -20,7 +20,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
@@ -33,6 +32,7 @@ import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFragment__account) { class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFragment__account) {
@@ -50,7 +50,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
viewModel.refreshState() viewModel.refreshState()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
viewModel = ViewModelProvider(this)[AccountSettingsViewModel::class.java] viewModel = ViewModelProvider(this)[AccountSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state -> viewModel.state.observe(viewLifecycleOwner) { state ->

View File

@@ -4,11 +4,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.keyvalue.SettingsValues import org.thoughtcrime.securesms.keyvalue.SettingsValues
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__appearance) { class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__appearance) {
@@ -24,7 +24,7 @@ class AppearanceSettingsFragment : DSLSettingsFragment(R.string.preferences__app
private val languageLabels by lazy { resources.getStringArray(R.array.language_entries) } private val languageLabels by lazy { resources.getStringArray(R.array.language_entries) }
private val languageValues by lazy { resources.getStringArray(R.array.language_values) } private val languageValues by lazy { resources.getStringArray(R.array.language_values) }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
viewModel = ViewModelProvider(this)[AppearanceSettingsViewModel::class.java] viewModel = ViewModelProvider(this)[AppearanceSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state -> viewModel.state.observe(viewLifecycleOwner) { state ->

View File

@@ -5,8 +5,12 @@ import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.gms.auth.api.phone.SmsRetriever
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.LoggingFragment import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.PlayServicesUtil.PlayServicesStatus
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_number_confirm) { class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_number_confirm) {
@@ -29,6 +33,35 @@ class ChangeNumberConfirmFragment : LoggingFragment(R.layout.fragment_change_num
editNumber.setOnClickListener { findNavController().navigateUp() } editNumber.setOnClickListener { findNavController().navigateUp() }
val changeNumber: View = view.findViewById(R.id.change_number_confirm_change_number) val changeNumber: View = view.findViewById(R.id.change_number_confirm_change_number)
changeNumber.setOnClickListener { findNavController().safeNavigate(R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment) } changeNumber.setOnClickListener { onConfirm() }
}
private fun onConfirm() {
val playServicesAvailable = PlayServicesUtil.getPlayServicesStatus(context) == PlayServicesStatus.SUCCESS
if (playServicesAvailable) {
val client = SmsRetriever.getClient(requireContext())
val task = client.startSmsRetriever()
task.addOnSuccessListener {
Log.i(TAG, "Successfully registered SMS listener.")
navigateToVerify()
}
task.addOnFailureListener { e ->
Log.w(TAG, "Failed to register SMS listener.", e)
navigateToVerify()
}
} else {
navigateToVerify()
}
}
private fun navigateToVerify() {
findNavController().safeNavigate(R.id.action_changePhoneNumberConfirmFragment_to_changePhoneNumberVerifyFragment)
}
companion object {
private val TAG = Log.tag(ChangeNumberConfirmFragment::class.java)
} }
} }

View File

@@ -58,7 +58,10 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
Single.just(false) Single.just(false)
} else { } else {
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.") Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni)) Single
.just(true)
.flatMap { changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni)) }
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
.map { true } .map { true }
} }
} }

View File

@@ -6,12 +6,12 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKeyPair import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.state.SignalProtocolStore import org.signal.libsignal.protocol.state.SignalProtocolStore
import org.signal.libsignal.protocol.util.KeyHelper import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.protocol.util.Medium import org.signal.libsignal.protocol.util.Medium
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.PreKeyUtil import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.crypto.storage.PreKeyMetadataStore
import org.thoughtcrime.securesms.database.IdentityDatabase import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
@@ -30,7 +30,6 @@ import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
import org.whispersystems.signalservice.api.SignalServiceAccountManager import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.SignalServiceMessageSender import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceIdType import org.whispersystems.signalservice.api.push.ServiceIdType
@@ -41,13 +40,47 @@ import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import org.whispersystems.signalservice.internal.push.WhoAmIResponse import org.whispersystems.signalservice.internal.push.WhoAmIResponse
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
import java.io.IOException import java.io.IOException
import java.security.MessageDigest import java.security.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.util.concurrent.locks.ReentrantLock
private val TAG: String = Log.tag(ChangeNumberRepository::class.java) private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
class ChangeNumberRepository(private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager()) { /**
* Provides various change number operations. All operations must run on [Schedulers.single] to support
* the global "I am changing the number" lock exclusivity.
*/
class ChangeNumberRepository(
private val accountManager: SignalServiceAccountManager = ApplicationDependencies.getSignalServiceAccountManager(),
private val messageSender: SignalServiceMessageSender = ApplicationDependencies.getSignalServiceMessageSender()
) {
companion object {
/**
* This lock should be held by anyone who is performing a change number operation, so that two different parties cannot change the user's number
* at the same time.
*/
val CHANGE_NUMBER_LOCK = ReentrantLock()
/**
* Adds Rx operators to chain to acquire and release the [CHANGE_NUMBER_LOCK] on subscribe and on finish.
*/
fun <T : Any> acquireReleaseChangeNumberLock(upstream: Single<T>): Single<T> {
return upstream.doOnSubscribe {
CHANGE_NUMBER_LOCK.lock()
SignalStore.misc().lockChangeNumber()
}
.subscribeOn(Schedulers.single())
.observeOn(Schedulers.single())
.doFinally {
if (CHANGE_NUMBER_LOCK.isHeldByCurrentThread) {
CHANGE_NUMBER_LOCK.unlock()
}
}
}
}
fun ensureDecryptionsDrained(): Completable { fun ensureDecryptionsDrained(): Completable {
return Completable.create { emitter -> return Completable.create { emitter ->
@@ -56,15 +89,38 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
.addDecryptionDrainedListener { .addDecryptionDrainedListener {
emitter.onComplete() emitter.onComplete()
} }
}.subscribeOn(Schedulers.io()) }.subscribeOn(Schedulers.single())
} }
fun changeNumber(code: String, newE164: String): Single<ServiceResponse<VerifyAccountResponse>> { fun changeNumber(code: String, newE164: String, pniUpdateMode: Boolean = false): Single<ServiceResponse<VerifyAccountResponse>> {
return Single.fromCallable { return Single.fromCallable {
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, null) var completed = false
SignalStore.misc().setPendingChangeNumberMetadata(metadata) var attempts = 0
accountManager.changeNumber(request) lateinit var changeNumberResponse: ServiceResponse<VerifyAccountResponse>
}.subscribeOn(Schedulers.io())
while (!completed && attempts < 5) {
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(
code = code,
newE164 = newE164,
registrationLock = null,
pniUpdateMode = pniUpdateMode
)
SignalStore.misc().setPendingChangeNumberMetadata(metadata)
changeNumberResponse = accountManager.changeNumber(request)
val possibleError: Throwable? = changeNumberResponse.applicationError.orElse(null)
if (possibleError is MismatchedDevicesException) {
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
attempts++
} else {
completed = true
}
}
changeNumberResponse
}.subscribeOn(Schedulers.single())
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) } .onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
} }
@@ -75,42 +131,65 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
tokenData: TokenData tokenData: TokenData
): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> { ): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> {
return Single.fromCallable { return Single.fromCallable {
val kbsData: KbsPinData
val registrationLock: String
try { try {
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!! kbsData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
val registrationLock: String = kbsData.masterKey.deriveRegistrationLock() registrationLock = kbsData.masterKey.deriveRegistrationLock()
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(code, newE164, registrationLock) } catch (e: KeyBackupSystemWrongPinException) {
return@fromCallable ServiceResponse.forExecutionError(e)
} catch (e: KeyBackupSystemNoDataException) {
return@fromCallable ServiceResponse.forExecutionError(e)
} catch (e: IOException) {
return@fromCallable ServiceResponse.forExecutionError(e)
}
var completed = false
var attempts = 0
lateinit var changeNumberResponse: ServiceResponse<VerifyAccountResponse>
while (!completed && attempts < 5) {
val (request: ChangePhoneNumberRequest, metadata: PendingChangeNumberMetadata) = createChangeNumberRequest(
code = code,
newE164 = newE164,
registrationLock = registrationLock,
pniUpdateMode = false
)
SignalStore.misc().setPendingChangeNumberMetadata(metadata) SignalStore.misc().setPendingChangeNumberMetadata(metadata)
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(request) changeNumberResponse = accountManager.changeNumber(request)
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
} catch (e: KeyBackupSystemWrongPinException) { val possibleError: Throwable? = changeNumberResponse.applicationError.orElse(null)
ServiceResponse.forExecutionError(e) if (possibleError is MismatchedDevicesException) {
} catch (e: KeyBackupSystemNoDataException) { messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
ServiceResponse.forExecutionError(e) attempts++
} catch (e: IOException) { } else {
ServiceResponse.forExecutionError(e) completed = true
}
} }
}.subscribeOn(Schedulers.io())
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(changeNumberResponse, kbsData)
}.subscribeOn(Schedulers.single())
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) } .onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
} }
@Suppress("UsePropertyAccessSyntax") @Suppress("UsePropertyAccessSyntax")
fun whoAmI(): Single<WhoAmIResponse> { fun whoAmI(): Single<WhoAmIResponse> {
return Single.fromCallable { ApplicationDependencies.getSignalServiceAccountManager().getWhoAmI() } return Single.fromCallable { ApplicationDependencies.getSignalServiceAccountManager().getWhoAmI() }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.single())
} }
@WorkerThread @WorkerThread
fun changeLocalNumber(e164: String, pni: PNI): Single<Unit> { fun changeLocalNumber(e164: String, pni: PNI): Single<Unit> {
val oldStorageId: ByteArray? = Recipient.self().storageServiceId val oldStorageId: ByteArray? = Recipient.self().storageServiceId
SignalDatabase.recipients.updateSelfPhone(e164) SignalDatabase.recipients.updateSelfPhone(e164, pni)
val newStorageId: ByteArray? = Recipient.self().storageServiceId val newStorageId: ByteArray? = Recipient.self().storageServiceId
if (MessageDigest.isEqual(oldStorageId, newStorageId)) { if (e164 != SignalStore.account().requireE164() && MessageDigest.isEqual(oldStorageId, newStorageId)) {
Log.w(TAG, "Self storage id was not rotated, attempting to rotate again") Log.w(TAG, "Self storage id was not rotated, attempting to rotate again")
SignalDatabase.recipients.rotateStorageId(Recipient.self().id) SignalDatabase.recipients.rotateStorageId(Recipient.self().id)
Recipient.self().live().refresh()
StorageSyncHelper.scheduleSyncForDataChange() StorageSyncHelper.scheduleSyncForDataChange()
val secondAttemptStorageId: ByteArray? = Recipient.self().storageServiceId val secondAttemptStorageId: ByteArray? = Recipient.self().storageServiceId
if (MessageDigest.isEqual(oldStorageId, secondAttemptStorageId)) { if (MessageDigest.isEqual(oldStorageId, secondAttemptStorageId)) {
@@ -118,7 +197,7 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
} }
} }
SignalDatabase.recipients.setPni(Recipient.self().id, pni) ApplicationDependencies.getRecipientCache().clear()
SignalStore.account().setE164(e164) SignalStore.account().setE164(e164)
SignalStore.account().setPni(pni) SignalStore.account().setPni(pni)
@@ -161,6 +240,9 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
System.currentTimeMillis(), System.currentTimeMillis(),
true true
) )
SignalStore.misc().setPniInitializedDevices(true)
ApplicationDependencies.getGroupsV2Authorization().clear()
} }
Recipient.self().live().refresh() Recipient.self().live().refresh()
@@ -190,7 +272,7 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
SignalStore.certificateValues().setUnidentifiedAccessCertificate(certificateType, certificate) SignalStore.certificateValues().setUnidentifiedAccessCertificate(certificateType, certificate)
} }
}.subscribeOn(Schedulers.io()) }.subscribeOn(Schedulers.single())
} }
@Suppress("UsePropertyAccessSyntax") @Suppress("UsePropertyAccessSyntax")
@@ -198,51 +280,69 @@ class ChangeNumberRepository(private val accountManager: SignalServiceAccountMan
private fun createChangeNumberRequest( private fun createChangeNumberRequest(
code: String, code: String,
newE164: String, newE164: String,
registrationLock: String? registrationLock: String?,
pniUpdateMode: Boolean
): ChangeNumberRequestData { ): ChangeNumberRequestData {
val messageSender: SignalServiceMessageSender = ApplicationDependencies.getSignalServiceMessageSender() val selfIdentifier: String = SignalStore.account().requireAci().toString()
val pniProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().pni() val aciProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().aci()
val pniMetadataStore: PreKeyMetadataStore = SignalStore.account().pniPreKeys
val devices: List<DeviceInfo> = accountManager.getDevices() val pniIdentity: IdentityKeyPair = if (pniUpdateMode) SignalStore.account().pniIdentityKey else IdentityKeyUtil.generateIdentityKeyPair()
val pniIdentity: IdentityKeyPair = IdentityKeyUtil.generateIdentityKeyPair()
val deviceMessages = mutableListOf<OutgoingPushMessage>() val deviceMessages = mutableListOf<OutgoingPushMessage>()
val devicePniSignedPreKeys = mutableMapOf<String, SignedPreKeyEntity>() val devicePniSignedPreKeys = mutableMapOf<Int, SignedPreKeyEntity>()
val pniRegistrationIds = mutableMapOf<String, Int>() val pniRegistrationIds = mutableMapOf<Int, Int>()
val primaryDeviceId = SignalServiceAddress.DEFAULT_DEVICE_ID.toString() val primaryDeviceId: Int = SignalServiceAddress.DEFAULT_DEVICE_ID
for (device in devices) { val devices: List<Int> = listOf(primaryDeviceId) + aciProtocolStore.getSubDeviceSessions(selfIdentifier)
val deviceId = device.id.toString()
// Signed Prekeys devices
val signedPreKeyRecord = if (deviceId == primaryDeviceId) { .filter { it == primaryDeviceId || aciProtocolStore.containsSession(SignalProtocolAddress(selfIdentifier, it)) }
PreKeyUtil.generateAndStoreSignedPreKey(pniProtocolStore, pniMetadataStore, pniIdentity.privateKey, false) .forEach { deviceId ->
} else { // Signed Prekeys
PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey) val signedPreKeyRecord = if (deviceId == primaryDeviceId) {
if (pniUpdateMode) {
ApplicationDependencies.getProtocolStore().pni().loadSignedPreKey(SignalStore.account().pniPreKeys.activeSignedPreKeyId)
} else {
PreKeyUtil.generateAndStoreSignedPreKey(ApplicationDependencies.getProtocolStore().pni(), SignalStore.account().pniPreKeys, pniIdentity.privateKey)
}
} else {
PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), pniIdentity.privateKey)
}
devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
// Registration Ids
var pniRegistrationId = if (deviceId == primaryDeviceId && pniUpdateMode) {
SignalStore.account().pniRegistrationId
} else {
-1
}
while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) {
pniRegistrationId = KeyHelper.generateRegistrationId(false)
}
pniRegistrationIds[deviceId] = pniRegistrationId
// Device Messages
if (deviceId != primaryDeviceId) {
val pniChangeNumber = SyncMessage.PniChangeNumber.newBuilder()
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
.setRegistrationId(pniRegistrationId)
.build()
deviceMessages += messageSender.getEncryptedSyncPniChangeNumberMessage(deviceId, pniChangeNumber)
}
} }
devicePniSignedPreKeys[deviceId] = SignedPreKeyEntity(signedPreKeyRecord.id, signedPreKeyRecord.keyPair.publicKey, signedPreKeyRecord.signature)
// Registration Ids val request = ChangePhoneNumberRequest(
var pniRegistrationId = -1 newE164,
while (pniRegistrationId < 0 || pniRegistrationIds.values.contains(pniRegistrationId)) { code,
pniRegistrationId = KeyHelper.generateRegistrationId(false) registrationLock,
} pniIdentity.publicKey,
pniRegistrationIds[deviceId] = pniRegistrationId deviceMessages,
devicePniSignedPreKeys.mapKeys { it.key.toString() },
pniRegistrationIds.mapKeys { it.key.toString() }
)
// Device Messages
if (deviceId != primaryDeviceId) {
val pniChangeNumber = SyncMessage.PniChangeNumber.newBuilder()
.setIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
.setSignedPreKey(signedPreKeyRecord.serialize().toProtoByteString())
.setRegistrationId(pniRegistrationId)
.build()
deviceMessages += messageSender.getEncryptedSyncPniChangeNumberMessage(device.id, pniChangeNumber)
}
}
val request = ChangePhoneNumberRequest(newE164, code, registrationLock, pniIdentity.publicKey, deviceMessages, devicePniSignedPreKeys, pniRegistrationIds)
val metadata = PendingChangeNumberMetadata.newBuilder() val metadata = PendingChangeNumberMetadata.newBuilder()
.setPreviousPni(SignalStore.account().pni!!.toByteString()) .setPreviousPni(SignalStore.account().pni!!.toByteString())
.setPniIdentityKeyPair(pniIdentity.serialize().toProtoByteString()) .setPniIdentityKeyPair(pniIdentity.serialize().toProtoByteString())

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository import org.thoughtcrime.securesms.pin.KbsRepository
import org.thoughtcrime.securesms.pin.TokenData import org.thoughtcrime.securesms.pin.TokenData
import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver
import org.thoughtcrime.securesms.registration.VerifyAccountRepository import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs
@@ -38,6 +39,7 @@ class ChangeNumberViewModel(
password: String, password: String,
verifyAccountRepository: VerifyAccountRepository, verifyAccountRepository: VerifyAccountRepository,
kbsRepository: KbsRepository, kbsRepository: KbsRepository,
private val smsRetrieverReceiver: SmsRetrieverReceiver = SmsRetrieverReceiver(ApplicationDependencies.getApplication())
) : BaseRegistrationViewModel(savedState, verifyAccountRepository, kbsRepository, password) { ) : BaseRegistrationViewModel(savedState, verifyAccountRepository, kbsRepository, password) {
var oldNumberState: NumberViewState = NumberViewState.Builder().build() var oldNumberState: NumberViewState = NumberViewState.Builder().build()
@@ -57,6 +59,13 @@ class ChangeNumberViewModel(
} catch (e: NumberParseException) { } catch (e: NumberParseException) {
Log.i(TAG, "Unable to parse number for default country code") Log.i(TAG, "Unable to parse number for default country code")
} }
smsRetrieverReceiver.registerReceiver()
}
override fun onCleared() {
super.onCleared()
smsRetrieverReceiver.unregisterReceiver()
} }
fun getLiveOldNumber(): LiveData<NumberViewState> { fun getLiveOldNumber(): LiveData<NumberViewState> {
@@ -114,13 +123,13 @@ class ChangeNumberViewModel(
override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyAccountResponseProcessor> { override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyAccountResponseProcessor> {
return super.verifyCodeWithoutRegistrationLock(code) return super.verifyCodeWithoutRegistrationLock(code)
.doOnSubscribe { SignalStore.misc().lockChangeNumber() } .compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
.flatMap(this::attemptToUnlockChangeNumber) .flatMap(this::attemptToUnlockChangeNumber)
} }
override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> { override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> {
return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin) return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
.doOnSubscribe { SignalStore.misc().lockChangeNumber() } .compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
.flatMap(this::attemptToUnlockChangeNumber) .flatMap(this::attemptToUnlockChangeNumber)
} }

View File

@@ -4,10 +4,10 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) { class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__chats) {
@@ -19,7 +19,7 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
viewModel.refresh() viewModel.refresh()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
val repository = ChatsSettingsRepository() val repository = ChatsSettingsRepository()
val factory = ChatsSettingsViewModel.Factory(repository) val factory = ChatsSettingsViewModel.Factory(repository)
viewModel = ViewModelProvider(this, factory)[ChatsSettingsViewModel::class.java] viewModel = ViewModelProvider(this, factory)[ChatsSettingsViewModel::class.java]

View File

@@ -1,20 +1,28 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms package org.thoughtcrime.securesms.components.settings.app.chats.sms
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.provider.Settings import android.provider.Settings
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation import androidx.navigation.Navigation
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.SmsUtil import org.thoughtcrime.securesms.util.SmsUtil
import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
private const val SMS_REQUEST_CODE: Short = 1234 private const val SMS_REQUEST_CODE: Short = 1234
@@ -22,13 +30,20 @@ private const val SMS_REQUEST_CODE: Short = 1234
class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) { class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
private lateinit var viewModel: SmsSettingsViewModel private lateinit var viewModel: SmsSettingsViewModel
private lateinit var smsExportLauncher: ActivityResultLauncher<Intent>
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
viewModel.checkSmsEnabled() viewModel.checkSmsEnabled()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
smsExportLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
showSmsRemovalDialog()
}
}
viewModel = ViewModelProvider(this)[SmsSettingsViewModel::class.java] viewModel = ViewModelProvider(this)[SmsSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { viewModel.state.observe(viewLifecycleOwner) {
@@ -42,6 +57,32 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
private fun getConfiguration(state: SmsSettingsState): DSLConfiguration { private fun getConfiguration(state: SmsSettingsState): DSLConfiguration {
return configure { return configure {
when (state.smsExportState) {
SmsSettingsState.SmsExportState.FETCHING -> Unit
SmsSettingsState.SmsExportState.HAS_UNEXPORTED_MESSAGES -> {
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__export_sms_messages),
onClick = {
smsExportLauncher.launch(SmsExportActivity.createIntent(requireContext()))
}
)
dividerPref()
}
SmsSettingsState.SmsExportState.ALL_MESSAGES_EXPORTED -> {
clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__remove_sms_messages),
onClick = {
showSmsRemovalDialog()
}
)
dividerPref()
}
SmsSettingsState.SmsExportState.NO_SMS_MESSAGES_IN_DATABASE -> Unit
SmsSettingsState.SmsExportState.NOT_AVAILABLE -> Unit
}
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
clickPref( clickPref(
title = DSLSettingsText.from(R.string.SmsSettingsFragment__use_as_default_sms_app), title = DSLSettingsText.from(R.string.SmsSettingsFragment__use_as_default_sms_app),
@@ -96,4 +137,21 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
startActivityForResult(intent, SMS_REQUEST_CODE.toInt()) startActivityForResult(intent, SMS_REQUEST_CODE.toInt())
} }
private fun showSmsRemovalDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.RemoveSmsMessagesDialogFragment__remove_sms_messages)
.setMessage(R.string.RemoveSmsMessagesDialogFragment__you_can_now_remove_sms_messages_from_signal)
.setPositiveButton(R.string.RemoveSmsMessagesDialogFragment__keep_messages) { _, _ ->
Snackbar.make(requireView(), R.string.SmsSettingsFragment__you_can_remove_sms_messages_from_signal_in_settings, Snackbar.LENGTH_SHORT).show()
}
.setNegativeButton(R.string.RemoveSmsMessagesDialogFragment__remove_messages) { _, _ ->
SignalExecutors.BOUNDED.execute {
SignalDatabase.sms.deleteExportedMessages()
SignalDatabase.mms.deleteExportedMessages()
}
Snackbar.make(requireView(), R.string.SmsSettingsFragment__removing_sms_messages_from_signal, Snackbar.LENGTH_SHORT).show()
}
.show()
}
} }

View File

@@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.MessageDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.util.FeatureFlags
class SmsSettingsRepository(
private val smsDatabase: MessageDatabase = SignalDatabase.sms,
private val mmsDatabase: MessageDatabase = SignalDatabase.mms
) {
fun getSmsExportState(): Single<SmsSettingsState.SmsExportState> {
if (!FeatureFlags.smsExporter()) {
return Single.just(SmsSettingsState.SmsExportState.NOT_AVAILABLE)
}
return Single.fromCallable {
checkInsecureMessageCount() ?: checkUnexportedInsecureMessageCount()
}.subscribeOn(Schedulers.io())
}
@WorkerThread
private fun checkInsecureMessageCount(): SmsSettingsState.SmsExportState? {
val totalSmsMmsCount = smsDatabase.insecureMessageCount + mmsDatabase.insecureMessageCount
return if (totalSmsMmsCount == 0) {
SmsSettingsState.SmsExportState.NO_SMS_MESSAGES_IN_DATABASE
} else {
null
}
}
@WorkerThread
private fun checkUnexportedInsecureMessageCount(): SmsSettingsState.SmsExportState {
val totalUnexportedCount = smsDatabase.unexportedInsecureMessagesCount + mmsDatabase.unexportedInsecureMessagesCount
return if (totalUnexportedCount > 0) {
SmsSettingsState.SmsExportState.HAS_UNEXPORTED_MESSAGES
} else {
SmsSettingsState.SmsExportState.ALL_MESSAGES_EXPORTED
}
}
}

View File

@@ -3,5 +3,14 @@ package org.thoughtcrime.securesms.components.settings.app.chats.sms
data class SmsSettingsState( data class SmsSettingsState(
val useAsDefaultSmsApp: Boolean, val useAsDefaultSmsApp: Boolean,
val smsDeliveryReportsEnabled: Boolean, val smsDeliveryReportsEnabled: Boolean,
val wifiCallingCompatibilityEnabled: Boolean val wifiCallingCompatibilityEnabled: Boolean,
) val smsExportState: SmsExportState = SmsExportState.FETCHING
) {
enum class SmsExportState {
FETCHING,
HAS_UNEXPORTED_MESSAGES,
ALL_MESSAGES_EXPORTED,
NO_SMS_MESSAGES_IN_DATABASE,
NOT_AVAILABLE
}
}

View File

@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.components.settings.app.chats.sms
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.Util
@@ -9,6 +11,9 @@ import org.thoughtcrime.securesms.util.livedata.Store
class SmsSettingsViewModel : ViewModel() { class SmsSettingsViewModel : ViewModel() {
private val repository = SmsSettingsRepository()
private val disposables = CompositeDisposable()
private val store = Store( private val store = Store(
SmsSettingsState( SmsSettingsState(
useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()), useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()),
@@ -19,6 +24,16 @@ class SmsSettingsViewModel : ViewModel() {
val state: LiveData<SmsSettingsState> = store.stateLiveData val state: LiveData<SmsSettingsState> = store.stateLiveData
init {
disposables += repository.getSmsExportState().subscribe { state ->
store.update { it.copy(smsExportState = state) }
}
}
override fun onCleared() {
disposables.clear()
}
fun setSmsDeliveryReportsEnabled(enabled: Boolean) { fun setSmsDeliveryReportsEnabled(enabled: Boolean) {
store.update { it.copy(smsDeliveryReportsEnabled = enabled) } store.update { it.copy(smsDeliveryReportsEnabled = enabled) }
SignalStore.settings().isSmsDeliveryReportsEnabled = enabled SignalStore.settings().isSmsDeliveryReportsEnabled = enabled

View File

@@ -5,12 +5,12 @@ import androidx.navigation.Navigation
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.mms.SentMediaQuality import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.webrtc.CallBandwidthMode import org.thoughtcrime.securesms.webrtc.CallBandwidthMode
import kotlin.math.abs import kotlin.math.abs
@@ -31,7 +31,7 @@ class DataAndStorageSettingsFragment : DSLSettingsFragment(R.string.preferences_
viewModel.refresh() viewModel.refresh()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val repository = DataAndStorageSettingsRepository() val repository = DataAndStorageSettingsRepository()
val factory = DataAndStorageSettingsViewModel.Factory(preferences, repository) val factory = DataAndStorageSettingsViewModel.Factory(preferences, repository)

View File

@@ -4,15 +4,15 @@ import androidx.navigation.Navigation
import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
class HelpSettingsFragment : DSLSettingsFragment(R.string.preferences__help) { class HelpSettingsFragment : DSLSettingsFragment(R.string.preferences__help) {
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
adapter.submitList(getConfiguration().toMappingModelList()) adapter.submitList(getConfiguration().toMappingModelList())
} }

View File

@@ -15,12 +15,12 @@ import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.LocalMetricsDatabase import org.thoughtcrime.securesms.database.LocalMetricsDatabase
import org.thoughtcrime.securesms.database.LogDatabase import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.database.MegaphoneDatabase
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobmanager.JobTracker import org.thoughtcrime.securesms.jobmanager.JobTracker
@@ -35,10 +35,12 @@ import org.thoughtcrime.securesms.jobs.StorageForcePushJob
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob
import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob import org.thoughtcrime.securesms.jobs.SubscriptionReceiptRequestResponseJob
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.payments.DataExportUtil import org.thoughtcrime.securesms.payments.DataExportUtil
import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.util.Optional import java.util.Optional
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -48,7 +50,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
private lateinit var viewModel: InternalSettingsViewModel private lateinit var viewModel: InternalSettingsViewModel
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
val repository = InternalSettingsRepository(requireContext()) val repository = InternalSettingsRepository(requireContext())
val factory = InternalSettingsViewModel.Factory(repository) val factory = InternalSettingsViewModel.Factory(repository)
viewModel = ViewModelProvider(this, factory)[InternalSettingsViewModel::class.java] viewModel = ViewModelProvider(this, factory)[InternalSettingsViewModel::class.java]
@@ -168,15 +170,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
sectionHeaderPref(R.string.preferences__internal_preferences_groups_v2) sectionHeaderPref(R.string.preferences__internal_preferences_groups_v2)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_do_not_create_gv2),
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_create_gv2_description),
isChecked = state.gv2doNotCreateGv2Groups,
onClick = {
viewModel.setGv2DoNotCreateGv2Groups(!state.gv2doNotCreateGv2Groups)
}
)
switchPref( switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites), title = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites),
summary = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites_description), summary = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites_description),
@@ -206,28 +199,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
dividerPref() dividerPref()
sectionHeaderPref(R.string.preferences__internal_preferences_groups_v1_migration)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_do_not_initiate_automigrate),
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_initiate_automigrate_description),
isChecked = state.disableAutoMigrationInitiation,
onClick = {
viewModel.setDisableAutoMigrationInitiation(!state.disableAutoMigrationInitiation)
}
)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_do_not_notify_automigrate),
summary = DSLSettingsText.from(R.string.preferences__internal_do_not_notify_automigrate_description),
isChecked = state.disableAutoMigrationNotification,
onClick = {
viewModel.setDisableAutoMigrationNotification(!state.disableAutoMigrationNotification)
}
)
dividerPref()
sectionHeaderPref(R.string.preferences__internal_network) sectionHeaderPref(R.string.preferences__internal_network)
switchPref( switchPref(
@@ -393,7 +364,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
} }
) )
if (FeatureFlags.donorBadges() && SignalStore.donationsValues().getSubscriber() != null) { if (SignalStore.donationsValues().getSubscriber() != null) {
dividerPref() dividerPref()
sectionHeaderPref(R.string.preferences__internal_badges) sectionHeaderPref(R.string.preferences__internal_badges)
@@ -431,6 +402,19 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
} }
) )
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_reset_donation_megaphone),
onClick = {
SignalDatabase.remoteMegaphones.debugRemoveAll()
MegaphoneDatabase.getInstance(ApplicationDependencies.getApplication()).let {
it.delete(Megaphones.Event.REMOTE_MEGAPHONE)
it.markFirstVisible(Megaphones.Event.DONATE_Q2_2022, System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31))
}
// Force repository database cache refresh
MegaphoneRepository(ApplicationDependencies.getApplication()).onFirstEverAppLaunch()
}
)
clickPref( clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_fetch_release_channel), title = DSLSettingsText.from(R.string.preferences__internal_fetch_release_channel),
onClick = { onClick = {
@@ -478,14 +462,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
sectionHeaderPref(R.string.ConversationListTabs__stories) sectionHeaderPref(R.string.ConversationListTabs__stories)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_disable_stories),
isChecked = state.disableStories,
onClick = {
viewModel.toggleStories()
}
)
clickPref( clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_clear_onboarding_state), title = DSLSettingsText.from(R.string.preferences__internal_clear_onboarding_state),
summary = DSLSettingsText.from(R.string.preferences__internal_clears_onboarding_flag_and_triggers_download_of_onboarding_stories), summary = DSLSettingsText.from(R.string.preferences__internal_clears_onboarding_flag_and_triggers_download_of_onboarding_stories),
@@ -494,6 +470,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
viewModel.onClearOnboardingState() viewModel.onClearOnboardingState()
} }
) )
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_stories_dialog_launcher),
onClick = {
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToStoryDialogsLauncherFragment())
}
)
} }
} }

View File

@@ -6,12 +6,9 @@ import org.thoughtcrime.securesms.emoji.EmojiFiles
data class InternalSettingsState( data class InternalSettingsState(
val seeMoreUserDetails: Boolean, val seeMoreUserDetails: Boolean,
val shakeToReport: Boolean, val shakeToReport: Boolean,
val gv2doNotCreateGv2Groups: Boolean,
val gv2forceInvites: Boolean, val gv2forceInvites: Boolean,
val gv2ignoreServerChanges: Boolean, val gv2ignoreServerChanges: Boolean,
val gv2ignoreP2PChanges: Boolean, val gv2ignoreP2PChanges: Boolean,
val disableAutoMigrationInitiation: Boolean,
val disableAutoMigrationNotification: Boolean,
val allowCensorshipSetting: Boolean, val allowCensorshipSetting: Boolean,
val callingServer: String, val callingServer: String,
val callingAudioProcessingMethod: CallManager.AudioProcessingMethod, val callingAudioProcessingMethod: CallManager.AudioProcessingMethod,
@@ -22,6 +19,5 @@ data class InternalSettingsState(
val removeSenderKeyMinimium: Boolean, val removeSenderKeyMinimium: Boolean,
val delayResends: Boolean, val delayResends: Boolean,
val disableStorageService: Boolean, val disableStorageService: Boolean,
val disableStories: Boolean,
val canClearOnboardingState: Boolean val canClearOnboardingState: Boolean
) )

View File

@@ -7,6 +7,7 @@ import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob
import org.thoughtcrime.securesms.keyvalue.InternalValues import org.thoughtcrime.securesms.keyvalue.InternalValues
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.livedata.Store import org.thoughtcrime.securesms.util.livedata.Store
@@ -38,11 +39,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh() refresh()
} }
fun setGv2DoNotCreateGv2Groups(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_DO_NOT_CREATE_GV2, enabled)
refresh()
}
fun setGv2ForceInvites(enabled: Boolean) { fun setGv2ForceInvites(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_FORCE_INVITES, enabled) preferenceDataStore.putBoolean(InternalValues.GV2_FORCE_INVITES, enabled)
refresh() refresh()
@@ -58,16 +54,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh() refresh()
} }
fun setDisableAutoMigrationInitiation(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_DISABLE_AUTOMIGRATE_INITIATION, enabled)
refresh()
}
fun setDisableAutoMigrationNotification(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_DISABLE_AUTOMIGRATE_NOTIFICATION, enabled)
refresh()
}
fun setAllowCensorshipSetting(enabled: Boolean) { fun setAllowCensorshipSetting(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.ALLOW_CENSORSHIP_SETTING, enabled) preferenceDataStore.putBoolean(InternalValues.ALLOW_CENSORSHIP_SETTING, enabled)
refresh() refresh()
@@ -108,12 +94,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh() refresh()
} }
fun toggleStories() {
val newState = !SignalStore.storyValues().isFeatureDisabled
SignalStore.storyValues().isFeatureDisabled = newState
store.update { getState().copy(disableStories = newState) }
}
fun addSampleReleaseNote() { fun addSampleReleaseNote() {
repository.addSampleReleaseNote() repository.addSampleReleaseNote()
} }
@@ -125,12 +105,9 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
private fun getState() = InternalSettingsState( private fun getState() = InternalSettingsState(
seeMoreUserDetails = SignalStore.internalValues().recipientDetails(), seeMoreUserDetails = SignalStore.internalValues().recipientDetails(),
shakeToReport = SignalStore.internalValues().shakeToReport(), shakeToReport = SignalStore.internalValues().shakeToReport(),
gv2doNotCreateGv2Groups = SignalStore.internalValues().gv2DoNotCreateGv2Groups(),
gv2forceInvites = SignalStore.internalValues().gv2ForceInvites(), gv2forceInvites = SignalStore.internalValues().gv2ForceInvites(),
gv2ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(), gv2ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(),
gv2ignoreP2PChanges = SignalStore.internalValues().gv2IgnoreP2PChanges(), gv2ignoreP2PChanges = SignalStore.internalValues().gv2IgnoreP2PChanges(),
disableAutoMigrationInitiation = SignalStore.internalValues().disableGv1AutoMigrateInitiation(),
disableAutoMigrationNotification = SignalStore.internalValues().disableGv1AutoMigrateNotification(),
allowCensorshipSetting = SignalStore.internalValues().allowChangingCensorshipSetting(), allowCensorshipSetting = SignalStore.internalValues().allowChangingCensorshipSetting(),
callingServer = SignalStore.internalValues().groupCallingServer(), callingServer = SignalStore.internalValues().groupCallingServer(),
callingAudioProcessingMethod = SignalStore.internalValues().callingAudioProcessingMethod(), callingAudioProcessingMethod = SignalStore.internalValues().callingAudioProcessingMethod(),
@@ -141,13 +118,13 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum(), removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum(),
delayResends = SignalStore.internalValues().delayResends(), delayResends = SignalStore.internalValues().delayResends(),
disableStorageService = SignalStore.internalValues().storageServiceDisabled(), disableStorageService = SignalStore.internalValues().storageServiceDisabled(),
disableStories = SignalStore.storyValues().isFeatureDisabled,
canClearOnboardingState = SignalStore.storyValues().hasDownloadedOnboardingStory && Stories.isFeatureEnabled() canClearOnboardingState = SignalStore.storyValues().hasDownloadedOnboardingStory && Stories.isFeatureEnabled()
) )
fun onClearOnboardingState() { fun onClearOnboardingState() {
SignalStore.storyValues().hasDownloadedOnboardingStory = false SignalStore.storyValues().hasDownloadedOnboardingStory = false
SignalStore.storyValues().userHasSeenOnboardingStory = false SignalStore.storyValues().userHasSeenOnboardingStory = false
Stories.onStorySettingsChanged(Recipient.self().id)
refresh() refresh()
StoryOnboardingDownloadJob.enqueueIfNeeded() StoryOnboardingDownloadJob.enqueueIfNeeded()
} }

View File

@@ -0,0 +1,49 @@
package org.thoughtcrime.securesms.components.settings.app.internal
import android.widget.Toast
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
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.stories.dialogs.StoryDialogs
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
class StoryDialogLauncherFragment : DSLSettingsFragment(titleId = R.string.preferences__internal_stories_dialog_launcher) {
override fun bindAdapter(adapter: MappingAdapter) {
adapter.submitList(getConfiguration().toMappingModelList())
}
private fun getConfiguration(): DSLConfiguration {
return configure {
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_retry_send),
onClick = {
StoryDialogs.resendStory(requireContext()) {
Toast.makeText(requireContext(), R.string.preferences__internal_retry_send, Toast.LENGTH_SHORT).show()
}
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_story_or_profile_selector),
onClick = {
StoryDialogs.displayStoryOrProfileImage(
context = requireContext(),
onViewStory = { Toast.makeText(requireContext(), R.string.StoryDialogs__view_story, Toast.LENGTH_SHORT).show() },
onViewAvatar = { Toast.makeText(requireContext(), R.string.StoryDialogs__view_profile_photo, Toast.LENGTH_SHORT).show() }
)
}
)
clickPref(
title = DSLSettingsText.from(R.string.preferences__internal_hide_story),
onClick = {
StoryDialogs.hideStory(requireContext(), "Spiderman") {
Toast.makeText(requireContext(), R.string.preferences__internal_hide_story, Toast.LENGTH_SHORT).show()
}
}
)
}
}
}

View File

@@ -5,19 +5,19 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.donations.StripeDeclineCode import org.signal.donations.StripeDeclineCode
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
class DonorErrorConfigurationFragment : DSLSettingsFragment() { class DonorErrorConfigurationFragment : DSLSettingsFragment() {
private val viewModel: DonorErrorConfigurationViewModel by viewModels() private val viewModel: DonorErrorConfigurationViewModel by viewModels()
private val lifecycleDisposable = LifecycleDisposable() private val lifecycleDisposable = LifecycleDisposable()
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state -> lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
adapter.submitList(getConfiguration(state).toMappingModelList()) adapter.submitList(getConfiguration(state).toMappingModelList())
} }

View File

@@ -22,7 +22,6 @@ import androidx.preference.PreferenceManager
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.PreferenceModel import org.thoughtcrime.securesms.components.settings.PreferenceModel
@@ -35,6 +34,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.util.RingtoneUtil import org.thoughtcrime.securesms.util.RingtoneUtil
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
private const val MESSAGE_SOUND_SELECT: Int = 1 private const val MESSAGE_SOUND_SELECT: Int = 1
@@ -70,7 +70,7 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__
} }
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
adapter.registerFactory( adapter.registerFactory(
LedColorPreference::class.java, LedColorPreference::class.java,
LayoutFactory(::LedColorPreferenceViewHolder, R.layout.dsl_preference_item) LayoutFactory(::LedColorPreferenceViewHolder, R.layout.dsl_preference_item)

View File

@@ -8,7 +8,6 @@ import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileAddMembers import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileAddMembers
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileRecipient import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileRecipient
@@ -18,6 +17,7 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
@@ -42,7 +42,7 @@ class AddAllowedMembersFragment : DSLSettingsFragment(layoutId = R.layout.fragme
} }
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
NotificationProfileAddMembers.register(adapter) NotificationProfileAddMembers.register(adapter)
NotificationProfileRecipient.register(adapter) NotificationProfileRecipient.register(adapter)

View File

@@ -17,7 +17,6 @@ import org.signal.core.util.BreakIteratorCompat
import org.signal.core.util.EditTextUtil import org.signal.core.util.EditTextUtil
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.emoji.EmojiUtil import org.thoughtcrime.securesms.components.emoji.EmojiUtil
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileViewModel.SaveNotificationProfileResult import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.EditNotificationProfileViewModel.SaveNotificationProfileResult
import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileNamePreset import org.thoughtcrime.securesms.components.settings.app.notifications.profiles.models.NotificationProfileNamePreset
@@ -25,6 +24,7 @@ import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiBottomSheetDial
import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.text.AfterTextChanged import org.thoughtcrime.securesms.util.text.AfterTextChanged
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
@@ -131,7 +131,7 @@ class EditNotificationProfileFragment : DSLSettingsFragment(layoutId = R.layout.
this.emojiView = emojiView this.emojiView = emojiView
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
NotificationProfileNamePreset.register(adapter) NotificationProfileNamePreset.register(adapter)
val onClick = { preset: NotificationProfileNamePreset.Model -> val onClick = { preset: NotificationProfileNamePreset.Model ->

View File

@@ -13,7 +13,6 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.emoji.EmojiUtil import org.thoughtcrime.securesms.components.emoji.EmojiUtil
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
@@ -31,6 +30,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.formatHours import org.thoughtcrime.securesms.util.formatHours
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.orderOfDaysInWeek import org.thoughtcrime.securesms.util.orderOfDaysInWeek
@@ -65,7 +65,7 @@ class NotificationProfileDetailsFragment : DSLSettingsFragment() {
toolbar = null toolbar = null
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
NotificationProfilePreference.register(adapter) NotificationProfilePreference.register(adapter)
NotificationProfileAddMembers.register(adapter) NotificationProfileAddMembers.register(adapter)
NotificationProfileRecipient.register(adapter) NotificationProfileRecipient.register(adapter)

View File

@@ -8,7 +8,6 @@ import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.emoji.EmojiUtil import org.thoughtcrime.securesms.components.emoji.EmojiUtil
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
@@ -20,6 +19,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.L
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
/** /**
@@ -48,7 +48,7 @@ class NotificationProfilesFragment : DSLSettingsFragment() {
toolbar = null toolbar = null
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
NoNotificationProfiles.register(adapter) NoNotificationProfiles.register(adapter)
LargeIconClickPreference.register(adapter) LargeIconClickPreference.register(adapter)
NotificationProfilePreference.register(adapter) NotificationProfilePreference.register(adapter)

View File

@@ -1,8 +1,11 @@
package org.thoughtcrime.securesms.components.settings.app.privacy package org.thoughtcrime.securesms.components.settings.app.privacy
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.Spanned import android.text.Spanned
import android.text.style.TextAppearanceSpan import android.text.style.TextAppearanceSpan
@@ -16,18 +19,17 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation import androidx.navigation.Navigation
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import mobi.upod.timedurationpicker.TimeDurationPicker import mobi.upod.timedurationpicker.TimeDurationPicker
import mobi.upod.timedurationpicker.TimeDurationPickerDialog import mobi.upod.timedurationpicker.TimeDurationPickerDialog
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.PassphraseChangeActivity import org.thoughtcrime.securesms.PassphraseChangeActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.ClickPreference import org.thoughtcrime.securesms.components.settings.ClickPreference
import org.thoughtcrime.securesms.components.settings.ClickPreferenceViewHolder import org.thoughtcrime.securesms.components.settings.ClickPreferenceViewHolder
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.PreferenceModel import org.thoughtcrime.securesms.components.settings.PreferenceModel
@@ -36,12 +38,8 @@ import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.crypto.MasterSecretUtil import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberListingMode import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberListingMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.KeyCachingService import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.stories.settings.custom.PrivateStorySettingsFragmentArgs
import org.thoughtcrime.securesms.stories.settings.story.PrivateStoryItem
import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.ExpirationUtil import org.thoughtcrime.securesms.util.ExpirationUtil
@@ -50,6 +48,7 @@ import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.lang.Integer.max import java.lang.Integer.max
import java.util.Locale import java.util.Locale
@@ -76,17 +75,22 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
viewModel.refreshBlockedCount() viewModel.refreshBlockedCount()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
adapter.registerFactory(ValueClickPreference::class.java, LayoutFactory(::ValueClickPreferenceViewHolder, R.layout.value_click_preference_item)) adapter.registerFactory(ValueClickPreference::class.java, LayoutFactory(::ValueClickPreferenceViewHolder, R.layout.value_click_preference_item))
PrivateStoryItem.register(adapter)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val repository = PrivacySettingsRepository() val repository = PrivacySettingsRepository()
val factory = PrivacySettingsViewModel.Factory(sharedPreferences, repository) val factory = PrivacySettingsViewModel.Factory(sharedPreferences, repository)
viewModel = ViewModelProvider(this, factory)[PrivacySettingsViewModel::class.java] viewModel = ViewModelProvider(this, factory)[PrivacySettingsViewModel::class.java]
val args: PrivacySettingsFragmentArgs by navArgs()
var showPaymentLock = true
viewModel.state.observe(viewLifecycleOwner) { state -> viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList()) adapter.submitList(getConfiguration(state).toMappingModelList())
if (args.showPaymentLock && showPaymentLock) {
showPaymentLock = false
recyclerView?.scrollToPosition(adapter.itemCount - 1)
}
} }
} }
@@ -215,7 +219,7 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
summary = DSLSettingsText.from(R.string.preferences__auto_lock_signal_after_a_specified_time_interval_of_inactivity), summary = DSLSettingsText.from(R.string.preferences__auto_lock_signal_after_a_specified_time_interval_of_inactivity),
isChecked = state.isObsoletePasswordTimeoutEnabled, isChecked = state.isObsoletePasswordTimeoutEnabled,
onClick = { onClick = {
viewModel.setObsoletePasswordTimeoutEnabled(!state.isObsoletePasswordEnabled) viewModel.setObsoletePasswordTimeoutEnabled(!state.isObsoletePasswordTimeoutEnabled)
} }
) )
@@ -297,56 +301,36 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
) )
if (Stories.isFeatureAvailable()) { if (Stories.isFeatureAvailable()) {
dividerPref() dividerPref()
sectionHeaderPref(R.string.ConversationListTabs__stories) clickPref(
title = DSLSettingsText.from(R.string.preferences__stories),
if (!SignalStore.storyValues().isFeatureDisabled) { summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__manage_your_stories),
customPref(
PrivateStoryItem.RecipientModel(
recipient = Recipient.self(),
onClick = { findNavController().safeNavigate(R.id.action_privacySettings_to_myStorySettings) }
)
)
space(DimensionUnit.DP.toPixels(24f).toInt())
customPref(
PrivateStoryItem.NewModel(
onClick = {
findNavController().safeNavigate(R.id.action_privacySettings_to_newPrivateStory)
}
)
)
state.privateStories.forEach {
customPref(
PrivateStoryItem.PartialModel(
privateStoryItemData = it,
onClick = { model ->
findNavController().safeNavigate(
R.id.action_privacySettings_to_privateStorySettings,
PrivateStorySettingsFragmentArgs.Builder(model.privateStoryItemData.id).build().toBundle()
)
}
)
)
}
}
switchPref(
title = DSLSettingsText.from(R.string.PrivacySettingsFragment__share_and_view_stories),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__you_will_no_longer_be_able),
isChecked = state.isStoriesEnabled,
onClick = { onClick = {
viewModel.setStoriesEnabled(!state.isStoriesEnabled) findNavController().safeNavigate(PrivacySettingsFragmentDirections.actionPrivacySettingsFragmentToStoryPrivacySettings(R.string.preferences__stories))
} }
) )
} }
dividerPref() dividerPref()
sectionHeaderPref(R.string.preferences_app_protection__payments)
switchPref(
title = DSLSettingsText.from(R.string.preferences__payment_lock),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__payment_lock_require_lock),
isChecked = state.paymentLock && ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure,
onClick = {
if (!ServiceUtil.getKeyguardManager(requireContext()).isKeyguardSecure) {
showGoToPhoneSettings()
} else {
viewModel.togglePaymentLock()
}
}
)
dividerPref()
clickPref( clickPref(
title = DSLSettingsText.from(R.string.preferences__advanced), title = DSLSettingsText.from(R.string.preferences__advanced),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__signal_message_and_calls), summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__signal_message_and_calls),
@@ -357,6 +341,29 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
} }
} }
private fun showGoToPhoneSettings() {
MaterialAlertDialogBuilder(requireContext()).apply {
setTitle(getString(R.string.PrivacySettingsFragment__cant_enable_title))
setMessage(getString(R.string.PrivacySettingsFragment__cant_enable_description))
setPositiveButton(R.string.PaymentsHomeFragment__enable) { _, _ ->
val intent = when {
Build.VERSION.SDK_INT >= 30 -> Intent(Settings.ACTION_BIOMETRIC_ENROLL)
Build.VERSION.SDK_INT >= 28 -> Intent(Settings.ACTION_FINGERPRINT_ENROLL)
else -> Intent(Settings.ACTION_SECURITY_SETTINGS)
}
try {
startActivity(intent)
} catch (e: ActivityNotFoundException) {
Log.w(TAG, "Failed to navigate to system settings.", e)
Toast.makeText(requireContext(), R.string.PrivacySettingsFragment__failed_to_navigate_to_system_settings, Toast.LENGTH_SHORT).show()
}
}
setNegativeButton(R.string.PaymentsHomeFragment__not_now) { _, _ -> }
show()
}
}
private fun getScreenLockInactivityTimeoutSummary(timeoutSeconds: Long): String { private fun getScreenLockInactivityTimeoutSummary(timeoutSeconds: Long): String {
val hours = TimeUnit.SECONDS.toHours(timeoutSeconds) val hours = TimeUnit.SECONDS.toHours(timeoutSeconds)
val minutes = TimeUnit.SECONDS.toMinutes(timeoutSeconds) - hours * 60 val minutes = TimeUnit.SECONDS.toMinutes(timeoutSeconds) - hours * 60

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.settings.app.privacy
import android.content.Context import android.content.Context
import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -23,12 +22,6 @@ class PrivacySettingsRepository {
} }
} }
fun getPrivateStories(consumer: (List<DistributionListPartialRecord>) -> Unit) {
SignalExecutors.BOUNDED.execute {
consumer(SignalDatabase.distributionLists.getCustomListsForUi())
}
}
fun syncReadReceiptState() { fun syncReadReceiptState() {
SignalExecutors.BOUNDED.execute { SignalExecutors.BOUNDED.execute {
SignalDatabase.recipients.markNeedsSync(Recipient.self().id) SignalDatabase.recipients.markNeedsSync(Recipient.self().id)

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components.settings.app.privacy package org.thoughtcrime.securesms.components.settings.app.privacy
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
data class PrivacySettingsState( data class PrivacySettingsState(
@@ -13,10 +12,9 @@ data class PrivacySettingsState(
val screenLockActivityTimeout: Long, val screenLockActivityTimeout: Long,
val screenSecurity: Boolean, val screenSecurity: Boolean,
val incognitoKeyboard: Boolean, val incognitoKeyboard: Boolean,
val paymentLock: Boolean,
val isObsoletePasswordEnabled: Boolean, val isObsoletePasswordEnabled: Boolean,
val isObsoletePasswordTimeoutEnabled: Boolean, val isObsoletePasswordTimeoutEnabled: Boolean,
val obsoletePasswordTimeout: Int, val obsoletePasswordTimeout: Int,
val universalExpireTimer: Int, val universalExpireTimer: Int
val privateStories: List<DistributionListPartialRecord>,
val isStoriesEnabled: Boolean
) )

View File

@@ -27,11 +27,6 @@ class PrivacySettingsViewModel(
store.update { it.copy(blockedCount = count) } store.update { it.copy(blockedCount = count) }
refresh() refresh()
} }
repository.getPrivateStories { privateStories ->
store.update { it.copy(privateStories = privateStories) }
refresh()
}
} }
fun setReadReceiptsEnabled(enabled: Boolean) { fun setReadReceiptsEnabled(enabled: Boolean) {
@@ -79,6 +74,11 @@ class PrivacySettingsViewModel(
refresh() refresh()
} }
fun togglePaymentLock() {
SignalStore.paymentsValues().paymentLock = state.value?.let { !it.paymentLock } ?: false
refresh()
}
fun setObsoletePasswordTimeoutEnabled(enabled: Boolean) { fun setObsoletePasswordTimeoutEnabled(enabled: Boolean) {
sharedPreferences.edit().putBoolean(TextSecurePreferences.PASSPHRASE_TIMEOUT_PREF, enabled).apply() sharedPreferences.edit().putBoolean(TextSecurePreferences.PASSPHRASE_TIMEOUT_PREF, enabled).apply()
refresh() refresh()
@@ -89,11 +89,6 @@ class PrivacySettingsViewModel(
refresh() refresh()
} }
fun setStoriesEnabled(isStoriesEnabled: Boolean) {
SignalStore.storyValues().isFeatureDisabled = !isStoriesEnabled
refresh()
}
fun refresh() { fun refresh() {
store.update(this::updateState) store.update(this::updateState)
} }
@@ -107,19 +102,18 @@ class PrivacySettingsViewModel(
screenLockActivityTimeout = TextSecurePreferences.getScreenLockTimeout(ApplicationDependencies.getApplication()), screenLockActivityTimeout = TextSecurePreferences.getScreenLockTimeout(ApplicationDependencies.getApplication()),
screenSecurity = TextSecurePreferences.isScreenSecurityEnabled(ApplicationDependencies.getApplication()), screenSecurity = TextSecurePreferences.isScreenSecurityEnabled(ApplicationDependencies.getApplication()),
incognitoKeyboard = TextSecurePreferences.isIncognitoKeyboardEnabled(ApplicationDependencies.getApplication()), incognitoKeyboard = TextSecurePreferences.isIncognitoKeyboardEnabled(ApplicationDependencies.getApplication()),
paymentLock = SignalStore.paymentsValues().paymentLock,
seeMyPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode, seeMyPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode,
findMeByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode, findMeByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode,
isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(ApplicationDependencies.getApplication()), isObsoletePasswordEnabled = !TextSecurePreferences.isPasswordDisabled(ApplicationDependencies.getApplication()),
isObsoletePasswordTimeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(ApplicationDependencies.getApplication()), isObsoletePasswordTimeoutEnabled = TextSecurePreferences.isPassphraseTimeoutEnabled(ApplicationDependencies.getApplication()),
obsoletePasswordTimeout = TextSecurePreferences.getPassphraseTimeoutInterval(ApplicationDependencies.getApplication()), obsoletePasswordTimeout = TextSecurePreferences.getPassphraseTimeoutInterval(ApplicationDependencies.getApplication()),
universalExpireTimer = SignalStore.settings().universalExpireTimer, universalExpireTimer = SignalStore.settings().universalExpireTimer
privateStories = emptyList(),
isStoriesEnabled = !SignalStore.storyValues().isFeatureDisabled
) )
} }
private fun updateState(state: PrivacySettingsState): PrivacySettingsState { private fun updateState(state: PrivacySettingsState): PrivacySettingsState {
return getState().copy(blockedCount = state.blockedCount, privateStories = state.privateStories) return getState().copy(blockedCount = state.blockedCount)
} }
class Factory( class Factory(

View File

@@ -19,7 +19,6 @@ import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
@@ -29,6 +28,7 @@ import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity
import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__advanced) { class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__advanced) {
@@ -75,7 +75,7 @@ class AdvancedPrivacySettingsFragment : DSLSettingsFragment(R.string.preferences
unregisterNetworkReceiver() unregisterNetworkReceiver()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
val repository = AdvancedPrivacySettingsRepository(requireContext()) val repository = AdvancedPrivacySettingsRepository(requireContext())
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val factory = AdvancedPrivacySettingsViewModel.Factory(preferences, repository) val factory = AdvancedPrivacySettingsViewModel.Factory(preferences, repository)

View File

@@ -10,7 +10,6 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
@@ -18,6 +17,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason
import org.thoughtcrime.securesms.groups.ui.GroupErrors import org.thoughtcrime.securesms.groups.ui.GroupErrors
import org.thoughtcrime.securesms.util.ExpirationUtil import org.thoughtcrime.securesms.util.ExpirationUtil
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.livedata.ProcessState import org.thoughtcrime.securesms.util.livedata.ProcessState
import org.thoughtcrime.securesms.util.livedata.distinctUntilChanged import org.thoughtcrime.securesms.util.livedata.distinctUntilChanged
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -48,7 +48,7 @@ class ExpireTimerSettingsFragment : DSLSettingsFragment(
recycler.clipToPadding = false recycler.clipToPadding = false
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
val provider = ViewModelProvider( val provider = ViewModelProvider(
NavHostFragment.findNavController(this).getViewModelStoreOwner(R.id.app_settings_expire_timer), NavHostFragment.findNavController(this).getViewModelStoreOwner(R.id.app_settings_expire_timer),
ExpireTimerSettingsViewModel.Factory(requireContext(), arguments.toConfig()) ExpireTimerSettingsViewModel.Factory(requireContext(), arguments.toConfig())

View File

@@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import org.signal.core.util.PendingIntentFlags
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.help.HelpFragment import org.thoughtcrime.securesms.help.HelpFragment
@@ -90,7 +91,7 @@ object DonationErrorNotifications {
context, context,
0, 0,
actionIntent, actionIntent,
if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_ONE_SHOT else 0 if (Build.VERSION.SDK_INT >= 23) PendingIntentFlags.oneShot() else PendingIntentFlags.mutable()
) )
} }
) )

View File

@@ -73,13 +73,13 @@ class DonationErrorParams<V> private constructor(
private fun <V> getVerificationErrorParams(context: Context, verificationError: DonationError.GiftRecipientVerificationError, callback: Callback<V>): DonationErrorParams<V> { private fun <V> getVerificationErrorParams(context: Context, verificationError: DonationError.GiftRecipientVerificationError, callback: Callback<V>): DonationErrorParams<V> {
return when (verificationError) { return when (verificationError) {
is DonationError.GiftRecipientVerificationError.FailedToFetchProfile -> DonationErrorParams( is DonationError.GiftRecipientVerificationError.FailedToFetchProfile -> DonationErrorParams(
title = R.string.DonationsErrors__could_not_verify_recipient, title = R.string.DonationsErrors__couldnt_send_gift,
message = R.string.DonationsErrors__please_check_your_network_connection, message = R.string.DonationsErrors__please_check_your_network_connection,
positiveAction = callback.onOk(context), positiveAction = callback.onOk(context),
negativeAction = null negativeAction = null
) )
else -> DonationErrorParams( else -> DonationErrorParams(
title = R.string.DonationsErrors__recipient_verification_failed, title = R.string.DonationsErrors__cant_send_gift,
message = R.string.DonationsErrors__target_does_not_support_gifting, message = R.string.DonationsErrors__target_does_not_support_gifting,
positiveAction = callback.onOk(context), positiveAction = callback.onOk(context),
negativeAction = null negativeAction = null

View File

@@ -12,7 +12,6 @@ import org.thoughtcrime.securesms.badges.gifts.ExpiredGiftSheet
import org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowActivity import org.thoughtcrime.securesms.badges.gifts.flow.GiftFlowActivity
import org.thoughtcrime.securesms.badges.models.BadgePreview import org.thoughtcrime.securesms.badges.models.BadgePreview
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
@@ -28,6 +27,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.subscription.Subscription import org.thoughtcrime.securesms.subscription.Subscription
import org.thoughtcrime.securesms.util.FeatureFlags import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import java.util.Currency import java.util.Currency
@@ -60,7 +60,7 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
viewModel.refresh() viewModel.refresh()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
ActiveSubscriptionPreference.register(adapter) ActiveSubscriptionPreference.register(adapter)
IndeterminateLoadingCircle.register(adapter) IndeterminateLoadingCircle.register(adapter)
BadgePreview.register(adapter) BadgePreview.register(adapter)

View File

@@ -17,7 +17,6 @@ import org.signal.core.util.concurrent.SimpleTask
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
@@ -26,6 +25,7 @@ import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.payments.FiatMoneyUtil import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.util.Locale import java.util.Locale
@@ -42,7 +42,7 @@ class DonationReceiptDetailFragment : DSLSettingsFragment(layoutId = R.layout.do
} }
) )
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
SplashImage.register(adapter) SplashImage.register(adapter)
val sharePngButton: MaterialButton = requireView().findViewById(R.id.share_png) val sharePngButton: MaterialButton = requireView().findViewById(R.id.share_png)

View File

@@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.BadgePreview import org.thoughtcrime.securesms.badges.models.BadgePreview
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
@@ -37,6 +36,7 @@ import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.subscription.Subscription import org.thoughtcrime.securesms.subscription.Subscription
import org.thoughtcrime.securesms.util.LifecycleDisposable import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.fragments.requireListener import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible import org.thoughtcrime.securesms.util.visible
@@ -82,7 +82,7 @@ class SubscribeFragment : DSLSettingsFragment(
viewModel.refreshActiveSubscription() viewModel.refreshActiveSubscription()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
donationPaymentComponent = requireListener() donationPaymentComponent = requireListener()
viewModel.refresh() viewModel.refresh()

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.conversation package org.thoughtcrime.securesms.components.settings.conversation
import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.PorterDuff import android.graphics.PorterDuff
@@ -39,7 +40,6 @@ import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
@@ -81,9 +81,9 @@ import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity
import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ContextUtil import org.thoughtcrime.securesms.util.ContextUtil
import org.thoughtcrime.securesms.util.ExpirationUtil import org.thoughtcrime.securesms.util.ExpirationUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.Material3OnScrollHelper import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.ViewUtil import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity import org.thoughtcrime.securesms.verify.VerifyIdentityActivity
@@ -201,7 +201,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
} }
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments()) val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
BioTextPreference.register(adapter) BioTextPreference.register(adapter)
@@ -230,7 +230,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
.withFixedSize(ViewUtil.dpToPx(80)) .withFixedSize(ViewUtil.dpToPx(80))
.load(state.recipient) .load(state.recipient)
if (FeatureFlags.displayDonorBadges() && !state.recipient.isSelf) { if (!state.recipient.isSelf) {
toolbarBadge.setBadgeFromRecipient(state.recipient) toolbarBadge.setBadgeFromRecipient(state.recipient)
} }
@@ -286,7 +286,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
requireContext(), requireContext(),
StoryViewerArgs( StoryViewerArgs(
recipientId = state.recipient.id, recipientId = state.recipient.id,
isInHiddenStoryMode = state.recipient.shouldHideStory() isInHiddenStoryMode = state.recipient.shouldHideStory(),
isFromQuote = true
) )
) )
StoryDialogs.displayStoryOrProfileImage( StoryDialogs.displayStoryOrProfileImage(
@@ -477,7 +478,11 @@ class ConversationSettingsFragment : DSLSettingsFragment(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_as_a_contact), title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_as_a_contact),
icon = DSLSettingsIcon.from(R.drawable.ic_plus_24), icon = DSLSettingsIcon.from(R.drawable.ic_plus_24),
onClick = { onClick = {
startActivityForResult(RecipientExporter.export(state.recipient).asAddContactIntent(), REQUEST_CODE_ADD_CONTACT) try {
startActivityForResult(RecipientExporter.export(state.recipient).asAddContactIntent(), REQUEST_CODE_ADD_CONTACT)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.ConversationSettingsFragment__contacts_app_not_found, Toast.LENGTH_SHORT).show()
}
} }
) )
} }

View File

@@ -12,7 +12,6 @@ import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.MainActivity import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
@@ -23,12 +22,14 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage
import org.thoughtcrime.securesms.subscription.Subscriber import org.thoughtcrime.securesms.subscription.Subscriber
import org.thoughtcrime.securesms.util.Base64 import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.Util import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.livedata.Store import org.thoughtcrime.securesms.util.livedata.Store
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.Objects import java.util.Objects
/** /**
@@ -45,7 +46,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
} }
) )
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
viewModel.state.observe(viewLifecycleOwner) { state -> viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList()) adapter.submitList(getConfiguration(state).toMappingModelList())
} }
@@ -62,27 +63,26 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
) )
if (!recipient.isGroup) { if (!recipient.isGroup) {
if (recipient.isSelf) { val e164: String = recipient.e164.orElse("null")
val aci: String = SignalStore.account().aci?.toString() ?: "null" longClickPref(
longClickPref( title = DSLSettingsText.from("E164"),
title = DSLSettingsText.from("ACI"), summary = DSLSettingsText.from(e164),
summary = DSLSettingsText.from(aci), onLongClick = { copyToClipboard(e164) }
onLongClick = { copyToClipboard(aci) } )
)
val pni: String = SignalStore.account().pni?.toString() ?: "null" val serviceId: String = recipient.serviceId.map { it.toString() }.orElse("null")
longClickPref( longClickPref(
title = DSLSettingsText.from("PNI"), title = DSLSettingsText.from("ServiceId"),
summary = DSLSettingsText.from(pni), summary = DSLSettingsText.from(serviceId),
onLongClick = { copyToClipboard(pni) } onLongClick = { copyToClipboard(serviceId) }
) )
} else {
val serviceId: String = recipient.serviceId.map(ServiceId::toString).orElse("null") val pni: String = recipient.pni.map { it.toString() }.orElse("null")
longClickPref( longClickPref(
title = DSLSettingsText.from("ServiceId"), title = DSLSettingsText.from("PNI"),
summary = DSLSettingsText.from(serviceId), summary = DSLSettingsText.from(pni),
onLongClick = { copyToClipboard(serviceId) } onLongClick = { copyToClipboard(pni) }
) )
}
} }
if (state.groupId != null) { if (state.groupId != null) {
@@ -215,6 +215,48 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
} }
) )
} }
sectionHeaderPref(DSLSettingsText.from("PNP"))
clickPref(
title = DSLSettingsText.from("Split contact"),
summary = DSLSettingsText.from("Splits this contact into two recipients and two threads so that you can test merging them together. This will remain the 'primary' recipient."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!recipient.hasE164()) {
Toast.makeText(context, "Recipient doesn't have an E164! Can't split.", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
SignalDatabase.recipients.debugClearE164AndPni(recipient.id)
val splitRecipientId: RecipientId = if (FeatureFlags.phoneNumberPrivacy()) {
SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.pni.orElse(null), recipient.pni.orElse(null), recipient.requireE164())
} else {
SignalDatabase.recipients.getAndPossiblyMerge(recipient.pni.orElse(null), recipient.requireE164())
}
val splitRecipient: Recipient = Recipient.resolved(splitRecipientId)
val splitThreadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(splitRecipient)
val messageId: Long = SignalDatabase.sms.insertMessageOutbox(
splitThreadId,
OutgoingEncryptedMessage(splitRecipient, "Test Message ${System.currentTimeMillis()}", 0),
false,
System.currentTimeMillis(),
null
)
SignalDatabase.sms.markAsSent(messageId, true)
SignalDatabase.threads.update(splitThreadId, true)
Toast.makeText(context, "Done! We split the E164/PNI from this contact into $splitRecipientId", Toast.LENGTH_SHORT).show()
}
.show()
}
)
} }
} }

View File

@@ -5,12 +5,12 @@ import androidx.annotation.StringRes
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.groups.ParcelableGroupId import org.thoughtcrime.securesms.groups.ParcelableGroupId
import org.thoughtcrime.securesms.groups.ui.GroupErrors import org.thoughtcrime.securesms.groups.ui.GroupErrors
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
class PermissionsSettingsFragment : DSLSettingsFragment( class PermissionsSettingsFragment : DSLSettingsFragment(
titleId = R.string.ConversationSettingsFragment__permissions titleId = R.string.ConversationSettingsFragment__permissions
@@ -30,7 +30,7 @@ class PermissionsSettingsFragment : DSLSettingsFragment(
} }
) )
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
viewModel.state.observe(viewLifecycleOwner) { state -> viewModel.state.observe(viewLifecycleOwner) { state ->
adapter.submitList(getConfiguration(state).toMappingModelList()) adapter.submitList(getConfiguration(state).toMappingModelList())
} }

View File

@@ -6,7 +6,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.thoughtcrime.securesms.MuteDialog import org.thoughtcrime.securesms.MuteDialog
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
@@ -14,6 +13,7 @@ import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.navigation.safeNavigate
class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment( class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
@@ -38,7 +38,7 @@ class SoundsAndNotificationsSettingsFragment : DSLSettingsFragment(
viewModel.channelConsistencyCheck() viewModel.channelConsistencyCheck()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
viewModel.state.observe(viewLifecycleOwner) { state -> viewModel.state.observe(viewLifecycleOwner) { state ->
if (state.channelConsistencyCheckComplete && state.recipientId != Recipient.UNKNOWN.id) { if (state.channelConsistencyCheckComplete && state.recipientId != Recipient.UNKNOWN.id) {
adapter.submitList(getConfiguration(state).toMappingModelList()) adapter.submitList(getConfiguration(state).toMappingModelList())

View File

@@ -14,7 +14,6 @@ import androidx.fragment.app.viewModels
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration 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.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.configure
@@ -22,6 +21,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.util.ConversationUtil import org.thoughtcrime.securesms.util.ConversationUtil
import org.thoughtcrime.securesms.util.RingtoneUtil import org.thoughtcrime.securesms.util.RingtoneUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
private val TAG = Log.tag(CustomNotificationsSettingsFragment::class.java) private val TAG = Log.tag(CustomNotificationsSettingsFragment::class.java)
@@ -48,7 +48,7 @@ class CustomNotificationsSettingsFragment : DSLSettingsFragment(R.string.CustomN
viewModel.channelConsistencyCheck() viewModel.channelConsistencyCheck()
} }
override fun bindAdapter(adapter: DSLSettingsAdapter) { override fun bindAdapter(adapter: MappingAdapter) {
messageSoundResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> messageSoundResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
handleResult(result, viewModel::setMessageSound) handleResult(result, viewModel::setMessageSound)
} }

View File

@@ -52,7 +52,7 @@ public class AttachmentRegionDecoder implements ImageRegionDecoder {
synchronized(this) { synchronized(this) {
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize; options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565; options.inPreferredConfig = Bitmap.Config.ARGB_8888;
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(rect, options); Bitmap bitmap = bitmapRegionDecoder.decodeRegion(rect, options);

View File

@@ -175,8 +175,8 @@ class VoiceNoteMediaItemFactory {
sender.getDisplayName(context), sender.getDisplayName(context),
threadRecipient.getDisplayName(context)); threadRecipient.getDisplayName(context));
} else if (preference.isDisplayContact()) { } else if (preference.isDisplayContact()) {
return sender.isSelf() ? context.getString(R.string.note_to_self) return sender.isSelf() && threadRecipient.isSelf() ? context.getString(R.string.note_to_self)
: sender.getDisplayName(context); : sender.getDisplayName(context);
} else { } else {
return context.getString(R.string.MessageNotifier_signal_message); return context.getString(R.string.MessageNotifier_signal_message);
} }

View File

@@ -13,6 +13,7 @@ import androidx.annotation.Nullable;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.ui.PlayerNotificationManager; import com.google.android.exoplayer2.ui.PlayerNotificationManager;
import org.signal.core.util.PendingIntentFlags;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.conversation.ConversationIntents;
@@ -107,7 +108,7 @@ class VoiceNoteNotificationManager {
return PendingIntent.getActivity(context, return PendingIntent.getActivity(context,
0, 0,
conversationActivity, conversationActivity,
PendingIntent.FLAG_CANCEL_CURRENT); PendingIntentFlags.cancelCurrent());
} }
@Override @Override

View File

@@ -4,7 +4,6 @@ import android.media.AudioManager
import android.os.Bundle import android.os.Bundle
import android.os.ResultReceiver import android.os.ResultReceiver
import com.google.android.exoplayer2.C import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.ControlDispatcher
import com.google.android.exoplayer2.PlaybackParameters import com.google.android.exoplayer2.PlaybackParameters
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.SimpleExoPlayer
@@ -22,8 +21,7 @@ class VoiceNotePlaybackController(
private val TAG = Log.tag(VoiceNoteMediaController::class.java) private val TAG = Log.tag(VoiceNoteMediaController::class.java)
} }
@Suppress("deprecation") override fun onCommand(p: Player, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
override fun onCommand(p: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?): Boolean {
Log.d(TAG, "[onCommand] Received player command $command (extras? ${extras != null})") Log.d(TAG, "[onCommand] Received player command $command (extras? ${extras != null})")
if (command == VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED) { if (command == VoiceNotePlaybackService.ACTION_NEXT_PLAYBACK_SPEED) {
@@ -37,13 +35,13 @@ class VoiceNotePlaybackController(
val currentStreamType = Util.getStreamTypeForAudioUsage(player.audioAttributes.usage) val currentStreamType = Util.getStreamTypeForAudioUsage(player.audioAttributes.usage)
if (newStreamType != currentStreamType) { if (newStreamType != currentStreamType) {
val attributes = when (newStreamType) { val attributes = when (newStreamType) {
AudioManager.STREAM_MUSIC -> AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build() AudioManager.STREAM_MUSIC -> AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build()
AudioManager.STREAM_VOICE_CALL -> AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_SPEECH).setUsage(C.USAGE_VOICE_COMMUNICATION).build() AudioManager.STREAM_VOICE_CALL -> AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_SPEECH).setUsage(C.USAGE_VOICE_COMMUNICATION).build()
else -> throw AssertionError() else -> throw AssertionError()
} }
player.playWhenReady = false player.playWhenReady = false
player.setAudioAttributes(attributes, false) player.setAudioAttributes(attributes, newStreamType == AudioManager.STREAM_MUSIC)
if (newStreamType == AudioManager.STREAM_VOICE_CALL) { if (newStreamType == AudioManager.STREAM_VOICE_CALL) {
player.playWhenReady = true player.playWhenReady = true

View File

@@ -13,7 +13,6 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.google.android.exoplayer2.ControlDispatcher;
import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.MediaItem;
import com.google.android.exoplayer2.MediaMetadata; import com.google.android.exoplayer2.MediaMetadata;
import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player;
@@ -40,7 +39,7 @@ import java.util.stream.Collectors;
/** /**
* ExoPlayer Preparer for Voice Notes. This only supports ACTION_PLAY_FROM_URI * ExoPlayer Preparer for Voice Notes. This only supports ACTION_PLAY_FROM_URI
*/ */
final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackPreparer { final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackPreparer {
private static final String TAG = Log.tag(VoiceNotePlaybackPreparer.class); private static final String TAG = Log.tag(VoiceNotePlaybackPreparer.class);
private static final Executor EXECUTOR = Executors.newSingleThreadExecutor(); private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
@@ -291,7 +290,6 @@ final class VoiceNotePlaybackPreparer implements MediaSessionConnector.PlaybackP
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Override @Override
public boolean onCommand(@NonNull Player player, public boolean onCommand(@NonNull Player player,
@NonNull ControlDispatcher controlDispatcher,
@NonNull String command, @NonNull String command,
@Nullable Bundle extras, @Nullable Bundle extras,
@Nullable ResultReceiver cb) @Nullable ResultReceiver cb)

View File

@@ -5,6 +5,7 @@ import com.google.android.exoplayer2.C
import com.google.android.exoplayer2.DefaultLoadControl import com.google.android.exoplayer2.DefaultLoadControl
import com.google.android.exoplayer2.ForwardingPlayer import com.google.android.exoplayer2.ForwardingPlayer
import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.SimpleExoPlayer
import com.google.android.exoplayer2.audio.AudioAttributes
import org.thoughtcrime.securesms.video.exo.SignalMediaSourceFactory import org.thoughtcrime.securesms.video.exo.SignalMediaSourceFactory
class VoiceNotePlayer @JvmOverloads constructor( class VoiceNotePlayer @JvmOverloads constructor(
@@ -15,7 +16,9 @@ class VoiceNotePlayer @JvmOverloads constructor(
DefaultLoadControl.Builder() DefaultLoadControl.Builder()
.setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE) .setBufferDurationsMs(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE)
.build() .build()
).build() ).build().apply {
setAudioAttributes(AudioAttributes.Builder().setContentType(C.AUDIO_CONTENT_TYPE_MUSIC).setUsage(C.USAGE_MEDIA).build(), true)
}
) : ForwardingPlayer(internalPlayer) { ) : ForwardingPlayer(internalPlayer) {
override fun seekTo(windowIndex: Int, positionMs: Long) { override fun seekTo(windowIndex: Int, positionMs: Long) {

View File

@@ -149,8 +149,8 @@ public class CallParticipantView extends ConstraintLayout {
boolean hasContentToRender = (participant.isVideoEnabled() || participant.isScreenSharing()) && participant.isForwardingVideo(); boolean hasContentToRender = (participant.isVideoEnabled() || participant.isScreenSharing()) && participant.isForwardingVideo();
rendererFrame.setVisibility(hasContentToRender ? View.VISIBLE : View.GONE); rendererFrame.setVisibility(hasContentToRender ? View.VISIBLE : View.INVISIBLE);
renderer.setVisibility(hasContentToRender ? View.VISIBLE : View.GONE); renderer.setVisibility(hasContentToRender ? View.VISIBLE : View.INVISIBLE);
if (participant.isVideoEnabled()) { if (participant.isVideoEnabled()) {
participant.getVideoSink().getLockableEglBase().performWithValidEglBase(eglBase -> { participant.getVideoSink().getLockableEglBase().performWithValidEglBase(eglBase -> {

View File

@@ -9,6 +9,7 @@ import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import org.signal.core.util.PendingIntentFlags;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.WebRtcCallActivity; import org.thoughtcrime.securesms.WebRtcCallActivity;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
@@ -28,7 +29,7 @@ public final class GroupCallSafetyNumberChangeNotificationUtil {
Intent contentIntent = new Intent(context, WebRtcCallActivity.class); Intent contentIntent = new Intent(context, WebRtcCallActivity.class);
contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); contentIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, 0); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, contentIntent, PendingIntentFlags.mutable());
Notification safetyNumberChangeNotification = new NotificationCompat.Builder(context, NotificationChannels.CALLS) Notification safetyNumberChangeNotification = new NotificationCompat.Builder(context, NotificationChannels.CALLS)
.setSmallIcon(R.drawable.ic_notification) .setSmallIcon(R.drawable.ic_notification)

View File

@@ -11,7 +11,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@@ -72,7 +72,7 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment {
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState); super.onActivityCreated(savedInstanceState);
final WebRtcCallViewModel viewModel = ViewModelProviders.of(requireActivity()).get(WebRtcCallViewModel.class); final WebRtcCallViewModel viewModel = new ViewModelProvider(requireActivity()).get(WebRtcCallViewModel.class);
initializeList(); initializeList();

View File

@@ -130,6 +130,14 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
itemView.setOnClickListener(v -> { itemView.setOnClickListener(v -> {
if (clickListener != null) clickListener.onItemClick(getView()); if (clickListener != null) clickListener.onItemClick(getView());
}); });
itemView.setOnLongClickListener(v -> {
if (clickListener != null) {
return clickListener.onItemLongClick(getView());
} else {
return false;
}
});
} }
public ContactSelectionListItem getView() { public ContactSelectionListItem getView() {
@@ -300,6 +308,11 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
private @Nullable String getHeaderLetterForDisplayName(@NonNull Cursor cursor) { private @Nullable String getHeaderLetterForDisplayName(@NonNull Cursor cursor) {
String name = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN); String name = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN);
if (name == null) {
return null;
}
Iterator<String> characterIterator = new CharacterIterable(name).iterator(); Iterator<String> characterIterator = new CharacterIterable(name).iterator();
if (!TextUtils.isEmpty(name) && characterIterator.hasNext()) { if (!TextUtils.isEmpty(name) && characterIterator.hasNext()) {
@@ -430,5 +443,6 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
public interface ItemClickListener { public interface ItemClickListener {
void onItemClick(ContactSelectionListItem item); void onItemClick(ContactSelectionListItem item);
boolean onItemLongClick(ContactSelectionListItem item);
} }
} }

View File

@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms.contacts;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.widget.CheckBox; import android.widget.CheckBox;
@@ -10,18 +12,24 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.components.AvatarImageView; import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.FromTextView; import org.thoughtcrime.securesms.components.FromTextView;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter; import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.profiles.manage.UsernameState;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
@@ -51,6 +59,8 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
private LiveRecipient recipient; private LiveRecipient recipient;
private GlideRequests glideRequests; private GlideRequests glideRequests;
private final UsernameFallbackPhotoProvider usernameFallbackPhotoProvider = new UsernameFallbackPhotoProvider();
public ContactSelectionListItem(Context context) { public ContactSelectionListItem(Context context) {
super(context); super(context);
} }
@@ -104,8 +114,11 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
this.contactLabel = label; this.contactLabel = label;
this.contactAbout = about; this.contactAbout = about;
this.contactPhotoImage.setFallbackPhotoProvider(null);
if (type == ContactRepository.NEW_PHONE_TYPE || type == ContactRepository.NEW_USERNAME_TYPE) { if (type == ContactRepository.NEW_PHONE_TYPE || type == ContactRepository.NEW_USERNAME_TYPE) {
this.recipient = null; this.recipient = null;
this.contactPhotoImage.setFallbackPhotoProvider(usernameFallbackPhotoProvider);
this.contactPhotoImage.setFallbackPhotoColor(AvatarColor.ON_SURFACE_VARIANT);
this.contactPhotoImage.setAvatar(glideRequests, null, false); this.contactPhotoImage.setAvatar(glideRequests, null, false);
} else if (recipientId != null) { } else if (recipientId != null) {
if (this.recipient != null) { if (this.recipient != null) {
@@ -168,6 +181,8 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private void setText(@Nullable Recipient recipient, int type, String name, String number, String label, @Nullable String about) { private void setText(@Nullable Recipient recipient, int type, String name, String number, String label, @Nullable String about) {
this.numberView.setVisibility(View.VISIBLE);
if (number == null || number.isEmpty()) { if (number == null || number.isEmpty()) {
this.nameView.setEnabled(false); this.nameView.setEnabled(false);
this.numberView.setText(""); this.numberView.setText("");
@@ -181,10 +196,9 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
this.nameView.setEnabled(true); this.nameView.setEnabled(true);
this.labelView.setVisibility(View.GONE); this.labelView.setVisibility(View.GONE);
} else if (type == ContactRepository.NEW_USERNAME_TYPE) { } else if (type == ContactRepository.NEW_USERNAME_TYPE) {
this.numberView.setText("@" + number); this.numberView.setVisibility(View.GONE);
this.nameView.setEnabled(true); this.nameView.setEnabled(true);
this.labelView.setText(label); this.labelView.setVisibility(View.GONE);
this.labelView.setVisibility(View.VISIBLE);
} else if (recipient != null && recipient.isDistributionList()) { } else if (recipient != null && recipient.isDistributionList()) {
this.numberView.setText(getViewerCount(number)); this.numberView.setText(getViewerCount(number));
this.labelView.setVisibility(View.GONE); this.labelView.setVisibility(View.GONE);
@@ -198,6 +212,8 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
if (recipient != null) { if (recipient != null) {
this.nameView.setText(recipient); this.nameView.setText(recipient);
chipName = recipient.getShortDisplayName(getContext()); chipName = recipient.getShortDisplayName(getContext());
} else if (type == ContactRepository.NEW_USERNAME_TYPE && number != null) {
this.nameView.setText(presentUsername(number));
} else { } else {
this.nameView.setText(name); this.nameView.setText(name);
chipName = name; chipName = name;
@@ -224,6 +240,14 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
int viewerCount = Integer.parseInt(number); int viewerCount = Integer.parseInt(number);
return getContext().getResources().getQuantityString(R.plurals.contact_selection_list_item__number_of_viewers, viewerCount, viewerCount); return getContext().getResources().getQuantityString(R.plurals.contact_selection_list_item__number_of_viewers, viewerCount, viewerCount);
} }
private CharSequence presentUsername(@NonNull String username) {
if (username.contains(UsernameState.DELIMITER)) {
return username;
} else {
return new SpannableStringBuilder(username).append(SpanUtil.color(ContextCompat.getColor(getContext(), R.color.signal_colorOutline), UsernameState.DELIMITER));
}
}
public @Nullable LiveRecipient getRecipient() { public @Nullable LiveRecipient getRecipient() {
return recipient; return recipient;
@@ -264,4 +288,11 @@ public class ContactSelectionListItem extends ConstraintLayout implements Recipi
Log.w(TAG, "Bad change! Local recipient doesn't match. Ignoring. Local: " + (this.recipient == null ? "null" : this.recipient.getId()) + ", Changed: " + recipient.getId()); Log.w(TAG, "Bad change! Local recipient doesn't match. Ignoring. Local: " + (this.recipient == null ? "null" : this.recipient.getId()) + ", Changed: " + recipient.getId());
} }
} }
private static class UsernameFallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
@Override
public @NonNull FallbackContactPhoto getPhotoForRecipientWithoutName() {
return new ResourceContactPhoto(R.drawable.ic_search_24, R.drawable.ic_search_24, R.drawable.ic_search_24);
}
}
} }

View File

@@ -22,18 +22,26 @@ import android.database.MatrixCursor;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.phonenumbers.NumberUtil; import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.UsernameUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
/** /**
* CursorLoader that initializes a ContactsDatabase instance * CursorLoader that initializes a ContactsDatabase instance
@@ -213,13 +221,36 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
} }
private Cursor getGroupsCursor() { private Cursor getGroupsCursor() {
MatrixCursor groupContacts = ContactsCursorRows.createMatrixCursor(); MatrixCursor groupContacts = ContactsCursorRows.createMatrixCursor();
Map<RecipientId, GroupDatabase.GroupRecord> groups = new LinkedHashMap<>();
try (GroupDatabase.Reader reader = SignalDatabase.groups().queryGroupsByTitle(getFilter(), flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode), !smsEnabled(mode))) { try (GroupDatabase.Reader reader = SignalDatabase.groups().queryGroupsByTitle(getFilter(), flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode), !smsEnabled(mode))) {
GroupDatabase.GroupRecord groupRecord; GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) { while ((groupRecord = reader.getNext()) != null) {
groupContacts.addRow(ContactsCursorRows.forGroup(groupRecord)); groups.put(groupRecord.getRecipientId(), groupRecord);
} }
} }
if (getFilter() != null && !Util.isEmpty(getFilter())) {
Set<RecipientId> filteredContacts = new HashSet<>();
try (Cursor cursor = SignalDatabase.recipients().queryAllContacts(getFilter())) {
while (cursor != null && cursor.moveToNext()) {
filteredContacts.add(RecipientId.from(CursorUtil.requireString(cursor, RecipientDatabase.ID)));
}
}
try (GroupDatabase.Reader reader = SignalDatabase.groups().queryGroupsByMembership(filteredContacts, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), hideGroupsV1(mode), !smsEnabled(mode))) {
GroupDatabase.GroupRecord groupRecord;
while ((groupRecord = reader.getNext()) != null) {
groups.put(groupRecord.getRecipientId(), groupRecord);
}
}
}
for (GroupDatabase.GroupRecord groupRecord : groups.values()) {
groupContacts.addRow(ContactsCursorRows.forGroup(groupRecord));
}
return groupContacts; return groupContacts;
} }
@@ -228,7 +259,7 @@ public class ContactsCursorLoader extends AbstractContactsCursorLoader {
} }
private Cursor getUsernameSearchCursor() { private Cursor getUsernameSearchCursor() {
return ContactsCursorRows.forUsernameSearch(getUnknownContactTitle(), getFilter()); return ContactsCursorRows.forUsernameSearch(getFilter());
} }
private String getUnknownContactTitle() { private String getUnknownContactTitle() {

View File

@@ -104,11 +104,11 @@ public final class ContactsCursorRows {
/** /**
* Create a row for a contacts cursor for a username the user is entering or has entered. * Create a row for a contacts cursor for a username the user is entering or has entered.
*/ */
public static @NonNull MatrixCursor forUsernameSearch(@NonNull String unknownContactTitle, @NonNull String filter) { public static @NonNull MatrixCursor forUsernameSearch(@NonNull String filter) {
MatrixCursor matrixCursor = createMatrixCursor(1); MatrixCursor matrixCursor = createMatrixCursor(1);
matrixCursor.addRow(new Object[]{null, matrixCursor.addRow(new Object[]{null,
unknownContactTitle, null,
filter, filter,
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"\u21e2", "\u21e2",
@@ -119,7 +119,7 @@ public final class ContactsCursorRows {
} }
public static @NonNull MatrixCursor forUsernameSearchHeader(@NonNull Context context) { public static @NonNull MatrixCursor forUsernameSearchHeader(@NonNull Context context) {
return forHeader(context.getString(R.string.ContactsCursorLoader_username_search)); return forHeader(context.getString(R.string.ContactsCursorLoader_find_by_username));
} }
public static @NonNull MatrixCursor forPhoneNumberSearchHeader(@NonNull Context context) { public static @NonNull MatrixCursor forPhoneNumberSearchHeader(@NonNull Context context) {

View File

@@ -16,8 +16,8 @@ import com.makeramen.roundedimageview.RoundedDrawable;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.avatar.Avatars;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair;
import java.util.Objects; import java.util.Objects;
@@ -63,14 +63,18 @@ public class ResourceContactPhoto implements FallbackContactPhoto {
} }
private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, @NonNull AvatarColor color, boolean inverted) { private @NonNull Drawable buildDrawable(@NonNull Context context, int resourceId, @NonNull AvatarColor color, boolean inverted) {
Avatars.ForegroundColor foregroundColor = Avatars.getForegroundColor(color); AvatarColorPair avatarColorPair = AvatarColorPair.create(context, color);
Drawable background = Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.circle_tintable));
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId)); final int backgroundColor = avatarColorPair.getBackgroundColor();
final int foregroundColor = avatarColorPair.getForegroundColor();
Drawable background = Objects.requireNonNull(ContextCompat.getDrawable(context, R.drawable.circle_tintable));
RoundedDrawable foreground = (RoundedDrawable) RoundedDrawable.fromDrawable(AppCompatResources.getDrawable(context, resourceId));
//noinspection ConstantConditions //noinspection ConstantConditions
foreground.setScaleType(scaleType); foreground.setScaleType(scaleType);
background.setColorFilter(inverted ? foregroundColor.getColorInt() : color.colorInt(), PorterDuff.Mode.SRC_IN); background.setColorFilter(inverted ? foregroundColor : backgroundColor, PorterDuff.Mode.SRC_IN);
foreground.setColorFilter(inverted ? color.colorInt() : foregroundColor.getColorInt(), PorterDuff.Mode.SRC_ATOP); foreground.setColorFilter(inverted ? backgroundColor : foregroundColor, PorterDuff.Mode.SRC_ATOP);
return new ExpandingLayerDrawable(new Drawable[] {background, foreground}); return new ExpandingLayerDrawable(new Drawable[] {background, foreground});
} }

View File

@@ -0,0 +1,37 @@
package org.thoughtcrime.securesms.contacts.management
import android.content.Context
import androidx.annotation.CheckResult
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientUtil
class ContactsManagementRepository(context: Context) {
private val context = context.applicationContext
@CheckResult
fun blockContact(recipient: Recipient): Completable {
return Completable.fromAction {
if (recipient.isDistributionList) {
error("Blocking a distribution list makes no sense")
} else if (recipient.isGroup) {
RecipientUtil.block(context, recipient)
} else {
RecipientUtil.blockNonGroup(context, recipient)
}
}.subscribeOn(Schedulers.io())
}
@CheckResult
fun hideContact(recipient: Recipient): Completable {
return Completable.fromAction {
if (recipient.isGroup || recipient.isDistributionList || recipient.isSelf) {
error("Cannot hide groups, self, or distribution lists.")
}
SignalDatabase.recipients.markHidden(recipient.id)
}
}
}

View File

@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms.contacts.management
import androidx.annotation.CheckResult
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import org.thoughtcrime.securesms.recipients.Recipient
class ContactsManagementViewModel(private val repository: ContactsManagementRepository) : ViewModel() {
@CheckResult
fun hideContact(recipient: Recipient): Completable {
return repository.hideContact(recipient).observeOn(AndroidSchedulers.mainThread())
}
@CheckResult
fun blockContact(recipient: Recipient): Completable {
return repository.blockContact(recipient).observeOn(AndroidSchedulers.mainThread())
}
class Factory(private val repository: ContactsManagementRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(ContactsManagementViewModel(repository)) as T
}
}
}

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