Compare commits

..

341 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
Cody Henthorne
6262f775d5 Bump version to 5.46.4 2022-08-15 16:17:23 -04:00
Cody Henthorne
18ae665cd1 Updated language translations. 2022-08-15 15:46:48 -04:00
Greyson Parrelli
029a76f8a2 Add additional verifications and logging around My Story db entry. 2022-08-15 13:34:35 -04:00
Greyson Parrelli
e0e3f7dfec Ensure main database is updated before opening secondary ones. 2022-08-15 11:56:17 -04:00
Greyson Parrelli
2220ceb9d9 Attempt to self-repair some types of profile issues. 2022-08-15 10:39:03 -04:00
Cody Henthorne
a28698da36 Bump version to 5.46.3 2022-08-12 15:25:47 -04:00
Cody Henthorne
9307835d2d Updated language translations. 2022-08-12 15:22:35 -04:00
Cody Henthorne
cfe167b639 Fix crash when submitting debuglog via registration flow. 2022-08-12 15:18:10 -04:00
Greyson Parrelli
64d3b36b28 Attempt repair if you have an invalid credential during invite accept. 2022-08-12 12:27:30 -04:00
Cody Henthorne
b433a7b816 Add control for inserting boost message in release notes channel. 2022-08-12 12:27:01 -04:00
Cody Henthorne
17643bf13b Add gift badge call to action for release notes. 2022-08-12 11:46:40 -04:00
Cody Henthorne
a444a96dc9 Fix drafts not loading in bubbled conversations. 2022-08-12 11:39:02 -04:00
Cody Henthorne
41a7560e76 Do not fail backup when missing attachments. 2022-08-12 11:21:21 -04:00
Cody Henthorne
90be2a0e53 Bump version to 5.46.2 2022-08-11 17:03:37 -04:00
Cody Henthorne
6c7bb85aa3 Updated language translations. 2022-08-11 16:57:02 -04:00
Alex Voloshyn
fe898d824b Upgrade payments to use 2.0.0 enclaves.
* Updated MrEnclave values for 2.0.0 support
* Updated MrEnclave values for TestNet
2022-08-11 16:52:30 -04:00
Cody Henthorne
ac8a972c6e Do not fail backup creation when sticker files are missing. 2022-08-11 11:25:36 -04:00
Cody Henthorne
1e691005c7 Fix QR scanning issues, again. 2022-08-11 11:06:08 -04:00
Cody Henthorne
0b0e25b121 Bump version to 5.46.1 2022-08-10 16:58:23 -04:00
Cody Henthorne
afff792ecc Fix drafts not working when typing indicators are disabled. 2022-08-10 16:45:43 -04:00
Alex Hart
7f47e50674 Bump version to 5.46.0 2022-08-10 16:15:24 -03:00
Alex Hart
bca886cfb9 Updated language translations. 2022-08-10 16:14:45 -03:00
Greyson Parrelli
55216f5583 Consider ContactRecords with the local user's PNI to be invalid. 2022-08-10 16:14:45 -03:00
Greyson Parrelli
f8220ca554 Add logs to detect negative changes in system clock. 2022-08-10 16:14:45 -03:00
Alex Hart
3cb674f095 Correct send button colors. 2022-08-10 16:14:45 -03:00
Alex Hart
5977016075 Add toolbar to media selection contact select. 2022-08-10 16:14:45 -03:00
Cody Henthorne
eefd7bd37a Fix QR scanning issues. 2022-08-10 16:14:45 -03:00
Cody Henthorne
019025ab8a Improve backup creation exception messaging to user. 2022-08-10 16:14:45 -03:00
Cody Henthorne
36f1183d6c Update libsignal-client for CDSv2. 2022-08-10 11:06:21 -04:00
Alex Hart
caf87def13 Rotate gift badge sending flag. 2022-08-10 10:00:19 -03:00
Alex Hart
729a9c0864 Log out share timestamp. 2022-08-10 09:50:32 -03:00
Greyson Parrelli
f004b72ba2 Use the PNP merging function for everything. 2022-08-09 18:36:04 -04:00
Cody Henthorne
e83a4692c5 Change calling rotation behavior for 1:1 calls. 2022-08-09 16:23:45 -04:00
Alex Hart
acf811c79a Introduce android view-bindings. 2022-08-09 16:23:45 -04:00
Greyson Parrelli
733b4ff805 Give story sends an IMPLICIT content hint. 2022-08-09 16:23:44 -04:00
Cody Henthorne
756b926f6f Color nav bar to match unmute in release note channel. 2022-08-09 16:23:44 -04:00
Alex Hart
5164a44ee8 Fix alignment of small arabic names in LTR languages. 2022-08-09 16:23:44 -04:00
Cody Henthorne
cfebd0eeb9 Verify backup can be decrypted as part of creation flow. 2022-08-09 16:23:44 -04:00
Alex Hart
5212b33b47 Add sms export library and sample app. 2022-08-09 16:23:44 -04:00
Greyson Parrelli
6120f90dcb Update CDS enclave. 2022-08-09 16:23:44 -04:00
Cody Henthorne
0a76eb81e6 Add save-as-you-compose drafts. 2022-08-09 16:23:44 -04:00
Alex Hart
192509f762 Fix action bar layout insets. 2022-08-09 16:23:44 -04:00
Brandon
de09571077 Fix reproducible builds README apk path.
Closes #12349
2022-08-09 16:23:44 -04:00
Sgn-32
8f5c326758 Remove seconds from screen lock timeout summary.
Closes #11241
2022-08-09 16:23:44 -04:00
Greyson Parrelli
91d3f331e5 Make CameraX blocklist remote configurable. 2022-08-09 16:23:44 -04:00
Cody Henthorne
ace4157a14 Make maps key externally configurable. 2022-08-09 16:23:44 -04:00
Cody Henthorne
83b97d274f Add support for PNI registration ids and PNP change number. 2022-08-09 16:23:44 -04:00
Greyson Parrelli
0d3ea22641 Fix accuracy of ConversationItemTest_linkifyUrlLinks. 2022-08-09 16:23:44 -04:00
Alex Hart
eb634d62ce Bump version to 5.45.6 2022-08-09 16:24:50 -03:00
Alex Hart
6353e7b1be Updated language translations. 2022-08-09 16:23:54 -03:00
Cody Henthorne
286c340f01 Do not start service is non-urgent push. 2022-08-09 15:11:10 -04:00
Greyson Parrelli
055b79c9f2 Prevent setting a null profile key during account restore. 2022-08-09 11:16:00 -04:00
Alex Hart
29a9297452 Bump version to 5.45.5 2022-08-08 13:40:23 -03:00
Alex Hart
929200d53d Updated language translations. 2022-08-08 13:40:22 -03:00
Greyson Parrelli
6e9b1551e7 Fix duplicate emoji results. 2022-08-08 13:40:22 -03:00
Cody Henthorne
1547ec2067 Fix illegal state exception during backup restore of unamed groups. 2022-08-08 13:40:22 -03:00
Alex Hart
f7dce21246 Rotate gift badge sending flag. 2022-08-08 13:40:22 -03:00
Greyson Parrelli
3d0634de8d Bump version to 5.45.4 2022-08-05 18:03:19 -04:00
Greyson Parrelli
64396c1de6 Updated language translations. 2022-08-05 18:03:19 -04:00
Greyson Parrelli
9f5b822e33 Do not show :query completion for possible time entries. 2022-08-05 18:03:19 -04:00
Greyson Parrelli
eac9f78dfa Fix issues around all-zero UUIDs. 2022-08-05 18:03:19 -04:00
Greyson Parrelli
b9879e7210 Bump version to 5.45.3 2022-08-05 13:59:54 -04:00
Greyson Parrelli
ea76ce9b87 Updated language translations. 2022-08-05 13:59:54 -04:00
Greyson Parrelli
4b6ff55779 Fix crash around unknown storage enums. 2022-08-05 13:59:54 -04:00
Greyson Parrelli
718eedcb34 Allow saving debuglogs to disk. 2022-08-05 13:16:41 -04:00
Alex Hart
999314255c Add further distribution sync logging. 2022-08-05 14:14:46 -03:00
Greyson Parrelli
886c4b64f2 Bump version to 5.45.2 2022-08-04 17:12:30 -04:00
Greyson Parrelli
887221fccf Updated language translations. 2022-08-04 17:12:30 -04:00
Cody Henthorne
d4c633a0f2 Include Signal release notes channel in backups. 2022-08-04 17:12:30 -04:00
Cody Henthorne
0c7a8a63b5 Use Mat3 menu and dialog in Media Preview toolbar/save. 2022-08-04 17:12:30 -04:00
Alex Hart
1b053a2613 Add explicit exceptions and group_type correction. 2022-08-04 17:12:30 -04:00
Cody Henthorne
539cd4059d Fix inline emoji search for media first flow. 2022-08-04 17:12:30 -04:00
Alex Hart
c21b0cd145 Fix camera initialization error for disabled hardware. 2022-08-04 17:12:30 -04:00
Alex Hart
0a2696113c Allow long form messages if stories aren't enabled.
Fixes #12369
2022-08-04 17:12:24 -04:00
Alex Hart
710bb386e2 Fail getRecipientIdForSyncRecord immediately if identifier is invalid. 2022-08-04 17:10:41 -04:00
Greyson Parrelli
2495781055 Bump version to 5.45.1 2022-08-03 17:34:06 -04:00
Greyson Parrelli
f9b29cd044 Updated language translations. 2022-08-03 17:33:03 -04:00
Alex Hart
a0cc2ff90a Add new my story migration. 2022-08-03 17:17:35 -04:00
Cody Henthorne
b002235ef7 Keep web socket open during calling to improve message delivery. 2022-08-03 17:17:35 -04:00
Greyson Parrelli
120dda6e68 Schedule a migration to fetch the latest search index. 2022-08-03 17:17:35 -04:00
Greyson Parrelli
907abf72d3 Improve emoji search results. 2022-08-03 17:17:35 -04:00
Cody Henthorne
18eac51576 Migrate all QR scanning to new scanner. 2022-08-03 17:17:35 -04:00
Alex Hart
caf1329005 Lock CameraX fragment to portrait. 2022-08-03 17:17:35 -04:00
Alex Hart
5f7b07147f Add proper media review send tint. 2022-08-03 17:17:35 -04:00
Cody Henthorne
d7d923c820 Tweak emoji suggestions UX. 2022-08-03 17:17:35 -04:00
1519 changed files with 373428 additions and 293817 deletions

1
.gitignore vendored
View File

@@ -27,3 +27,4 @@ obj/
jni/libspeex/.deps/
pkcs11.password
dev.keystore
maps.key

View File

@@ -57,8 +57,8 @@ ktlint {
version = "0.43.2"
}
def canonicalVersionCode = 1098
def canonicalVersionName = "5.45.0"
def canonicalVersionCode = 1130
def canonicalVersionName = "5.51.1"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
@@ -158,6 +158,9 @@ android {
exclude 'signal_jni.dll'
}
buildFeatures {
viewBinding true
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
@@ -171,6 +174,8 @@ android {
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "Signal");
manifestPlaceholders = [mapsKey:"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"]
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
@@ -178,7 +183,7 @@ android {
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
@@ -195,12 +200,14 @@ android {
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "String", "CDSI_MRENCLAVE", "\"e5eaa62da3514e8b37ccabddb87e52e7f319ccf5120a13f9e1b42b87ec9dd3dd\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
buildConfigField "String", "CDS_MRENCLAVE", "\"74778bb0f93ae1f78c26e67152bab0bbeb693cd56d1bb9b4e9244157acc58081\""
buildConfigField "String", "CDSI_MRENCLAVE", "\"ef4787a56a154ac6d009138cac17155acd23cfe4329281252365dd7c252e7fbf\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
"\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
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", "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('", "') + '"}'
@@ -231,7 +238,7 @@ android {
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "org.thoughtcrime.securesms.testing.SignalTestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
@@ -263,6 +270,8 @@ android {
testProguardFiles 'proguard/proguard-automation.pro',
'proguard/proguard.cfg'
manifestPlaceholders = [mapsKey:getMapsKey()]
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
}
@@ -344,12 +353,15 @@ android {
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
buildConfigField "String", "CDS_MRENCLAVE", "\"74778bb0f93ae1f78c26e67152bab0bbeb693cd56d1bb9b4e9244157acc58081\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
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", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\""
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
@@ -405,10 +417,11 @@ dependencies {
implementation (libs.androidx.appcompat) {
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.material.material
implementation libs.androidx.legacy.support
@@ -421,7 +434,9 @@ dependencies {
implementation libs.androidx.multidex
implementation libs.androidx.navigation.fragment.ktx
implementation libs.androidx.navigation.ui.ktx
implementation libs.androidx.lifecycle.extensions
implementation libs.androidx.lifecycle.viewmodel.ktx
implementation libs.androidx.lifecycle.livedata.ktx
implementation libs.androidx.lifecycle.process
implementation libs.androidx.lifecycle.viewmodel.savedstate
implementation libs.androidx.lifecycle.common.java8
implementation libs.androidx.lifecycle.reactivestreams.ktx
@@ -458,6 +473,7 @@ dependencies {
implementation project(':donations')
implementation project(':contacts')
implementation project(':qr')
implementation project(':sms-exporter')
implementation libs.libsignal.android
implementation libs.google.protobuf.javalite
@@ -544,6 +560,13 @@ dependencies {
androidTestImplementation testLibs.androidx.test.core
androidTestImplementation testLibs.androidx.test.core.ktx
androidTestImplementation testLibs.androidx.test.ext.junit.ktx
androidTestImplementation testLibs.mockito.android
androidTestImplementation testLibs.mockito.kotlin
androidTestImplementation testLibs.square.okhttp.mockserver
instrumentationImplementation (libs.androidx.fragment.testing) {
exclude group: 'androidx.test', module: 'core'
}
testImplementation testLibs.espresso.core
@@ -555,7 +578,7 @@ dependencies {
implementation libs.rxjava3.rxkotlin
implementation libs.rxdogtag
androidTestUtil 'androidx.test:orchestrator:1.4.1'
androidTestUtil testLibs.androidx.test.orchestrator
}
def getLastCommitTimestamp() {
@@ -634,3 +657,11 @@ def getDateSuffix() {
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
return formattedDate
}
def getMapsKey() {
def mapKey = file("${project.rootDir}/maps.key")
if (mapKey.exists()) {
return mapKey.readLines()[0]
}
return "AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"
}

View File

@@ -0,0 +1,15 @@
package org.thoughtcrime.securesms
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
/**
* Application context for running instrumentation tests (aka androidTests).
*/
class SignalInstrumentationApplicationContext : ApplicationContext() {
override fun initializeAppDependencies() {
val default = ApplicationDependencyProvider(this)
ApplicationDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
}
}

View File

@@ -0,0 +1,376 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import androidx.lifecycle.SavedStateHandle
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.mockito.kotlin.mock
import org.signal.core.util.ThreadUtil
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.MockProvider
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.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsNull
import org.thoughtcrime.securesms.testing.assertIsSize
import org.thoughtcrime.securesms.testing.connectionFailure
import org.thoughtcrime.securesms.testing.failure
import org.thoughtcrime.securesms.testing.parsedRequestBody
import org.thoughtcrime.securesms.testing.success
import org.thoughtcrime.securesms.testing.timeout
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.MismatchedDevices
import org.whispersystems.signalservice.internal.push.PreKeyState
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class ChangeNumberViewModelTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var viewModel: ChangeNumberViewModel
private lateinit var kbsRepository: KbsRepository
@Before
fun setUp() {
ApplicationDependencies.getSignalServiceAccountManager().setSoTimeoutMillis(1000)
kbsRepository = mock()
ThreadUtil.runOnMainSync {
viewModel = ChangeNumberViewModel(
localNumber = harness.self.requireE164(),
changeNumberRepository = ChangeNumberRepository(),
savedState = SavedStateHandle(),
password = SignalStore.account().servicePassword!!,
verifyAccountRepository = VerifyAccountRepository(harness.application),
kbsRepository = kbsRepository
)
viewModel.setNewCountry(1)
viewModel.setNewNationalNumber("5555550102")
}
}
@After
fun tearDown() {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@Test
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
// 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()
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
},
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)
}
/**
* If we encounter a server error, this means the server ack our request and rejected it. In this
* case we know the change *did not* take on the server and can reset to a clean state.
*/
@Test
fun testChangeNumber_givenServerFailedApiCall() {
// GIVEN
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { MockResponse().failure(500) },
)
// WHEN
val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs true
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
/**
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
* number on the server side. We have to do a whoami call to query the server for our details and then
* respond accordingly.
*
* In this case, the whoami is our old details, so we can know the change *did not* take on the server
* and can reset to a clean state.
*/
@Test
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToServer() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { MockResponse().connectionFailure() },
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) }
)
// WHEN
val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().isChangeNumberLocked assertIs false
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
/**
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
* number on the server side. We have to do a whoami call to query the server for our details and then
* respond accordingly.
*
* In this case, the whoami is our new details, so we can know the change *did* take on the server
* and need to keep the app in a locked state. The test then uses the ChangeNumberLockActivity to unlock
* and apply the pending state after confirming the change on the server.
*/
@Test
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
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()
MockResponse().timeout()
},
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, newPni, "+15555550102")) },
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
val processor: VerifyAccountResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().isChangeNumberLocked assertIs true
SignalStore.misc().pendingChangeNumberMetadata.assertIsNotNull()
// WHEN AGAIN Processing lock
val scenario = harness.launchActivity<ChangeNumberLockActivity>()
scenario.onActivity {}
ThreadUtil.sleep(500)
// THEN AGAIN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() {
// 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(
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v1/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
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)
}
@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) {
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore = SignalStore.account().pniPreKeys
Recipient.self().requireE164() assertIs "+15555550102"
Recipient.self().requirePni() assertIs newPni
SignalStore.account().pniRegistrationId assertIs changeNumberRequest.pniRegistrationIds["1"]!!
SignalStore.account().pniIdentityKey.publicKey assertIs changeNumberRequest.pniIdentityKey
pniMetadataStore.activeSignedPreKeyId assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.keyId
val activeSignedPreKey: SignedPreKeyRecord = pniProtocolStore.loadSignedPreKey(pniMetadataStore.activeSignedPreKeyId)
activeSignedPreKey.keyPair.publicKey assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.publicKey
activeSignedPreKey.signature assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.signature
setPreKeysRequest.signedPreKey.publicKey assertIs activeSignedPreKey.keyPair.publicKey
setPreKeysRequest.preKeys assertIsSize 100
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
}

View File

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

View File

@@ -18,6 +18,77 @@ class RecipientDatabaseTest {
@get:Rule
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
fun givenABlockedRecipient_whenIQueryAllContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]

View File

@@ -292,6 +292,7 @@ class RecipientDatabaseTest_getAndPossiblyMergeLegacy {
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertFalse(existingRecipient2.hasE164())
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}

View File

@@ -9,7 +9,6 @@ import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
@@ -188,7 +187,6 @@ class RecipientDatabaseTest_getAndPossiblyMergePnp {
}
/** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */
@Ignore("Change self isn't implemented yet!")
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
@@ -237,16 +235,15 @@ class RecipientDatabaseTest_getAndPossiblyMergePnp {
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, null)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMergePnp(null, E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingAciId, retrievedId)
val mergedId: RecipientId = recipientDatabase.getAndPossiblyMergePnp(ACI_A, E164_A)
assertEquals(existingAciId, mergedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
val retrievedRecipient = Recipient.resolved(mergedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
// TODO: Recipient remapping!
// val existingE164Recipient = Recipient.resolved(existingE164Id)
// assertEquals(retrievedId, existingE164Recipient.id)
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(mergedId, existingE164Recipient.id)
changeNumberListener.waitForJobManager()
assertFalse(changeNumberListener.numberChangeWasEnqueued)
@@ -268,13 +265,11 @@ class RecipientDatabaseTest_getAndPossiblyMergePnp {
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
// TODO: Recipient remapping!
// val existingE164Recipient = Recipient.resolved(existingE164Id)
// assertEquals(retrievedId, existingE164Recipient.id)
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
// TODO: Change number!
// changeNumberListener.waitForJobManager()
// assert(changeNumberListener.numberChangeWasEnqueued)
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** No new rules here, just a more complex scenario to show how different rules interact. */
@@ -297,8 +292,8 @@ class RecipientDatabaseTest_getAndPossiblyMergePnp {
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertFalse(existingRecipient2.hasE164())
// TODO: Change number!
// assert(changeNumberListener.numberChangeWasEnqueued)
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/**
@@ -319,13 +314,11 @@ class RecipientDatabaseTest_getAndPossiblyMergePnp {
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
// TODO: Recipient remapping!
// val recipientWithId2 = Recipient.resolved(existingId2)
// assertEquals(retrievedId, recipientWithId2.id)
val recipientWithId2 = Recipient.resolved(existingId2)
assertEquals(retrievedId, recipientWithId2.id)
}
/** We never want to remove the e164 of our own contact entry. Leave the e164 alone. */
@Ignore("Change self isn't implemented yet!")
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
@@ -402,13 +395,11 @@ class RecipientDatabaseTest_getAndPossiblyMergePnp {
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
// TODO: Change number!
// changeNumberListener.waitForJobManager()
// assert(changeNumberListener.numberChangeWasEnqueued)
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Ignore("This level of merging isn't implemented yet!")
@Test
fun getAndPossiblyMerge_merge_general() {
// Setup

View File

@@ -16,7 +16,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.lang.IllegalArgumentException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
@@ -61,14 +60,7 @@ class RecipientDatabaseTest_processPnpTuple {
}
}
@Test(expected = IllegalArgumentException::class)
fun noMatch_pniOnly() {
test {
process(null, PNI_A, null)
}
}
@Test(expected = IllegalArgumentException::class)
@Test(expected = IllegalStateException::class)
fun noMatch_noData() {
test {
process(null, null, null)
@@ -416,7 +408,13 @@ class RecipientDatabaseTest_processPnpTuple {
}
fun process(e164: String?, pni: PNI?, aci: ACI?) {
generatedIds += recipientDatabase.processPnpTuple(e164, pni, aci, false)
SignalDatabase.rawDatabase.beginTransaction()
try {
generatedIds += recipientDatabase.processPnpTuple(e164, pni, aci, pniVerified = false).finalId
SignalDatabase.rawDatabase.setTransactionSuccessful()
} finally {
SignalDatabase.rawDatabase.endTransaction()
}
}
fun expect(e164: String?, pni: PNI?, aci: ACI?) {

View File

@@ -14,7 +14,6 @@ import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.lang.AssertionError
import java.lang.IllegalArgumentException
import java.lang.IllegalStateException
import java.util.UUID
@@ -68,12 +67,7 @@ class RecipientDatabaseTest_processPnpTupleToChangeSet {
)
}
@Test(expected = IllegalArgumentException::class)
fun noMatch_pniOnly() {
db.processPnpTupleToChangeSet(null, PNI_A, null, pniVerified = false)
}
@Test(expected = IllegalArgumentException::class)
@Test(expected = IllegalStateException::class)
fun noMatch_noData() {
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
}
@@ -603,11 +597,48 @@ class RecipientDatabaseTest_processPnpTupleToChangeSet {
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.Update(
PnpOperation.SetPni(
recipientId = result.secondId,
pni = PNI_A,
),
PnpOperation.SetE164(
recipientId = result.secondId,
e164 = E164_A,
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_B, PNI_A, null),
Input(E164_C, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.SetPni(
recipientId = result.secondId,
pni = PNI_A,
aci = ACI_A
),
PnpOperation.SetE164(
recipientId = result.secondId,
e164 = E164_A,
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_C,
newE164 = E164_A
)
)
),
@@ -806,5 +837,6 @@ class RecipientDatabaseTest_processPnpTupleToChangeSet {
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
const val E164_C = "+14441234567"
}
}

View File

@@ -0,0 +1,119 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.SqlUtil
import org.thoughtcrime.securesms.database.DistributionListDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.whispersystems.signalservice.api.push.DistributionId
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class MyStoryMigrationTest {
@get:Rule val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
@Test
fun givenAValidMyStory_whenIMigrate_thenIExpectMyStoryToBeValid() {
// GIVEN
assertValidMyStoryExists()
// WHEN
runMigration()
// THEN
assertValidMyStoryExists()
}
@Test
fun givenNoMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() {
// GIVEN
deleteMyStory()
// WHEN
runMigration()
// THEN
assertValidMyStoryExists()
}
@Test
fun givenA00000000DistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() {
// GIVEN
setMyStoryDistributionId("0000-0000")
// WHEN
runMigration()
// THEN
assertValidMyStoryExists()
}
@Test
fun givenARandomDistributionIdForMyStory_whenIMigrate_thenIExpectMyStoryToBeCreated() {
// GIVEN
setMyStoryDistributionId(UUID.randomUUID().toString())
// WHEN
runMigration()
// THEN
assertValidMyStoryExists()
}
private fun setMyStoryDistributionId(serializedId: String) {
SignalDatabase.rawDatabase.update(
DistributionListDatabase.LIST_TABLE_NAME,
contentValuesOf(
DistributionListDatabase.DISTRIBUTION_ID to serializedId
),
"_id = ?",
SqlUtil.buildArgs(DistributionListId.MY_STORY)
)
}
private fun deleteMyStory() {
SignalDatabase.rawDatabase.delete(
DistributionListDatabase.LIST_TABLE_NAME,
"_id = ?",
SqlUtil.buildArgs(DistributionListId.MY_STORY)
)
}
private fun assertValidMyStoryExists() {
SignalDatabase.rawDatabase.query(
DistributionListDatabase.LIST_TABLE_NAME,
SqlUtil.COUNT,
"_id = ? AND ${DistributionListDatabase.DISTRIBUTION_ID} = ?",
SqlUtil.buildArgs(DistributionListId.MY_STORY, DistributionId.MY_STORY.toString()),
null,
null,
null
).use {
if (it.moveToNext()) {
val count = it.getInt(0)
assertEquals("assertValidMyStoryExists: Query produced an unexpected count.", 1, count)
} else {
fail("assertValidMyStoryExists: Query did not produce a count.")
}
}
}
private fun runMigration() {
V151_MyStoryMigration.migrate(
InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
SignalDatabase.rawDatabase,
0,
1
)
}
}

View File

@@ -0,0 +1,110 @@
package org.thoughtcrime.securesms.dependencies
import android.app.Application
import okhttp3.ConnectionSpec
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.mockwebserver.RecordedRequest
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.KbsEnclave
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess
import org.thoughtcrime.securesms.push.SignalServiceTrustStore
import org.thoughtcrime.securesms.testing.Verb
import org.thoughtcrime.securesms.testing.runSync
import org.thoughtcrime.securesms.util.Base64
import org.whispersystems.signalservice.api.KeyBackupService
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl
import java.security.KeyStore
import java.util.Optional
/**
* Dependency provider used for instrumentation tests (aka androidTests).
*
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess] and
* [KeyBackupService].
*/
class InstrumentationApplicationDependencyProvider(application: Application, default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
private val serviceTrustStore: TrustStore
private val uncensoredConfiguration: SignalServiceConfiguration
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
private val keyBackupService: KeyBackupService
init {
runSync {
webServer = MockWebServer()
baseUrl = webServer.url("").toString()
}
webServer.setDispatcher(object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
val handler = handlers.firstOrNull {
request.method == it.verb && request.path.startsWith("/${it.path}")
}
return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500)
}
})
serviceTrustStore = SignalServiceTrustStore(application)
uncensoredConfiguration = SignalServiceConfiguration(
arrayOf(SignalServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
mapOf(
0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT))
),
arrayOf(SignalContactDiscoveryUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
arrayOf(SignalKeyBackupServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
emptyList(),
Optional.of(SignalServiceNetworkAccess.DNS),
Optional.empty(),
Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS)
)
serviceNetworkAccessMock = mock {
on { getConfiguration() } doReturn uncensoredConfiguration
on { getConfiguration(any()) } doReturn uncensoredConfiguration
on { uncensoredConfiguration } doReturn uncensoredConfiguration
}
keyBackupService = mock()
}
override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess {
return serviceNetworkAccessMock
}
override fun provideKeyBackupService(signalServiceAccountManager: SignalServiceAccountManager, keyStore: KeyStore, enclave: KbsEnclave): KeyBackupService {
return keyBackupService
}
companion object {
lateinit var webServer: MockWebServer
private set
lateinit var baseUrl: String
private set
private val handlers: MutableList<Verb> = mutableListOf()
fun addMockWebRequestHandlers(vararg verbs: Verb) {
handlers.addAll(verbs)
}
fun clearHandlers() {
handlers.clear()
}
}
}

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

@@ -0,0 +1,114 @@
package org.thoughtcrime.securesms.testing
import io.reactivex.rxjava3.core.Single
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
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.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository
import org.thoughtcrime.securesms.pin.TokenData
import org.thoughtcrime.securesms.test.BuildConfig
import org.whispersystems.signalservice.api.KbsPinData
import org.whispersystems.signalservice.api.KeyBackupService
import org.whispersystems.signalservice.api.kbs.HashedPin
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
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.contacts.entities.TokenResponse
import org.whispersystems.signalservice.internal.push.AuthCredentials
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.SenderCertificate
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
import java.security.SecureRandom
/**
* Warehouse of reusable test data and mock configurations.
*/
object MockProvider {
val senderCertificate = SenderCertificate().apply { certificate = ByteArray(0) }
val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply {
backupCredentials = AuthCredentials.create("username", "password")
}
val primaryOnlyDeviceList = DeviceInfoList().apply {
devices = listOf(
DeviceInfo().apply {
id = 1
}
)
}
fun createVerifyAccountResponse(aci: ServiceId, newPni: ServiceId): VerifyAccountResponse {
return VerifyAccountResponse().apply {
uuid = aci.toString()
pni = newPni.toString()
storageCapable = false
}
}
fun createWhoAmIResponse(aci: ServiceId, pni: ServiceId, e164: String): WhoAmIResponse {
return WhoAmIResponse().apply {
this.uuid = aci.toString()
this.pni = pni.toString()
this.number = e164
}
}
fun mockGetRegistrationLockStringFlow(kbsRepository: KbsRepository) {
val tokenData: TokenData = mock {
on { enclave } doReturn BuildConfig.KBS_ENCLAVE
on { basicAuth } doReturn "basicAuth"
on { triesRemaining } doReturn 10
on { tokenResponse } doReturn TokenResponse()
}
kbsRepository.stub {
on { getToken(any() as String) } doReturn Single.just(ServiceResponse.forResult(tokenData, 200, ""))
}
val session: KeyBackupService.RestoreSession = object : KeyBackupService.RestoreSession {
override fun hashSalt(): ByteArray = Hex.fromStringCondensed("cba811749042b303a6a7efa5ccd160aea5e3ea243c8d2692bd13d515732f51a8")
override fun restorePin(hashedPin: HashedPin?): KbsPinData = KbsPinData(MasterKey.createNew(SecureRandom()), null)
}
val kbsService = ApplicationDependencies.getKeyBackupService(BuildConfig.KBS_ENCLAVE)
kbsService.stub {
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,48 @@
package org.thoughtcrime.securesms.testing
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.RecordedRequest
import okhttp3.mockwebserver.SocketPolicy
import org.thoughtcrime.securesms.util.JsonUtils
import java.util.concurrent.TimeUnit
typealias ResponseFactory = (request: RecordedRequest) -> MockResponse
/**
* Represent an HTTP verb for mocking web requests.
*/
sealed class Verb(val verb: String, val path: String, val responseFactory: ResponseFactory)
class Get(path: String, responseFactory: ResponseFactory) : Verb("GET", path, responseFactory)
class Put(path: String, responseFactory: ResponseFactory) : Verb("PUT", path, responseFactory)
fun MockResponse.success(response: Any? = null): MockResponse {
return setResponseCode(200).apply {
if (response != null) {
setBody(JsonUtils.toJson(response))
}
}
}
fun MockResponse.failure(code: Int, response: Any? = null): MockResponse {
return setResponseCode(code).apply {
if (response != null) {
setBody(JsonUtils.toJson(response))
}
}
}
fun MockResponse.connectionFailure(): MockResponse {
return setSocketPolicy(SocketPolicy.DISCONNECT_AT_START)
}
fun MockResponse.timeout(): MockResponse {
return setHeadersDelay(1, TimeUnit.DAYS)
.setBodyDelay(1, TimeUnit.DAYS)
}
inline fun <reified T> RecordedRequest.parsedRequestBody(): T {
val bodyString = String(body.readByteArray())
return JsonUtils.fromJson(bodyString, T::class.java)
}

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

@@ -8,6 +8,7 @@ import android.content.SharedPreferences
import android.preference.PreferenceManager
import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import org.junit.rules.ExternalResource
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.SignalProtocolAddress
@@ -17,6 +18,7 @@ import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.profiles.ProfileName
@@ -29,6 +31,8 @@ import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.lang.IllegalArgumentException
import java.util.UUID
@@ -54,6 +58,8 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
context = InstrumentationRegistry.getInstrumentation().targetContext
self = setupSelf()
others = setupOthers()
InstrumentationApplicationDependencyProvider.clearHandlers()
}
private fun setupSelf(): Recipient {
@@ -67,18 +73,22 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
val registrationRepository = RegistrationRepository(application)
registrationRepository.registerAccountWithoutRegistrationLock(
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(Put("/v2/keys") { MockResponse().success() })
val response: ServiceResponse<VerifyAccountResponse> = registrationRepository.registerAccountWithoutRegistrationLock(
RegistrationData(
code = "123123",
e164 = "+15554045550101",
e164 = "+15555550101",
password = Util.getSecret(18),
registrationId = registrationRepository.registrationId,
profileKey = registrationRepository.getProfileKey("+15554045550101"),
fcmToken = null
profileKey = registrationRepository.getProfileKey("+15555550101"),
fcmToken = null,
pniRegistrationId = registrationRepository.pniRegistrationId
),
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false)
).blockingGet()
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
SignalStore.kbsValues().optOut()
RegistrationUtil.maybeMarkRegistrationComplete(application)
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
@@ -98,7 +108,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
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)
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey)
others += recipientId
@@ -107,7 +117,7 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
return others
}
inline fun <reified T : Activity> launchActivity(initIntent: Intent.() -> Unit): ActivityScenario<T> {
inline fun <reified T : Activity> launchActivity(initIntent: Intent.() -> Unit = {}): ActivityScenario<T> {
return androidx.test.core.app.launchActivity(Intent(context, T::class.java).apply(initIntent))
}

View File

@@ -0,0 +1,16 @@
package org.thoughtcrime.securesms.testing
import android.app.Application
import android.content.Context
import androidx.test.runner.AndroidJUnitRunner
import org.thoughtcrime.securesms.SignalInstrumentationApplicationContext
/**
* Custom runner that replaces application with [SignalInstrumentationApplicationContext].
*/
@Suppress("unused")
class SignalTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, className: String?, context: Context?): Application {
return super.newApplication(cl, SignalInstrumentationApplicationContext::class.java.name, context)
}
}

View File

@@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.testing
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.notNullValue
import org.hamcrest.Matchers.nullValue
import java.util.concurrent.CountDownLatch
/**
* Run the given [runnable] on a new thread and wait for it to finish.
*/
fun runSync(runnable: () -> Unit) {
val lock = CountDownLatch(1)
Thread {
try {
runnable.invoke()
} finally {
lock.countDown()
}
}.start()
lock.await()
}
/* Various kotlin-ifications of hamcrest matchers */
fun <T : Any?> T.assertIsNull() {
assertThat(this, nullValue())
}
fun <T : Any?> T.assertIsNotNull() {
assertThat(this, notNullValue())
}
infix fun <T : Any> T.assertIs(expected: T) {
assertThat(this, `is`(expected))
}
infix fun <T : Any> T.assertIsNot(expected: T) {
assertThat(this, not(`is`(expected)))
}
infix fun <E, T : Collection<E>> T.assertIsSize(expected: Int) {
assertThat(this, hasSize(expected))
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms">
<application
android:usesCleartextTraffic="true"
tools:replace="android:usesCleartextTraffic"
tools:ignore="UnusedAttribute" />
</manifest>

View File

@@ -109,7 +109,7 @@
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
android:value="${mapsKey}"/>
<meta-data android:name="android.supports_size_changes"
android:value="true" />
@@ -155,7 +155,8 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<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>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -181,10 +182,10 @@
<activity android:name=".sharing.v2.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="true"
android:excludeFromRecents="true"
android:taskAffinity=""
android:windowSoftInputMode="stateHidden"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.SEND" />
@@ -212,13 +213,14 @@
</activity>
<activity android:name=".stickers.StickerPackPreviewActivity"
android:exported="true"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<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.BROWSABLE" />
<data android:scheme="sgnl"
@@ -255,6 +257,7 @@
</activity-alias>
<activity android:name=".deeplinks.DeepLinkEntryActivity"
android:exported="true"
android:noHistory="true"
android:theme="@style/Signal.Transparent">
@@ -386,10 +389,12 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".verify.VerifyIdentityActivity"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".components.settings.app.AppSettingsActivity"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize">
@@ -517,10 +522,11 @@
android:finishOnTaskLaunch="true" />
<activity android:name=".PlayServicesProblemActivity"
android:exported="false"
android:theme="@style/TextSecure.DialogActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".SmsSendtoActivity">
<activity android:name=".SmsSendtoActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SENDTO" />
<action android:name="android.intent.action.VIEW" />
@@ -539,6 +545,7 @@
</activity>
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
android:exported="true"
android:excludeFromRecents="true"
android:permission="android.permission.CALL_PHONE"
android:theme="@style/NoAnimation.Theme.BlackScreen"
@@ -554,7 +561,7 @@
</activity>
<activity android:name=".mediasend.AvatarSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:theme="@style/TextSecure.DarkNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".blocked.BlockedUsersActivity"
@@ -570,6 +577,10 @@
android:theme="@style/TextSecure.LightRegistrationTheme"
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"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />
@@ -659,7 +670,7 @@
<activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.FullScreenMedia" />
android:theme="@style/TextSecure.DarkNoActionBar" />
<activity android:name=".wallpaper.crop.WallpaperCropActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -670,6 +681,12 @@
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
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.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
@@ -682,13 +699,13 @@
</intent-filter>
</service>
<service android:name=".components.voice.VoiceNotePlaybackService">
<service android:name=".components.voice.VoiceNotePlaybackService" android:exported="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
@@ -728,7 +745,7 @@
<service android:name=".gcm.FcmFetchForegroundService" />
<service android:name=".gcm.FcmReceiveService">
<service android:name=".gcm.FcmReceiveService" android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
@@ -819,51 +836,51 @@
</provider>
<receiver android:name=".service.BootReceiver">
<receiver android:name=".service.BootReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="org.thoughtcrime.securesms.RESTART"/>
</intent-filter>
</receiver>
<receiver android:name=".service.DirectoryRefreshListener">
<receiver android:name=".service.DirectoryRefreshListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".service.RotateSignedPreKeyListener">
<receiver android:name=".service.RotateSignedPreKeyListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".service.RotateSenderCertificateListener">
<receiver android:name=".service.RotateSenderCertificateListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".messageprocessingalarm.MessageProcessReceiver">
<receiver android:name=".messageprocessingalarm.MessageProcessReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="org.thoughtcrime.securesms.action.PROCESS_MESSAGES" />
</intent-filter>
</receiver>
<receiver android:name=".service.LocalBackupListener">
<receiver android:name=".service.LocalBackupListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".service.PersistentConnectionBootListener">
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
<receiver android:name=".notifications.LocaleChangedReceiver">
<receiver android:name=".notifications.LocaleChangedReceiver" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED"/>
</intent-filter>
@@ -871,7 +888,7 @@
<receiver android:name=".notifications.MessageNotifier$ReminderReceiver"/>
<receiver android:name=".notifications.DeleteNotificationReceiver">
<receiver android:name=".notifications.DeleteNotificationReceiver" android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.DELETE_NOTIFICATION"/>
</intent-filter>
@@ -899,16 +916,19 @@
<service
android:name=".jobmanager.KeepAliveService"
android:enabled="@bool/enable_alarm_manager" />
android:enabled="@bool/enable_alarm_manager"
android:exported="false"/>
<receiver
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 -->
<receiver
android:name=".jobmanager.BootReceiver"
android:enabled="true">
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</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;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.util.FeatureFlags;
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.
*/
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

@@ -21,6 +21,7 @@ import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.multidex.MultiDexApplication;
@@ -49,20 +50,21 @@ import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
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.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
@@ -179,13 +181,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeFcmCheck)
.addNonBlocking(CreateSignedPreKeyJob::enqueueIfNeeded)
.addNonBlocking(PreKeysSyncJob::enqueueIfNeeded)
.addNonBlocking(this::initializePeriodicTasks)
.addNonBlocking(this::initializeCircumvention)
.addNonBlocking(this::initializePendingMessages)
.addNonBlocking(this::initializeCleanup)
.addNonBlocking(this::initializeGlideCodecs)
.addNonBlocking(RefreshPreKeysJob::scheduleIfNecessary)
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
.addNonBlocking(EmojiSource::refresh)
@@ -195,6 +196,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
.addPostRender(this::initializeTrimThreadsByDateManager)
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
@@ -205,6 +207,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -230,6 +233,16 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getShakeToReport().enable();
checkBuildExpiration();
long lastForegroundTime = SignalStore.misc().getLastForegroundTime();
long currentTime = System.currentTimeMillis();
long timeDiff = currentTime - lastForegroundTime;
if (timeDiff < 0) {
Log.w(TAG, "Time travel! The system clock has moved backwards. (currentTime: " + currentTime + " ms, lastForegroundTime: " + lastForegroundTime + " ms, diff: " + timeDiff + " ms)");
}
SignalStore.misc().setLastForegroundTime(currentTime);
});
Log.d(TAG, "onStart() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -329,7 +342,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
ApplicationDependencies.getIncomingMessageObserver();
}
private void initializeAppDependencies() {
@VisibleForTesting
void initializeAppDependencies() {
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this));
}
@@ -373,6 +387,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
}
private void initializeTrimThreadsByDateManager() {
KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration();
if (keepMessagesDuration != KeepMessagesDuration.FOREVER) {
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
}
}
private void initializePeriodicTasks() {
RotateSignedPreKeyListener.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 ContactChipViewModel contactChipViewModel;
private LifecycleDisposable lifecycleDisposable;
private HeaderActionProvider headerActionProvider;
private TextView headerActionView;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback;
private GlideRequests glideRequests;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean hideCount;
private boolean canSelectSelf;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback;
@Nullable private OnItemLongClickListener onItemLongClickListener;
private GlideRequests glideRequests;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean hideCount;
private boolean canSelectSelf;
@Override
public void onAttach(@NonNull Context context) {
@@ -206,6 +206,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (getParentFragment() instanceof HeaderActionProvider) {
headerActionProvider = (HeaderActionProvider) getParentFragment();
}
if (context instanceof OnItemLongClickListener) {
onItemLongClickListener = (OnItemLongClickListener) context;
}
if (getParentFragment() instanceof OnItemLongClickListener) {
onItemLongClickListener = (OnItemLongClickListener) getParentFragment();
}
}
@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() {
@@ -850,6 +867,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
@NonNull HeaderAction getHeaderAction();
}
public interface OnItemLongClickListener {
boolean onLongClick(ContactSelectionListItem contactSelectionListItem);
}
public interface AbstractContactsCursorLoaderFactoryProvider {
@NonNull AbstractContactsCursorLoader.Factory get();
}

View File

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

View File

@@ -44,13 +44,15 @@ import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.animation.DepthPageTransformer;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@@ -84,8 +86,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import io.reactivex.rxjava3.subjects.BehaviorSubject;
/**
* Activity for displaying media attachments in-app
*/
@@ -168,7 +168,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
setSupportActionBar(findViewById(R.id.toolbar));
voiceNoteMediaController = new VoiceNoteMediaController(this);
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
viewModel = new ViewModelProvider(this).get(MediaPreviewViewModel.class);
fullscreenHelper = new FullscreenHelper(this);
@@ -466,7 +466,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
builder.setIcon(R.drawable.ic_warning);
builder.setTitle(R.string.MediaPreviewActivity_media_delete_confirmation_title);
builder.setMessage(R.string.MediaPreviewActivity_media_delete_confirmation_message);

View File

@@ -20,11 +20,27 @@ import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
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.Nullable;
import androidx.annotation.StringRes;
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.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.conversation.ConversationIntents;
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.recipients.Recipient;
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 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.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Activity container for starting a new conversation.
*
* @author Moxie Marlinspike
*
*/
public class NewConversationActivity extends ContactSelectionActivity
implements ContactSelectionListFragment.ListCallback
implements ContactSelectionListFragment.ListCallback, ContactSelectionListFragment.OnItemLongClickListener
{
@SuppressWarnings("unused")
private static final String TAG = Log.tag(NewConversationActivity.class);
private ContactsManagementViewModel viewModel;
private ActivityResultLauncher<Intent> contactLauncher;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@Override
public void onCreate(Bundle bundle, boolean ready) {
super.onCreate(bundle, ready);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
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
@@ -120,10 +161,18 @@ public class NewConversationActivity extends ContactSelectionActivity
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home: super.onBackPressed(); 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;
case android.R.id.home:
super.onBackPressed();
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;
@@ -162,4 +211,143 @@ public class NewConversationActivity extends ContactSelectionActivity
handleCreateGroup();
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.appcompat.widget.Toolbar;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricManager.Authenticators;
import androidx.biometric.BiometricPrompt;
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.TextSecurePreferences;
import kotlin.Unit;
/**
* Activity that prompts for a user's passphrase.
*
@@ -72,8 +73,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class PassphrasePromptActivity extends PassphraseActivity {
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 String BUNDLE_ALREADY_SHOWN = "bundle_already_shown";
public static final String FROM_FOREGROUND = "from_foreground";
@@ -90,9 +89,9 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private ImageButton hideButton;
private AnimatingToggle visibilityToggle;
private BiometricManager biometricManager;
private BiometricPrompt biometricPrompt;
private BiometricPrompt.PromptInfo biometricPromptInfo;
private BiometricManager biometricManager;
private BiometricPrompt biometricPrompt;
private BiometricDeviceAuthentication biometricAuth;
private boolean authenticated;
private boolean hadFailure;
@@ -249,12 +248,12 @@ public class PassphrasePromptActivity extends PassphraseActivity {
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
biometricManager = BiometricManager.from(this);
biometricPrompt = new BiometricPrompt(this, new BiometricAuthenticationListener());
biometricPromptInfo = new BiometricPrompt.PromptInfo
.Builder()
.setAllowedAuthenticators(ALLOWED_AUTHENTICATORS)
.setTitle(getString(R.string.PassphrasePromptActivity_unlock_signal))
.build();
BiometricPrompt.PromptInfo biometricPromptInfo = new BiometricPrompt.PromptInfo
.Builder()
.setAllowedAuthenticators(BiometricDeviceAuthentication.ALLOWED_AUTHENTICATORS)
.setTitle(getString(R.string.PassphrasePromptActivity_unlock_signal))
.build();
biometricAuth = new BiometricDeviceAuthentication(biometricManager, biometricPrompt, biometricPromptInfo);
setSupportActionBar(toolbar);
getSupportActionBar().setTitle("");
@@ -279,7 +278,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private void setLockTypeVisibility() {
if (TextSecurePreferences.isScreenLockEnabled(this)) {
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);
lockScreenButton.setVisibility(View.VISIBLE);
} else {
@@ -290,33 +289,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
}
private void resumeScreenLock(boolean force) {
KeyguardManager keyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
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...");
if (!biometricAuth.authenticate(getApplicationContext(), force, this::showConfirmDeviceCredentialIntent)) {
handleAuthenticated();
}
}
@@ -332,6 +305,16 @@ public class PassphrasePromptActivity extends PassphraseActivity {
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 {
@Override
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.devicetransfer.olddevice.OldDeviceTransferActivity;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.username.AddAUsernameActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
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_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private static final int STATE_CREATE_USERNAME = 11;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -156,6 +160,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
case STATE_CREATE_USERNAME: return getCreateUsernameIntent();
default: return null;
}
}
@@ -175,6 +180,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_CREATE_SIGNAL_PIN;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;
} else if (shouldAskUserToCreateUsername()) {
return STATE_CREATE_USERNAME;
} else if (userMustCreateSignalPin()) {
return STATE_CREATE_SIGNAL_PIN;
} 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();
}
private boolean shouldAskUserToCreateUsername() {
return FeatureFlags.usernames() &&
FeatureFlags.phoneNumberPrivacy() &&
!SignalStore.uiHints().hasSetOrSkippedUsernameCreation() &&
SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode() == PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED;
}
private Intent getCreatePassphraseIntent() {
return getRoutedIntent(PassphraseCreateActivity.class, getIntent());
}
@@ -238,7 +252,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private Intent getCreateProfileNameIntent() {
return getRoutedIntent(EditProfileActivity.class, getIntent());
Intent intent = EditProfileActivity.getIntentForUserProfile(this);
return getRoutedIntent(intent, getIntent());
}
private Intent getOldDeviceTransferIntent() {
@@ -258,6 +273,15 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
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) {
final Intent intent = new Intent(this, destination);
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.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.media.AudioManager;
import android.os.Build;
@@ -40,15 +39,18 @@ import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.lifecycle.ViewModelProvider;
import androidx.window.DisplayFeature;
import androidx.window.FoldingFeature;
import androidx.window.WindowLayoutInfo;
import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter;
import androidx.window.layout.DisplayFeature;
import androidx.window.layout.FoldingFeature;
import androidx.window.layout.WindowInfoTracker;
import androidx.window.layout.WindowLayoutInfo;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
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 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 WifiToCellularPopupWindow wifiToCellularPopupWindow;
private DeviceOrientationMonitor deviceOrientationMonitor;
private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen;
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable;
private boolean hasWarnedAboutBluetooth;
private androidx.window.WindowManager windowManager;
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
private ThrottledDebouncer requestNewSizesThrottle;
private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen;
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable;
private boolean hasWarnedAboutBluetooth;
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
private WindowInfoTrackerCallbackAdapter windowInfoTrackerCallbackAdapter;
private ThrottledDebouncer requestNewSizesThrottle;
private Disposable ephemeralStateDisposable = Disposable.empty();
@@ -133,7 +136,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@SuppressLint("SourceLockedOrientationActivity")
@Override
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_KEEP_SCREEN_ON);
super.onCreate(savedInstanceState);
@@ -158,10 +161,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
windowManager = new androidx.window.WindowManager(this);
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));
}
@@ -185,11 +188,25 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
if (!EventBus.getDefault().isRegistered(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
public void onNewIntent(Intent intent) {
Log.i(TAG, "onNewIntent");
Log.i(TAG, "onNewIntent(" + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")");
super.onNewIntent(intent);
processIntent(intent);
}
@@ -234,7 +251,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
protected void onDestroy() {
super.onDestroy();
windowManager.unregisterLayoutChangeCallback(windowLayoutInfoConsumer);
windowInfoTrackerCallbackAdapter.removeWindowLayoutInfoListener(windowLayoutInfoConsumer);
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() {
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
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.getControlsRotation().observe(this, callScreen::rotateControls);
addOnPictureInPictureModeChangedListener(info -> {
viewModel.setIsInPipMode(info.isInPictureInPictureMode());
participantUpdateWindow.setEnabled(!info.isInPictureInPictureMode());
});
}
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {

View File

@@ -0,0 +1,39 @@
package org.thoughtcrime.securesms.backup;
public class BackupEvent {
public enum Type {
PROGRESS,
PROGRESS_VERIFYING,
FINISHED
}
private final Type type;
private final long count;
private final long estimatedTotalCount;
public BackupEvent(Type type, long count, long estimatedTotalCount) {
this.type = type;
this.count = count;
this.estimatedTotalCount = estimatedTotalCount;
}
public Type getType() {
return type;
}
public long getCount() {
return count;
}
public long getEstimatedTotalCount() {
return estimatedTotalCount;
}
public double getCompletionPercentage() {
if (estimatedTotalCount == 0) {
return 0;
}
return Math.min(99.9f, (double) count * 100L / (double) estimatedTotalCount);
}
}

View File

@@ -10,6 +10,7 @@ import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import org.signal.core.util.PendingIntentFlags;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper;
@@ -21,6 +22,8 @@ public enum BackupFileIOError {
ACCESS_ERROR(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_directory_has_been_deleted_or_moved),
FILE_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_file_is_too_large),
NOT_ENOUGH_SPACE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_there_is_not_enough_space),
VERIFICATION_FAILED(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_could_not_be_verified),
ATTACHMENT_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_contains_a_very_large_file),
UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups);
private static final short BACKUP_FAILED_ID = 31321;
@@ -38,11 +41,12 @@ public enum BackupFileIOError {
}
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)
.setSmallIcon(R.drawable.ic_signal_backup)
.setContentTitle(context.getString(titleId))
.setContentText(context.getString(messageId))
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(messageId)))
.setContentIntent(pendingIntent)
.build();
@@ -50,22 +54,25 @@ public enum BackupFileIOError {
.notify(BACKUP_FAILED_ID, backupFailedNotification);
}
public static void postNotificationForException(@NonNull Context context, @NonNull IOException e, int runAttempt) {
public static void postNotificationForException(@NonNull Context context, @NonNull IOException e) {
BackupFileIOError error = getFromException(e);
if (error != null) {
error.postNotification(context);
}
if (error == null && runAttempt > 0) {
} else {
UNKNOWN.postNotification(context);
}
}
private static @Nullable BackupFileIOError getFromException(@NonNull IOException e) {
if (e.getMessage() != null) {
if (e.getMessage().contains("EFBIG")) return FILE_TOO_LARGE;
else if (e.getMessage().contains("ENOSPC")) return NOT_ENOUGH_SPACE;
if (e instanceof FullBackupExporter.InvalidBackupStreamException) {
return ATTACHMENT_TOO_LARGE;
} else if (e.getMessage() != null) {
if (e.getMessage().contains("EFBIG")) {
return FILE_TOO_LARGE;
} else if (e.getMessage().contains("ENOSPC")) {
return NOT_ENOUGH_SPACE;
}
}
return null;

View File

@@ -0,0 +1,161 @@
package org.thoughtcrime.securesms.backup;
import androidx.annotation.NonNull;
import org.signal.core.util.Conversions;
import org.signal.core.util.StreamUtil;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.util.ByteUtil;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
class BackupRecordInputStream extends FullBackupBase.BackupStream {
private final InputStream in;
private final Cipher cipher;
private final Mac mac;
private final byte[] cipherKey;
private final byte[] iv;
private int counter;
BackupRecordInputStream(@NonNull InputStream in, @NonNull String passphrase) throws IOException {
try {
this.in = in;
byte[] headerLengthBytes = new byte[4];
StreamUtil.readFully(in, headerLengthBytes);
int headerLength = Conversions.byteArrayToInt(headerLengthBytes);
byte[] headerFrame = new byte[headerLength];
StreamUtil.readFully(in, headerFrame);
BackupProtos.BackupFrame frame = BackupProtos.BackupFrame.parseFrom(headerFrame);
if (!frame.hasHeader()) {
throw new IOException("Backup stream does not start with header!");
}
BackupProtos.Header header = frame.getHeader();
this.iv = header.getIv().toByteArray();
if (iv.length != 16) {
throw new IOException("Invalid IV length!");
}
byte[] key = getBackupKey(passphrase, header.hasSalt() ? header.getSalt().toByteArray() : null);
byte[] derived = HKDF.deriveSecrets(key, "Backup Export".getBytes(), 64);
byte[][] split = ByteUtil.split(derived, 32, 32);
this.cipherKey = split[0];
byte[] macKey = split[1];
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
this.mac = Mac.getInstance("HmacSHA256");
this.mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
this.counter = Conversions.byteArrayToInt(iv);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
BackupProtos.BackupFrame readFrame() throws IOException {
return readFrame(in);
}
void readAttachmentTo(OutputStream out, int length) throws IOException {
try {
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
mac.update(iv);
byte[] buffer = new byte[8192];
while (length > 0) {
int read = in.read(buffer, 0, Math.min(buffer.length, length));
if (read == -1) throw new IOException("File ended early!");
mac.update(buffer, 0, read);
byte[] plaintext = cipher.update(buffer, 0, read);
if (plaintext != null) {
out.write(plaintext, 0, plaintext.length);
}
length -= read;
}
byte[] plaintext = cipher.doFinal();
if (plaintext != null) {
out.write(plaintext, 0, plaintext.length);
}
out.close();
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
byte[] theirMac = new byte[10];
try {
StreamUtil.readFully(in, theirMac);
} catch (IOException e) {
throw new IOException(e);
}
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new BadMacException();
}
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
private BackupProtos.BackupFrame readFrame(InputStream in) throws IOException {
try {
byte[] length = new byte[4];
StreamUtil.readFully(in, length);
byte[] frame = new byte[Conversions.byteArrayToInt(length)];
StreamUtil.readFully(in, frame);
byte[] theirMac = new byte[10];
System.arraycopy(frame, frame.length - 10, theirMac, 0, theirMac.length);
mac.update(frame, 0, frame.length - 10);
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new IOException("Bad MAC");
}
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
byte[] plaintext = cipher.doFinal(frame, 0, frame.length - 10);
return BackupProtos.BackupFrame.parseFrom(plaintext);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
static class BadMacException extends IOException {}
}

View File

@@ -0,0 +1,82 @@
package org.thoughtcrime.securesms.backup
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
/**
* Given a backup file, run over it and verify it will decrypt properly when attempting to import it.
*/
object BackupVerifier {
private val TAG = Log.tag(BackupVerifier::class.java)
@JvmStatic
fun verifyFile(cipherStream: InputStream, passphrase: String, expectedCount: Long): Boolean {
val inputStream = BackupRecordInputStream(cipherStream, passphrase)
var count = 0L
var frame: BackupFrame = inputStream.readFrame()
while (!frame.end) {
val verified = when {
frame.hasAttachment() -> verifyAttachment(frame.attachment, inputStream)
frame.hasSticker() -> verifySticker(frame.sticker, inputStream)
frame.hasAvatar() -> verifyAvatar(frame.avatar, inputStream)
else -> true
}
if (!verified) {
return false
}
EventBus.getDefault().post(BackupEvent(BackupEvent.Type.PROGRESS_VERIFYING, ++count, expectedCount))
frame = inputStream.readFrame()
}
cipherStream.close()
return true
}
private fun verifyAttachment(attachment: BackupProtos.Attachment, inputStream: BackupRecordInputStream): Boolean {
try {
inputStream.readAttachmentTo(NullOutputStream, attachment.length)
} catch (e: IOException) {
Log.w(TAG, "Bad attachment: ${attachment.attachmentId}", e)
return false
}
return true
}
private fun verifySticker(sticker: BackupProtos.Sticker, inputStream: BackupRecordInputStream): Boolean {
try {
inputStream.readAttachmentTo(NullOutputStream, sticker.length)
} catch (e: IOException) {
Log.w(TAG, "Bad sticker: ${sticker.rowId}", e)
return false
}
return true
}
private fun verifyAvatar(avatar: BackupProtos.Avatar, inputStream: BackupRecordInputStream): Boolean {
try {
inputStream.readAttachmentTo(NullOutputStream, avatar.length)
} catch (e: IOException) {
Log.w(TAG, "Bad sticker: ${avatar.recipientId}", e)
return false
}
return true
}
private object NullOutputStream : OutputStream() {
override fun write(b: Int) = Unit
override fun write(b: ByteArray?) = Unit
override fun write(b: ByteArray?, off: Int, len: Int) = Unit
}
}

View File

@@ -17,8 +17,6 @@ public abstract class FullBackupBase {
static class BackupStream {
static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
try {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0));
MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] input = passphrase.replace(" ", "").getBytes();
byte[] hash = input;
@@ -26,7 +24,6 @@ public abstract class FullBackupBase {
if (salt != null) digest.update(salt);
for (int i = 0; i < DIGEST_ROUNDS; i++) {
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0));
digest.update(hash);
hash = digest.digest(input);
}
@@ -37,42 +34,4 @@ public abstract class FullBackupBase {
}
}
}
public static class BackupEvent {
public enum Type {
PROGRESS,
FINISHED
}
private final Type type;
private final long count;
private final long estimatedTotalCount;
BackupEvent(Type type, long count, long estimatedTotalCount) {
this.type = type;
this.count = count;
this.estimatedTotalCount = estimatedTotalCount;
}
public Type getType() {
return type;
}
public long getCount() {
return count;
}
public long getEstimatedTotalCount() {
return estimatedTotalCount;
}
public double getCompletionPercentage() {
if (estimatedTotalCount == 0) {
return 0;
}
return Math.min(99.9f, (double) count * 100L / (double) estimatedTotalCount);
}
}
}

View File

@@ -17,8 +17,11 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SetUtil;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.kdf.HKDFv3;
import org.signal.libsignal.protocol.kdf.HKDF;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
@@ -47,14 +50,11 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -79,25 +79,25 @@ public class FullBackupExporter extends FullBackupBase {
private static final String TAG = Log.tag(FullBackupExporter.class);
private static final long DATABASE_VERSION_RECORD_COUNT = 1L;
private static final long TABLE_RECORD_COUNT_MULTIPLIER = 3L;
private static final long DATABASE_VERSION_RECORD_COUNT = 1L;
private static final long TABLE_RECORD_COUNT_MULTIPLIER = 3L;
private static final long IDENTITY_KEY_BACKUP_RECORD_COUNT = 2L;
private static final long FINAL_MESSAGE_COUNT = 1L;
private static final long FINAL_MESSAGE_COUNT = 1L;
private static final Set<String> BLACKLISTED_TABLES = SetUtil.newHashSet(
SignedPreKeyDatabase.TABLE_NAME,
OneTimePreKeyDatabase.TABLE_NAME,
SessionDatabase.TABLE_NAME,
SearchDatabase.SMS_FTS_TABLE_NAME,
SearchDatabase.MMS_FTS_TABLE_NAME,
EmojiSearchDatabase.TABLE_NAME,
SenderKeyDatabase.TABLE_NAME,
SenderKeySharedDatabase.TABLE_NAME,
PendingRetryReceiptDatabase.TABLE_NAME,
AvatarPickerDatabase.TABLE_NAME
SignedPreKeyDatabase.TABLE_NAME,
OneTimePreKeyDatabase.TABLE_NAME,
SessionDatabase.TABLE_NAME,
SearchDatabase.SMS_FTS_TABLE_NAME,
SearchDatabase.MMS_FTS_TABLE_NAME,
EmojiSearchDatabase.TABLE_NAME,
SenderKeyDatabase.TABLE_NAME,
SenderKeySharedDatabase.TABLE_NAME,
PendingRetryReceiptDatabase.TABLE_NAME,
AvatarPickerDatabase.TABLE_NAME
);
public static void export(@NonNull Context context,
public static BackupEvent export(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase input,
@NonNull File output,
@@ -106,12 +106,12 @@ public class FullBackupExporter extends FullBackupBase {
throws IOException
{
try (OutputStream outputStream = new FileOutputStream(output)) {
internalExport(context, attachmentSecret, input, outputStream, passphrase, true, cancellationSignal);
return internalExport(context, attachmentSecret, input, outputStream, passphrase, true, cancellationSignal);
}
}
@RequiresApi(29)
public static void export(@NonNull Context context,
public static BackupEvent export(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase input,
@NonNull DocumentFile output,
@@ -120,7 +120,7 @@ public class FullBackupExporter extends FullBackupBase {
throws IOException
{
try (OutputStream outputStream = Objects.requireNonNull(context.getContentResolver().openOutputStream(output.getUri()))) {
internalExport(context, attachmentSecret, input, outputStream, passphrase, true, cancellationSignal);
return internalExport(context, attachmentSecret, input, outputStream, passphrase, true, cancellationSignal);
}
}
@@ -131,21 +131,21 @@ public class FullBackupExporter extends FullBackupBase {
@NonNull String passphrase)
throws IOException
{
internalExport(context, attachmentSecret, input, outputStream, passphrase, false, () -> false);
EventBus.getDefault().post(internalExport(context, attachmentSecret, input, outputStream, passphrase, false, () -> false));
}
private static void internalExport(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase input,
@NonNull OutputStream fileOutputStream,
@NonNull String passphrase,
boolean closeOutputStream,
@NonNull BackupCancellationSignal cancellationSignal)
private static BackupEvent internalExport(@NonNull Context context,
@NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase input,
@NonNull OutputStream fileOutputStream,
@NonNull String passphrase,
boolean closeOutputStream,
@NonNull BackupCancellationSignal cancellationSignal)
throws IOException
{
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
int count = 0;
long estimatedCountOutside = 0L;
long estimatedCountOutside;
try {
outputStream.writeDatabaseVersion(input.getVersion());
@@ -162,17 +162,17 @@ public class FullBackupExporter extends FullBackupBase {
for (String table : tables) {
throwIfCanceled(cancellationSignal);
if (table.equals(MmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isNonExpiringMmsMessage(cursor) && isNotReleaseChannel(cursor), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, estimatedCount, cancellationSignal);
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isNonExpiringSmsMessage(cursor) && isNotReleaseChannel(cursor), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count, estimatedCount, cancellationSignal);
} else if (table.equals(ReactionDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, new MessageId(CursorUtil.requireLong(cursor, ReactionDatabase.MESSAGE_ID), CursorUtil.requireBoolean(cursor, ReactionDatabase.IS_MMS))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(MentionDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessageAndNotReleaseChannel(input, CursorUtil.requireLong(cursor, MentionDatabase.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, CursorUtil.requireLong(cursor, MentionDatabase.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessageAndNotReleaseChannel(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessageAndNotReleaseChannel(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (table.equals(StickerDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) {
@@ -211,8 +211,8 @@ public class FullBackupExporter extends FullBackupBase {
if (closeOutputStream) {
outputStream.close();
}
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count, estimatedCountOutside));
}
return new BackupEvent(BackupEvent.Type.FINISHED, ++count, estimatedCountOutside);
}
private static long calculateCount(@NonNull Context context, @NonNull SQLiteDatabase input, List<String> tables) {
@@ -315,7 +315,7 @@ public class FullBackupExporter extends FullBackupBase {
statement.append('(');
for (int i=0;i<cursor.getColumnCount();i++) {
for (int i = 0; i < cursor.getColumnCount(); i++) {
statement.append('?');
if (cursor.getType(i) == Cursor.FIELD_TYPE_STRING) {
@@ -329,10 +329,10 @@ public class FullBackupExporter extends FullBackupBase {
} else if (cursor.getType(i) == Cursor.FIELD_TYPE_NULL) {
statementBuilder.addParameters(BackupProtos.SqlStatement.SqlParameter.newBuilder().setNullparameter(true));
} else {
throw new AssertionError("unknown type?" + cursor.getType(i));
throw new AssertionError("unknown type?" + cursor.getType(i));
}
if (i < cursor.getColumnCount()-1) {
if (i < cursor.getColumnCount() - 1) {
statement.append(',');
}
}
@@ -352,80 +352,96 @@ public class FullBackupExporter extends FullBackupBase {
return count;
}
private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) {
try {
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE));
private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret,
@NonNull Cursor cursor,
@NonNull BackupFrameOutputStream outputStream,
int count,
long estimatedCount)
throws IOException
{
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE));
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM));
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentDatabase.DATA_RANDOM));
if (!TextUtils.isEmpty(data)) {
long fileLength = new File(data).length();
long dbLength = size;
if (!TextUtils.isEmpty(data)) {
long fileLength = new File(data).length();
long dbLength = size;
if (size <= 0 || fileLength != dbLength) {
size = calculateVeryOldStreamLength(attachmentSecret, random, data);
Log.w(TAG, "Needed size calculation! Manual: " + size + " File: " + fileLength + " DB: " + dbLength + " ID: " + new AttachmentId(rowId, uniqueId));
}
if (size <= 0 || fileLength != dbLength) {
size = calculateVeryOldStreamLength(attachmentSecret, random, data);
Log.w(TAG, "Needed size calculation! Manual: " + size + " File: " + fileLength + " DB: " + dbLength + " ID: " + new AttachmentId(rowId, uniqueId));
}
}
if (!TextUtils.isEmpty(data) && size > 0) {
InputStream inputStream;
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
if (!TextUtils.isEmpty(data) && size > 0) {
try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) {
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
inputStream.close();
} catch (FileNotFoundException e) {
Log.w(TAG, "Missing attachment: " + e.getMessage());
}
} catch (IOException e) {
Log.w(TAG, e);
}
return count;
}
private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) {
try {
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
private static int exportSticker(@NonNull AttachmentSecret attachmentSecret,
@NonNull Cursor cursor,
@NonNull BackupFrameOutputStream outputStream,
int count,
long estimatedCount)
throws IOException
{
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
String data = cursor.getString(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_PATH));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
if (!TextUtils.isEmpty(data) && size > 0) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
outputStream.writeSticker(rowId, inputStream, size);
}
if (!TextUtils.isEmpty(data) && size > 0) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
outputStream.writeSticker(rowId, inputStream, size);
} catch (FileNotFoundException e) {
Log.w(TAG, "Missing sticker: " + e.getMessage());
}
} catch (IOException e) {
Log.w(TAG, e);
}
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;
InputStream inputStream;
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) {
int read;
byte[] buffer = new byte[8192];
int read;
byte[] buffer = new byte[8192];
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
result += read;
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
result += read;
}
} catch (FileNotFoundException e) {
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;
}
private static InputStream openAttachmentStream(@NonNull AttachmentSecret attachmentSecret, @Nullable byte[] random, @NonNull String data) throws IOException {
if (random != null && random.length == 32) {
return ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
} else {
return ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
}
}
private static int exportKeyValues(@NonNull BackupFrameOutputStream outputStream,
@NonNull List<String> keysToIncludeInBackup,
int count,
@@ -479,7 +495,7 @@ public class FullBackupExporter extends FullBackupBase {
private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) {
return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 &&
cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
}
private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) {
@@ -488,7 +504,7 @@ public class FullBackupExporter extends FullBackupBase {
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull MessageId messageId) {
if (messageId.isMms()) {
return isForNonExpiringMmsMessageAndNotReleaseChannel(db, messageId.getId());
return isForNonExpiringMmsMessage(db, messageId.getId());
} else {
return isForNonExpiringSmsMessage(db, messageId.getId());
}
@@ -508,46 +524,39 @@ public class FullBackupExporter extends FullBackupBase {
return false;
}
private static boolean isForNonExpiringMmsMessageAndNotReleaseChannel(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MmsDatabase.RECIPIENT_ID, MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE};
private static boolean isForNonExpiringMmsMessage(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MmsDatabase.RECIPIENT_ID, MmsDatabase.EXPIRES_IN, MmsDatabase.VIEW_ONCE };
String where = MmsDatabase.ID + " = ?";
String[] args = new String[] { String.valueOf(mmsId) };
try (Cursor mmsCursor = db.query(MmsDatabase.TABLE_NAME, columns, where, args, null, null, null)) {
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return isNonExpiringMmsMessage(mmsCursor) && isNotReleaseChannel(mmsCursor);
return isNonExpiringMmsMessage(mmsCursor);
}
}
return false;
}
private static boolean isNotReleaseChannel(Cursor cursor) {
RecipientId releaseChannel = SignalStore.releaseChannelValues().getReleaseChannelRecipientId();
return releaseChannel == null || cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.RECIPIENT_ID)) != releaseChannel.toLong();
}
private static class BackupFrameOutputStream extends BackupStream {
private final OutputStream outputStream;
private final Cipher cipher;
private final Mac mac;
private final byte[] cipherKey;
private final byte[] macKey;
private byte[] iv;
private int counter;
private final byte[] cipherKey;
private final byte[] iv;
private int counter;
private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException {
try {
byte[] salt = Util.getSecretBytes(32);
byte[] key = getBackupKey(passphrase, salt);
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64);
byte[] derived = HKDF.deriveSecrets(key, "Backup Export".getBytes(), 64);
byte[][] split = ByteUtil.split(derived, 32, 32);
this.cipherKey = split[0];
this.macKey = split[1];
byte[] macKey = split[1];
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
this.mac = Mac.getInstance("HmacSHA256");
@@ -582,12 +591,17 @@ public class FullBackupExporter extends FullBackupBase {
}
public void write(@NonNull String avatarName, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAvatar(BackupProtos.Avatar.newBuilder()
.setRecipientId(avatarName)
.setLength(Util.toIntExact(size))
.build())
.build());
try {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAvatar(BackupProtos.Avatar.newBuilder()
.setRecipientId(avatarName)
.setLength(Util.toIntExact(size))
.build())
.build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write avatar to backup", e);
throw new InvalidBackupStreamException();
}
if (writeStream(in) != size) {
throw new IOException("Size mismatch!");
@@ -595,13 +609,18 @@ public class FullBackupExporter extends FullBackupBase {
}
public void write(@NonNull AttachmentId attachmentId, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAttachment(BackupProtos.Attachment.newBuilder()
.setRowId(attachmentId.getRowId())
.setAttachmentId(attachmentId.getUniqueId())
.setLength(Util.toIntExact(size))
.build())
.build());
try {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setAttachment(BackupProtos.Attachment.newBuilder()
.setRowId(attachmentId.getRowId())
.setAttachmentId(attachmentId.getUniqueId())
.setLength(Util.toIntExact(size))
.build())
.build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write " + attachmentId + " to backup", e);
throw new InvalidBackupStreamException();
}
if (writeStream(in) != size) {
throw new IOException("Size mismatch!");
@@ -609,12 +628,17 @@ public class FullBackupExporter extends FullBackupBase {
}
public void writeSticker(long rowId, @NonNull InputStream in, long size) throws IOException {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setSticker(BackupProtos.Sticker.newBuilder()
.setRowId(rowId)
.setLength(Util.toIntExact(size))
.build())
.build());
try {
write(outputStream, BackupProtos.BackupFrame.newBuilder()
.setSticker(BackupProtos.Sticker.newBuilder()
.setRowId(rowId)
.setLength(Util.toIntExact(size))
.build())
.build());
} catch (ArithmeticException e) {
Log.w(TAG, "Unable to write sticker to backup", e);
throw new InvalidBackupStreamException();
}
if (writeStream(in) != size) {
throw new IOException("Size mismatch!");
@@ -688,17 +712,20 @@ public class FullBackupExporter extends FullBackupBase {
public void close() throws IOException {
outputStream.flush();
outputStream.close();
}
}
public interface PostProcessor {
int postProcess(@NonNull Cursor cursor, int count);
int postProcess(@NonNull Cursor cursor, int count) throws IOException;
}
public interface BackupCancellationSignal {
boolean isCanceled();
}
public static final class BackupCanceledException extends IOException { }
public static final class BackupCanceledException extends IOException {}
public static final class InvalidBackupStreamException extends IOException {}
}

View File

@@ -14,11 +14,8 @@ import androidx.annotation.NonNull;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.SqlUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.kdf.HKDFv3;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment;
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion;
@@ -38,7 +35,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.signal.core.util.SqlUtil;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -46,24 +42,12 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class FullBackupImporter extends FullBackupBase {
@SuppressWarnings("unused")
@@ -185,7 +169,7 @@ public class FullBackupImporter extends FullBackupBase {
contentValues.put(AttachmentDatabase.DATA, dataFile.getAbsolutePath());
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first);
} catch (BadMacException e) {
} catch (BackupRecordInputStream.BadMacException e) {
Log.w(TAG, "Bad MAC for attachment " + attachment.getAttachmentId() + "! Can't restore it.", e);
dataFile.delete();
contentValues.put(AttachmentDatabase.DATA, (String) null);
@@ -301,144 +285,6 @@ public class FullBackupImporter extends FullBackupBase {
}
}
private static class BackupRecordInputStream extends BackupStream {
private final InputStream in;
private final Cipher cipher;
private final Mac mac;
private final byte[] cipherKey;
private final byte[] macKey;
private byte[] iv;
private int counter;
private BackupRecordInputStream(@NonNull InputStream in, @NonNull String passphrase) throws IOException {
try {
this.in = in;
byte[] headerLengthBytes = new byte[4];
StreamUtil.readFully(in, headerLengthBytes);
int headerLength = Conversions.byteArrayToInt(headerLengthBytes);
byte[] headerFrame = new byte[headerLength];
StreamUtil.readFully(in, headerFrame);
BackupFrame frame = BackupFrame.parseFrom(headerFrame);
if (!frame.hasHeader()) {
throw new IOException("Backup stream does not start with header!");
}
BackupProtos.Header header = frame.getHeader();
this.iv = header.getIv().toByteArray();
if (iv.length != 16) {
throw new IOException("Invalid IV length!");
}
byte[] key = getBackupKey(passphrase, header.hasSalt() ? header.getSalt().toByteArray() : null);
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64);
byte[][] split = ByteUtil.split(derived, 32, 32);
this.cipherKey = split[0];
this.macKey = split[1];
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
this.mac = Mac.getInstance("HmacSHA256");
this.mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
this.counter = Conversions.byteArrayToInt(iv);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
BackupFrame readFrame() throws IOException {
return readFrame(in);
}
void readAttachmentTo(OutputStream out, int length) throws IOException {
try {
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
mac.update(iv);
byte[] buffer = new byte[8192];
while (length > 0) {
int read = in.read(buffer, 0, Math.min(buffer.length, length));
if (read == -1) throw new IOException("File ended early!");
mac.update(buffer, 0, read);
byte[] plaintext = cipher.update(buffer, 0, read);
if (plaintext != null) {
out.write(plaintext, 0, plaintext.length);
}
length -= read;
}
byte[] plaintext = cipher.doFinal();
if (plaintext != null) {
out.write(plaintext, 0, plaintext.length);
}
out.close();
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
byte[] theirMac = new byte[10];
try {
StreamUtil.readFully(in, theirMac);
} catch (IOException e) {
throw new IOException(e);
}
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new BadMacException();
}
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
private BackupFrame readFrame(InputStream in) throws IOException {
try {
byte[] length = new byte[4];
StreamUtil.readFully(in, length);
byte[] frame = new byte[Conversions.byteArrayToInt(length)];
StreamUtil.readFully(in, frame);
byte[] theirMac = new byte[10];
System.arraycopy(frame, frame.length - 10, theirMac, 0, theirMac.length);
mac.update(frame, 0, frame.length - 10);
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new IOException("Bad MAC");
}
Conversions.intToByteArray(iv, 0, counter++);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
byte[] plaintext = cipher.doFinal(frame, 0, frame.length - 10);
return BackupFrame.parseFrom(plaintext);
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}
}
}
private static class BadMacException extends IOException {}
public static class DatabaseDowngradeException extends IOException {
DatabaseDowngradeException(int currentVersion, int backupVersion) {
super("Tried to import a backup with version " + backupVersion + " into a database with version " + currentVersion);

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

View File

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

View File

@@ -36,6 +36,10 @@ object GiftRowItem {
private val taglineView = itemView.findViewById<TextView>(R.id.tagline)
private val priceView = itemView.findViewById<TextView>(R.id.price)
init {
itemView.isSelected = true
}
override fun bind(model: Model) {
checkView.visible = false
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.BadgePreview
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.LifecycleDisposable
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.
@@ -50,7 +50,7 @@ class SelectFeaturedBadgeFragment : DSLSettingsFragment(
return Material3OnScrollHelper(requireActivity(), scrollShadow)
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
override fun bindAdapter(adapter: MappingAdapter) {
Badge.register(adapter) { badge, isSelected, _ ->
if (!isSelected) {
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.view.ViewBadgeBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.SubscriptionsRepository
@@ -18,6 +17,7 @@ import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
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 ->
if (badge.isExpired() || isFaded) {
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.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -68,10 +67,7 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
action.setOnClickListener {
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url))
}
} else if (
FeatureFlags.donorBadges() &&
Recipient.self().badges.none { it.category == Badge.Category.Donor && !it.isBoost() && !it.isExpired() }
) {
} else if (Recipient.self().badges.none { it.category == Badge.Category.Donor && !it.isBoost() && !it.isExpired() }) {
action.setOnClickListener {
startActivity(AppSettingsActivity.subscriptions(requireContext()))
}
@@ -143,10 +139,6 @@ class ViewBadgeBottomSheetDialogFragment : FixedRoundedCornerBottomSheetDialogFr
recipientId: RecipientId,
startBadge: Badge? = null
) {
if (!FeatureFlags.displayDonorBadges() && recipientId != Recipient.self().id) {
return
}
ViewBadgeBottomSheetDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_START_BADGE, startBadge)

View File

@@ -10,7 +10,7 @@ import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
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.snackbar.Snackbar;
@@ -48,7 +48,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
BlockedUsersRepository repository = new BlockedUsersRepository(this);
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);
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.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
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 -> {
if (list.isEmpty()) {
empty.setVisibility(View.VISIBLE);

View File

@@ -17,6 +17,7 @@ import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.lifecycle.Observer;
import com.airbnb.lottie.LottieAnimationView;
@@ -126,6 +127,11 @@ public final class AudioView extends FrameLayout {
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.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, 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 @NonNull Drawable unknownRecipientDrawable;
private @Nullable AvatarColor fallbackPhotoColor;
public AvatarImageView(Context context) {
super(context);
@@ -105,6 +106,10 @@ public final class AvatarImageView extends AppCompatImageView {
this.fallbackPhotoProvider = fallbackPhotoProvider;
}
public void setFallbackPhotoColor(@Nullable AvatarColor fallbackPhotoColor) {
this.fallbackPhotoColor = fallbackPhotoColor;
}
/**
* Shows self as the actual profile picture.
*/
@@ -213,7 +218,7 @@ public final class AvatarImageView extends AppCompatImageView {
requestManager.clear(this);
if (fallbackPhotoProvider != null) {
setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName()
.asDrawable(getContext(), AvatarColor.UNKNOWN, inverted));
.asDrawable(getContext(), Util.firstNonNull(fallbackPhotoColor, AvatarColor.UNKNOWN), inverted));
} else {
setImageDrawable(unknownRecipientDrawable);
}

View File

@@ -46,13 +46,17 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
public class ComposeText extends EmojiEditText {
private static final char EMOJI_STARTER = ':';
private static final long EMOJI_KEYWORD_DELAY = TimeUnit.SECONDS.toMillis(1);
private static final long EMOJI_KEYWORD_DELAY = 1500;
private static final Pattern TIME_PATTERN = Pattern.compile("^[0-9]{1,2}:[0-9]{1,2}$");
private CharSequence hint;
private SpannableString subHint;
@@ -322,11 +326,9 @@ public class ComposeText extends EmojiEditText {
}
private void doAfterCursorChange(@NonNull Editable text) {
removeCallbacks(keywordSearchRunnable);
if (enoughToFilter(text, false)) {
performFiltering(text, false);
} else {
postDelayed(keywordSearchRunnable, EMOJI_KEYWORD_DELAY);
clearInlineQuery();
}
}
@@ -424,16 +426,42 @@ public class ComposeText extends EmojiEditText {
}
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--;
}
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == starter) {
return delimiterSearchIndex + 1;
if (couldBeTimeEntry(text, delimiterSearchIndex)) {
return -1;
} else {
return delimiterSearchIndex + 1;
}
}
return -1;
}
/**
* Return true if we think the user may be inputting a time.
*/
private static boolean couldBeTimeEntry(@NonNull CharSequence text, int startIndex) {
if (startIndex <= 0 || startIndex + 1 >= text.length()) {
return false;
}
int startOfToken = startIndex;
while (startOfToken > 0 && !Character.isWhitespace(text.charAt(startOfToken))) {
startOfToken--;
}
startOfToken++;
int endOfToken = startIndex;
while (endOfToken < text.length() && !Character.isWhitespace(text.charAt(endOfToken))) {
endOfToken++;
}
return TIME_PATTERN.matcher(text.subSequence(startOfToken, endOfToken)).find();
}
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
private static final String TAG = Log.tag(CommitContentListener.class);

View File

@@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
@@ -210,6 +211,10 @@ public class InputPanel extends LinearLayout
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
this.linkPreview.setCorners(cornerRadius, cornerRadius);
}
if (listener != null) {
listener.onQuoteChanged(id, author.getId());
}
}
public void clearQuote() {
@@ -230,6 +235,10 @@ public class InputPanel extends LinearLayout
});
quoteAnimator.start();
if (listener != null) {
listener.onQuoteCleared();
}
}
private static ValueAnimator createHeightAnimator(@NonNull View view,
@@ -572,6 +581,8 @@ public class InputPanel extends LinearLayout
void onEmojiToggle();
void onLinkPreviewCanceled();
void onStickerSuggestionSelected(@NonNull StickerRecord sticker);
void onQuoteChanged(long id, @NonNull RecipientId author);
void onQuoteCleared();
}
private static class SlideToCancel {

View File

@@ -80,6 +80,10 @@ class Material3SearchToolbar @JvmOverloads constructor(
}
}
fun clearText() {
input.setText("")
}
interface Listener {
fun onSearchTextChange(text: String)
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

@@ -1,33 +0,0 @@
package org.thoughtcrime.securesms.components.camera;
import android.content.Context;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class CameraSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
private boolean ready;
@SuppressWarnings("deprecation")
public CameraSurfaceView(Context context) {
super(context);
getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
getHolder().addCallback(this);
}
public boolean isReady() {
return ready;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
ready = true;
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
ready = false;
}
}

View File

@@ -1,110 +0,0 @@
package org.thoughtcrime.securesms.components.camera;
import android.app.Activity;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.util.DisplayMetrics;
import android.view.Surface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
@SuppressWarnings("deprecation")
public class CameraUtils {
private static final String TAG = Log.tag(CameraUtils.class);
/*
* modified from: https://github.com/commonsguy/cwac-camera/blob/master/camera/src/com/commonsware/cwac/camera/CameraUtils.java
*/
public static @Nullable Size getPreferredPreviewSize(int displayOrientation,
int width,
int height,
@NonNull Parameters parameters) {
final int targetWidth = displayOrientation % 180 == 90 ? height : width;
final int targetHeight = displayOrientation % 180 == 90 ? width : height;
final double targetRatio = (double) targetWidth / targetHeight;
Log.d(TAG, String.format(Locale.US,
"getPreferredPreviewSize(%d, %d, %d) -> target %dx%d, AR %.02f",
displayOrientation, width, height,
targetWidth, targetHeight, targetRatio));
List<Size> sizes = parameters.getSupportedPreviewSizes();
List<Size> ideals = new LinkedList<>();
List<Size> bigEnough = new LinkedList<>();
for (Size size : sizes) {
Log.d(TAG, String.format(Locale.US, " %dx%d (%.02f)", size.width, size.height, (float)size.width / size.height));
if (size.height == size.width * targetRatio && size.height >= targetHeight && size.width >= targetWidth) {
ideals.add(size);
Log.d(TAG, " (ideal ratio)");
} else if (size.width >= targetWidth && size.height >= targetHeight) {
bigEnough.add(size);
Log.d(TAG, " (good size, suboptimal ratio)");
}
}
if (!ideals.isEmpty()) return Collections.min(ideals, new AreaComparator());
else if (!bigEnough.isEmpty()) return Collections.min(bigEnough, new AspectRatioComparator(targetRatio));
else return Collections.max(sizes, new AreaComparator());
}
// based on
// http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
// and http://stackoverflow.com/a/10383164/115145
public static int getCameraDisplayOrientation(@NonNull Activity activity,
@NonNull CameraInfo info)
{
int rotation = activity.getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
DisplayMetrics dm = new DisplayMetrics();
activity.getWindowManager().getDefaultDisplay().getMetrics(dm);
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
return (360 - ((info.orientation + degrees) % 360)) % 360;
} else {
return (info.orientation - degrees + 360) % 360;
}
}
private static class AreaComparator implements Comparator<Size> {
@Override
public int compare(Size lhs, Size rhs) {
return Long.signum(lhs.width * lhs.height - rhs.width * rhs.height);
}
}
private static class AspectRatioComparator extends AreaComparator {
private final double target;
public AspectRatioComparator(double target) {
this.target = target;
}
@Override
public int compare(Size lhs, Size rhs) {
final double lhsDiff = Math.abs(target - (double) lhs.width / lhs.height);
final double rhsDiff = Math.abs(target - (double) rhs.width / rhs.height);
if (lhsDiff < rhsDiff) return -1;
else if (lhsDiff > rhsDiff) return 1;
else return super.compare(lhs, rhs);
}
}
}

View File

@@ -1,575 +0,0 @@
/***
Copyright (c) 2013-2014 CommonsWare, LLC
Portions Copyright (C) 2007 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.thoughtcrime.securesms.components.camera;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Build.VERSION;
import android.util.AttributeSet;
import android.view.OrientationEventListener;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.signal.qr.kitkat.QrCameraView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
@SuppressWarnings("deprecation")
public class CameraView extends ViewGroup {
private static final String TAG = Log.tag(CameraView.class);
private final CameraSurfaceView surface;
private final OnOrientationChange onOrientationChange;
private volatile Optional<Camera> camera = Optional.empty();
private volatile int cameraId = CameraInfo.CAMERA_FACING_BACK;
private volatile int displayOrientation = -1;
private @NonNull State state = State.PAUSED;
private @Nullable Size previewSize;
private @NonNull List<CameraViewListener> listeners = Collections.synchronizedList(new LinkedList<CameraViewListener>());
private int outputOrientation = -1;
public CameraView(Context context) {
this(context, null);
}
public CameraView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CameraView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setBackgroundColor(Color.BLACK);
if (attrs != null) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
int camera = typedArray.getInt(R.styleable.CameraView_camera, -1);
if (camera != -1) cameraId = camera;
else if (isMultiCamera()) cameraId = TextSecurePreferences.getDirectCaptureCameraId(context);
typedArray.recycle();
}
surface = new CameraSurfaceView(getContext());
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
addView(surface);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void onResume() {
if (state != State.PAUSED) return;
state = State.RESUMED;
Log.i(TAG, "onResume() queued");
enqueueTask(new SerialAsyncTask<Void>() {
@Override
protected
@Nullable
Void onRunBackground() {
try {
long openStartMillis = System.currentTimeMillis();
camera = Optional.ofNullable(Camera.open(cameraId));
Log.i(TAG, "camera.open() -> " + (System.currentTimeMillis() - openStartMillis) + "ms");
synchronized (CameraView.this) {
CameraView.this.notifyAll();
}
if (camera.isPresent()) onCameraReady(camera.get());
} catch (Exception e) {
Log.w(TAG, e);
}
return null;
}
@Override
protected void onPostMain(Void avoid) {
if (!camera.isPresent()) {
Log.w(TAG, "tried to open camera but got null");
for (CameraViewListener listener : listeners) {
listener.onCameraFail();
}
return;
}
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
onOrientationChange.enable();
}
Log.i(TAG, "onResume() completed");
}
});
}
public void onPause() {
if (state == State.PAUSED) return;
state = State.PAUSED;
Log.i(TAG, "onPause() queued");
enqueueTask(new SerialAsyncTask<Void>() {
private Optional<Camera> cameraToDestroy;
@Override
protected void onPreMain() {
cameraToDestroy = camera;
camera = Optional.empty();
}
@Override
protected Void onRunBackground() {
if (cameraToDestroy.isPresent()) {
try {
stopPreview();
cameraToDestroy.get().setPreviewCallback(null);
cameraToDestroy.get().release();
Log.w(TAG, "released old camera instance");
} catch (Exception e) {
Log.w(TAG, e);
}
}
return null;
}
@Override protected void onPostMain(Void avoid) {
onOrientationChange.disable();
displayOrientation = -1;
outputOrientation = -1;
removeView(surface);
addView(surface);
Log.i(TAG, "onPause() completed");
}
});
for (CameraViewListener listener : listeners) {
listener.onCameraStop();
}
}
public boolean isStarted() {
return state != State.PAUSED;
}
@SuppressWarnings("SuspiciousNameCombination")
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = r - l;
final int height = b - t;
final int previewWidth;
final int previewHeight;
if (camera.isPresent() && previewSize != null) {
if (displayOrientation == 90 || displayOrientation == 270) {
previewWidth = previewSize.height;
previewHeight = previewSize.width;
} else {
previewWidth = previewSize.width;
previewHeight = previewSize.height;
}
} else {
previewWidth = width;
previewHeight = height;
}
if (previewHeight == 0 || previewWidth == 0) {
Log.w(TAG, "skipping layout due to zero-width/height preview size");
return;
}
if (width * previewHeight > height * previewWidth) {
final int scaledChildHeight = previewHeight * width / previewWidth;
surface.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2);
} else {
final int scaledChildWidth = previewWidth * height / previewHeight;
surface.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
Log.i(TAG, "onSizeChanged(" + oldw + "x" + oldh + " -> " + w + "x" + h + ")");
super.onSizeChanged(w, h, oldw, oldh);
if (camera.isPresent()) startPreview(camera.get().getParameters());
}
public void addListener(@NonNull CameraViewListener listener) {
listeners.add(listener);
}
public void setPreviewCallback(final @NonNull QrCameraView.PreviewCallback previewCallback) {
enqueueTask(new PostInitializationTask<Void>() {
@Override
protected void onPostMain(Void avoid) {
if (camera.isPresent()) {
camera.get().setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if (!CameraView.this.camera.isPresent()) {
return;
}
final int rotation = getCameraPictureOrientation();
final Size previewSize = camera.getParameters().getPreviewSize();
if (data != null) {
previewCallback.onPreviewFrame(new QrCameraView.PreviewFrame(data, previewSize.width, previewSize.height, rotation));
}
}
});
}
}
});
}
public boolean isMultiCamera() {
return Camera.getNumberOfCameras() > 1;
}
public boolean isRearCamera() {
return cameraId == CameraInfo.CAMERA_FACING_BACK;
}
public void flipCamera() {
if (Camera.getNumberOfCameras() > 1) {
cameraId = cameraId == CameraInfo.CAMERA_FACING_BACK
? CameraInfo.CAMERA_FACING_FRONT
: CameraInfo.CAMERA_FACING_BACK;
onPause();
onResume();
TextSecurePreferences.setDirectCaptureCameraId(getContext(), cameraId);
}
}
@TargetApi(14)
private void onCameraReady(final @NonNull Camera camera) {
final Parameters parameters = camera.getParameters();
if (VERSION.SDK_INT >= 14) {
parameters.setRecordingHint(true);
final List<String> focusModes = parameters.getSupportedFocusModes();
if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
} else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
}
displayOrientation = CameraUtils.getCameraDisplayOrientation(getActivity(), getCameraInfo());
camera.setDisplayOrientation(displayOrientation);
camera.setParameters(parameters);
enqueueTask(new PostInitializationTask<Void>() {
@Override
protected Void onRunBackground() {
try {
camera.setPreviewDisplay(surface.getHolder());
startPreview(parameters);
} catch (Exception e) {
Log.w(TAG, "couldn't set preview display", e);
}
return null;
}
});
}
private void startPreview(final @NonNull Parameters parameters) {
if (this.camera.isPresent()) {
try {
final Camera camera = this.camera.get();
final Size preferredPreviewSize = getPreferredPreviewSize(parameters);
if (preferredPreviewSize != null && !parameters.getPreviewSize().equals(preferredPreviewSize)) {
Log.i(TAG, "starting preview with size " + preferredPreviewSize.width + "x" + preferredPreviewSize.height);
if (state == State.ACTIVE) stopPreview();
previewSize = preferredPreviewSize;
parameters.setPreviewSize(preferredPreviewSize.width, preferredPreviewSize.height);
camera.setParameters(parameters);
} else {
previewSize = parameters.getPreviewSize();
}
long previewStartMillis = System.currentTimeMillis();
camera.startPreview();
Log.i(TAG, "camera.startPreview() -> " + (System.currentTimeMillis() - previewStartMillis) + "ms");
state = State.ACTIVE;
ThreadUtil.runOnMain(new Runnable() {
@Override
public void run() {
requestLayout();
for (CameraViewListener listener : listeners) {
listener.onCameraStart();
}
}
});
} catch (Exception e) {
Log.w(TAG, e);
}
}
}
private void stopPreview() {
if (camera.isPresent()) {
try {
camera.get().stopPreview();
state = State.RESUMED;
} catch (Exception e) {
Log.w(TAG, e);
}
}
}
private Size getPreferredPreviewSize(@NonNull Parameters parameters) {
return CameraUtils.getPreferredPreviewSize(displayOrientation,
getMeasuredWidth(),
getMeasuredHeight(),
parameters);
}
private int getCameraPictureOrientation() {
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
outputOrientation = getCameraPictureRotation(getActivity().getWindowManager()
.getDefaultDisplay()
.getOrientation());
} else if (getCameraInfo().facing == CameraInfo.CAMERA_FACING_FRONT) {
outputOrientation = (360 - displayOrientation) % 360;
} else {
outputOrientation = displayOrientation;
}
return outputOrientation;
}
// https://github.com/signalapp/Signal-Android/issues/4715
private boolean isTroublemaker() {
return getCameraInfo().facing == CameraInfo.CAMERA_FACING_FRONT &&
"JWR66Y".equals(Build.DISPLAY) &&
"yakju".equals(Build.PRODUCT);
}
private @NonNull CameraInfo getCameraInfo() {
final CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
return info;
}
// XXX this sucks
private Activity getActivity() {
return (Activity)getContext();
}
public int getCameraPictureRotation(int orientation) {
final CameraInfo info = getCameraInfo();
final int rotation;
orientation = (orientation + 45) / 90 * 90;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
rotation = (info.orientation - orientation + 360) % 360;
} else {
rotation = (info.orientation + orientation) % 360;
}
return rotation;
}
private class OnOrientationChange extends OrientationEventListener {
public OnOrientationChange(Context context) {
super(context);
disable();
}
@Override
public void onOrientationChanged(int orientation) {
if (camera.isPresent() && orientation != ORIENTATION_UNKNOWN) {
int newOutputOrientation = getCameraPictureRotation(orientation);
if (newOutputOrientation != outputOrientation) {
outputOrientation = newOutputOrientation;
Camera.Parameters params = camera.get().getParameters();
params.setRotation(outputOrientation);
try {
camera.get().setParameters(params);
}
catch (Exception e) {
Log.e(TAG, "Exception updating camera parameters in orientation change", e);
}
}
}
}
}
public void takePicture(final Rect previewRect) {
if (!camera.isPresent() || camera.get().getParameters() == null) {
Log.w(TAG, "camera not in capture-ready state");
return;
}
camera.get().setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, final Camera camera) {
final int rotation = getCameraPictureOrientation();
final Size previewSize = camera.getParameters().getPreviewSize();
final Rect croppingRect = getCroppedRect(previewSize, previewRect, rotation);
Log.i(TAG, "previewSize: " + previewSize.width + "x" + previewSize.height);
Log.i(TAG, "data bytes: " + data.length);
Log.i(TAG, "previewFormat: " + camera.getParameters().getPreviewFormat());
Log.i(TAG, "croppingRect: " + croppingRect.toString());
Log.i(TAG, "rotation: " + rotation);
new CaptureTask(previewSize, rotation, croppingRect).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, data);
}
});
}
private Rect getCroppedRect(Size cameraPreviewSize, Rect visibleRect, int rotation) {
final int previewWidth = cameraPreviewSize.width;
final int previewHeight = cameraPreviewSize.height;
if (rotation % 180 > 0) rotateRect(visibleRect);
float scale = (float) previewWidth / visibleRect.width();
if (visibleRect.height() * scale > previewHeight) {
scale = (float) previewHeight / visibleRect.height();
}
final float newWidth = visibleRect.width() * scale;
final float newHeight = visibleRect.height() * scale;
final float centerX = (VERSION.SDK_INT < 14 || isTroublemaker()) ? previewWidth - newWidth / 2 : previewWidth / 2;
final float centerY = previewHeight / 2;
visibleRect.set((int) (centerX - newWidth / 2),
(int) (centerY - newHeight / 2),
(int) (centerX + newWidth / 2),
(int) (centerY + newHeight / 2));
if (rotation % 180 > 0) rotateRect(visibleRect);
return visibleRect;
}
@SuppressWarnings("SuspiciousNameCombination")
private void rotateRect(Rect rect) {
rect.set(rect.top, rect.left, rect.bottom, rect.right);
}
private void enqueueTask(SerialAsyncTask job) {
AsyncTask.SERIAL_EXECUTOR.execute(job);
}
public static abstract class SerialAsyncTask<Result> implements Runnable {
@Override
public final void run() {
if (!onWait()) {
Log.w(TAG, "skipping task, preconditions not met in onWait()");
return;
}
ThreadUtil.runOnMainSync(this::onPreMain);
final Result result = onRunBackground();
ThreadUtil.runOnMainSync(() -> onPostMain(result));
}
protected boolean onWait() { return true; }
protected void onPreMain() {}
protected Result onRunBackground() { return null; }
protected void onPostMain(Result result) {}
}
private abstract class PostInitializationTask<Result> extends SerialAsyncTask<Result> {
@Override protected boolean onWait() {
synchronized (CameraView.this) {
if (!camera.isPresent()) {
return false;
}
while (getMeasuredHeight() <= 0 || getMeasuredWidth() <= 0 || !surface.isReady()) {
Log.i(TAG, String.format("waiting. surface ready? %s", surface.isReady()));
Util.wait(CameraView.this, 0);
}
return true;
}
}
}
private class CaptureTask extends AsyncTask<byte[], Void, byte[]> {
private final Size previewSize;
private final int rotation;
private final Rect croppingRect;
public CaptureTask(Size previewSize, int rotation, Rect croppingRect) {
this.previewSize = previewSize;
this.rotation = rotation;
this.croppingRect = croppingRect;
}
@Override
protected byte[] doInBackground(byte[]... params) {
final byte[] data = params[0];
try {
return BitmapUtil.createFromNV21(data,
previewSize.width,
previewSize.height,
rotation,
croppingRect,
cameraId == CameraInfo.CAMERA_FACING_FRONT);
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
@Override
protected void onPostExecute(byte[] imageBytes) {
if (imageBytes != null) {
for (CameraViewListener listener : listeners) {
listener.onImageCapture(imageBytes);
}
}
}
}
private static class PreconditionsNotMetException extends Exception {}
public interface CameraViewListener {
void onImageCapture(@NonNull final byte[] imageBytes);
void onCameraFail();
void onCameraStart();
void onCameraStop();
}
private enum State {
PAUSED, RESUMED, ACTIVE
}
}

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

View File

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

View File

@@ -1,10 +1,12 @@
package org.thoughtcrime.securesms.components.sensors;
import android.content.ContentResolver;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.provider.Settings;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
@@ -22,8 +24,9 @@ public final class DeviceOrientationMonitor implements DefaultLifecycleObserver
private static final float LANDSCAPE_PITCH_MINIMUM = -0.5f;
private static final float LANDSCAPE_PITCH_MAXIMUM = 0.5f;
private final SensorManager sensorManager;
private final EventListener eventListener = new EventListener();
private final SensorManager sensorManager;
private final ContentResolver contentResolver;
private final EventListener eventListener = new EventListener();
private final float[] accelerometerReading = new float[3];
private final float[] magnetometerReading = new float[3];
@@ -34,7 +37,8 @@ public final class DeviceOrientationMonitor implements DefaultLifecycleObserver
private final MutableLiveData<Orientation> orientation = new MutableLiveData<>(Orientation.PORTRAIT_BOTTOM_EDGE);
public DeviceOrientationMonitor(@NonNull Context context) {
this.sensorManager = ServiceUtil.getSensorManager(context);
this.sensorManager = ServiceUtil.getSensorManager(context);
this.contentResolver = context.getContentResolver();
}
@Override
@@ -65,6 +69,12 @@ public final class DeviceOrientationMonitor implements DefaultLifecycleObserver
}
private void updateOrientationAngles() {
int rotationLocked = Settings.System.getInt(contentResolver, Settings.System.ACCELEROMETER_ROTATION, -1);
if (rotationLocked == 0) {
orientation.setValue(Orientation.PORTRAIT_BOTTOM_EDGE);
return;
}
boolean success = SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerReading, magnetometerReading);
if (!success) {
SensorUtil.getRotationMatrixWithoutMagneticSensorData(rotationMatrix, accelerometerReading);

View File

@@ -12,10 +12,13 @@ import androidx.annotation.StringRes
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import java.lang.UnsupportedOperationException
abstract class DSLSettingsFragment(
@StringRes private val titleId: Int = -1,
@@ -27,9 +30,11 @@ abstract class DSLSettingsFragment(
protected var recyclerView: RecyclerView? = null
private set
private var toolbar: Toolbar? = null
@CallSuper
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar? = view.findViewById(R.id.toolbar)
toolbar = view.findViewById(R.id.toolbar)
if (titleId != -1) {
toolbar?.setTitle(titleId)
@@ -44,7 +49,13 @@ abstract class DSLSettingsFragment(
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 {
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? {
@@ -76,7 +91,25 @@ abstract class DSLSettingsFragment(
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() {
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.components.AvatarImageView
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.DSLSettingsIcon
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.Util
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.navigation.safeNavigate
@@ -30,7 +30,7 @@ class AppSettingsFragment : DSLSettingsFragment(R.string.text_secure_normal__men
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(PaymentsPreference::class.java, LayoutFactory(::PaymentsPreferenceViewHolder, R.layout.dsl_payments_preference))
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(
title = DSLSettingsText.from(R.string.preferences__donate_to_signal),
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 org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
@@ -33,6 +32,7 @@ import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
import org.thoughtcrime.securesms.pin.RegistrationLockV2Dialog
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFragment__account) {
@@ -50,7 +50,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
viewModel.refreshState()
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
override fun bindAdapter(adapter: MappingAdapter) {
viewModel = ViewModelProvider(this)[AccountSettingsViewModel::class.java]
viewModel.state.observe(viewLifecycleOwner) { state ->

View File

@@ -4,11 +4,11 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.keyvalue.SettingsValues
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
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 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.state.observe(viewLifecycleOwner) { state ->

View File

@@ -5,8 +5,12 @@ import android.view.View
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
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.R
import org.thoughtcrime.securesms.util.PlayServicesUtil
import org.thoughtcrime.securesms.util.PlayServicesUtil.PlayServicesStatus
import org.thoughtcrime.securesms.util.navigation.safeNavigate
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() }
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

@@ -38,7 +38,7 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
setContentView(R.layout.activity_change_number_lock)
changeNumberRepository = ChangeNumberRepository(applicationContext)
changeNumberRepository = ChangeNumberRepository()
checkWhoAmI()
}
@@ -50,25 +50,28 @@ class ChangeNumberLockActivity : PassphraseRequiredActivity() {
override fun onBackPressed() = Unit
private fun checkWhoAmI() {
disposables.add(
changeNumberRepository.whoAmI()
.flatMap { whoAmI ->
if (Objects.equals(whoAmI.number, SignalStore.account().e164)) {
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
Single.just(false)
} else {
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))
.map { true }
}
disposables += changeNumberRepository
.whoAmI()
.flatMap { whoAmI ->
if (Objects.equals(whoAmI.number, SignalStore.account().e164)) {
Log.i(TAG, "Local and remote numbers match, nothing needs to be done.")
Single.just(false)
} else {
Log.i(TAG, "Local (${SignalStore.account().e164}) and remote (${whoAmI.number}) numbers do not match, updating local.")
Single
.just(true)
.flatMap { changeNumberRepository.changeLocalNumber(whoAmI.number, PNI.parseOrThrow(whoAmI.pni)) }
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
.map { true }
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus)
)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(onSuccess = { onChangeStatusConfirmed() }, onError = this::onFailedToGetChangeNumberStatus)
}
private fun onChangeStatusConfirmed() {
SignalStore.misc().unlockChangeNumber()
SignalStore.misc().clearPendingChangeNumberMetadata()
MaterialAlertDialogBuilder(this)
.setTitle(R.string.ChangeNumberLockActivity__change_status_confirmed)

View File

@@ -1,11 +1,21 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import android.content.Context
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.IdentityKeyPair
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.state.SignalProtocolStore
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.protocol.util.Medium
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.PreKeyUtil
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.PendingChangeNumberMetadata
import org.thoughtcrime.securesms.database.model.toProtoByteString
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.CertificateType
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -17,22 +27,101 @@ import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.whispersystems.signalservice.api.KbsPinData
import org.whispersystems.signalservice.api.KeyBackupSystemNoDataException
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceIdType
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
import java.io.IOException
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.concurrent.locks.ReentrantLock
private val TAG: String = Log.tag(ChangeNumberRepository::class.java)
class ChangeNumberRepository(private val context: Context) {
/**
* 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()
) {
private val accountManager = ApplicationDependencies.getSignalServiceAccountManager()
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()
fun changeNumber(code: String, newE164: String): Single<ServiceResponse<VerifyAccountResponse>> {
return Single.fromCallable { accountManager.changeNumber(code, newE164, null) }
.subscribeOn(Schedulers.io())
/**
* 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 {
return Completable.create { emitter ->
ApplicationDependencies
.getIncomingMessageObserver()
.addDecryptionDrainedListener {
emitter.onComplete()
}
}.subscribeOn(Schedulers.single())
}
fun changeNumber(code: String, newE164: String, pniUpdateMode: Boolean = false): Single<ServiceResponse<VerifyAccountResponse>> {
return Single.fromCallable {
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 = 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) }
}
fun changeNumber(
@@ -42,38 +131,65 @@ class ChangeNumberRepository(private val context: Context) {
tokenData: TokenData
): Single<ServiceResponse<VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse>> {
return Single.fromCallable {
try {
val kbsData: KbsPinData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
val registrationLock: String = kbsData.masterKey.deriveRegistrationLock()
val kbsData: KbsPinData
val registrationLock: String
val response: ServiceResponse<VerifyAccountResponse> = accountManager.changeNumber(code, newE164, registrationLock)
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(response, kbsData)
try {
kbsData = KbsRepository.restoreMasterKey(pin, tokenData.enclave, tokenData.basicAuth, tokenData.tokenResponse)!!
registrationLock = kbsData.masterKey.deriveRegistrationLock()
} catch (e: KeyBackupSystemWrongPinException) {
ServiceResponse.forExecutionError(e)
return@fromCallable ServiceResponse.forExecutionError(e)
} catch (e: KeyBackupSystemNoDataException) {
ServiceResponse.forExecutionError(e)
return@fromCallable ServiceResponse.forExecutionError(e)
} catch (e: IOException) {
ServiceResponse.forExecutionError(e)
return@fromCallable ServiceResponse.forExecutionError(e)
}
}.subscribeOn(Schedulers.io())
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)
changeNumberResponse = accountManager.changeNumber(request)
val possibleError: Throwable? = changeNumberResponse.applicationError.orElse(null)
if (possibleError is MismatchedDevicesException) {
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
attempts++
} else {
completed = true
}
}
VerifyAccountRepository.VerifyAccountWithRegistrationLockResponse.from(changeNumberResponse, kbsData)
}.subscribeOn(Schedulers.single())
.onErrorReturn { t -> ServiceResponse.forExecutionError(t) }
}
@Suppress("UsePropertyAccessSyntax")
fun whoAmI(): Single<WhoAmIResponse> {
return Single.fromCallable { ApplicationDependencies.getSignalServiceAccountManager().getWhoAmI() }
.subscribeOn(Schedulers.io())
.subscribeOn(Schedulers.single())
}
@WorkerThread
fun changeLocalNumber(e164: String, pni: PNI): Single<Unit> {
val oldStorageId: ByteArray? = Recipient.self().storageServiceId
SignalDatabase.recipients.updateSelfPhone(e164)
SignalDatabase.recipients.updateSelfPhone(e164, pni)
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")
SignalDatabase.recipients.rotateStorageId(Recipient.self().id)
Recipient.self().live().refresh()
StorageSyncHelper.scheduleSyncForDataChange()
val secondAttemptStorageId: ByteArray? = Recipient.self().storageServiceId
if (MessageDigest.isEqual(oldStorageId, secondAttemptStorageId)) {
@@ -81,11 +197,57 @@ class ChangeNumberRepository(private val context: Context) {
}
}
SignalDatabase.recipients.setPni(Recipient.self().id, pni)
ApplicationDependencies.getRecipientCache().clear()
SignalStore.account().setE164(e164)
SignalStore.account().setPni(pni)
ApplicationDependencies.getGroupsV2Authorization().clear()
val metadata: PendingChangeNumberMetadata? = SignalStore.misc().pendingChangeNumberMetadata
if (metadata == null) {
Log.w(TAG, "No change number metadata, this shouldn't happen")
throw AssertionError("No change number metadata")
}
val originalPni = ServiceId.fromByteString(metadata.previousPni)
if (originalPni == pni) {
Log.i(TAG, "No change has occurred, PNI is unchanged: $pni")
} else {
val pniIdentityKeyPair = IdentityKeyPair(metadata.pniIdentityKeyPair.toByteArray())
val pniRegistrationId = metadata.pniRegistrationId
val pniSignedPreyKeyId = metadata.pniSignedPreKeyId
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore = SignalStore.account().pniPreKeys
SignalStore.account().pniRegistrationId = pniRegistrationId
SignalStore.account().setPniIdentityKeyAfterChangeNumber(pniIdentityKeyPair)
val signedPreKey = pniProtocolStore.loadSignedPreKey(pniSignedPreyKeyId)
val oneTimePreKeys = PreKeyUtil.generateAndStoreOneTimePreKeys(pniProtocolStore, pniMetadataStore)
pniMetadataStore.activeSignedPreKeyId = signedPreKey.id
accountManager.setPreKeys(ServiceIdType.PNI, pniProtocolStore.identityKeyPair.publicKey, signedPreKey, oneTimePreKeys)
pniMetadataStore.isSignedPreKeyRegistered = true
pniProtocolStore.identities().saveIdentityWithoutSideEffects(
Recipient.self().id,
pniProtocolStore.identityKeyPair.publicKey,
IdentityDatabase.VerifiedStatus.VERIFIED,
true,
System.currentTimeMillis(),
true
)
SignalStore.misc().setPniInitializedDevices(true)
ApplicationDependencies.getGroupsV2Authorization().clear()
}
Recipient.self().live().refresh()
StorageSyncHelper.scheduleSyncForDataChange()
ApplicationDependencies.closeConnections()
ApplicationDependencies.getIncomingMessageObserver()
@@ -110,6 +272,86 @@ class ChangeNumberRepository(private val context: Context) {
SignalStore.certificateValues().setUnidentifiedAccessCertificate(certificateType, certificate)
}
}.subscribeOn(Schedulers.io())
}.subscribeOn(Schedulers.single())
}
@Suppress("UsePropertyAccessSyntax")
@WorkerThread
private fun createChangeNumberRequest(
code: String,
newE164: String,
registrationLock: String?,
pniUpdateMode: Boolean
): ChangeNumberRequestData {
val selfIdentifier: String = SignalStore.account().requireAci().toString()
val aciProtocolStore: SignalProtocolStore = ApplicationDependencies.getProtocolStore().aci()
val pniIdentity: IdentityKeyPair = if (pniUpdateMode) SignalStore.account().pniIdentityKey else IdentityKeyUtil.generateIdentityKeyPair()
val deviceMessages = mutableListOf<OutgoingPushMessage>()
val devicePniSignedPreKeys = mutableMapOf<Int, SignedPreKeyEntity>()
val pniRegistrationIds = mutableMapOf<Int, Int>()
val primaryDeviceId: Int = SignalServiceAddress.DEFAULT_DEVICE_ID
val devices: List<Int> = listOf(primaryDeviceId) + aciProtocolStore.getSubDeviceSessions(selfIdentifier)
devices
.filter { it == primaryDeviceId || aciProtocolStore.containsSession(SignalProtocolAddress(selfIdentifier, it)) }
.forEach { deviceId ->
// Signed Prekeys
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)
}
}
val request = ChangePhoneNumberRequest(
newE164,
code,
registrationLock,
pniIdentity.publicKey,
deviceMessages,
devicePniSignedPreKeys.mapKeys { it.key.toString() },
pniRegistrationIds.mapKeys { it.key.toString() }
)
val metadata = PendingChangeNumberMetadata.newBuilder()
.setPreviousPni(SignalStore.account().pni!!.toByteString())
.setPniIdentityKeyPair(pniIdentity.serialize().toProtoByteString())
.setPniRegistrationId(pniRegistrationIds[primaryDeviceId]!!)
.setPniSignedPreKeyId(devicePniSignedPreKeys[primaryDeviceId]!!.keyId)
.build()
return ChangeNumberRequestData(request, metadata)
}
data class ChangeNumberRequestData(val changeNumberRequest: ChangePhoneNumberRequest, val pendingChangeNumberMetadata: PendingChangeNumberMetadata)
}

View File

@@ -48,29 +48,29 @@ class ChangeNumberVerifyFragment : LoggingFragment(R.layout.fragment_change_phon
}
private fun requestCode() {
lifecycleDisposable.add(
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { processor ->
if (processor.hasResult()) {
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
} else if (processor.localRateLimit()) {
Log.i(TAG, "Unable to request sms code due to local rate limit")
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
} else if (processor.captchaRequired()) {
Log.i(TAG, "Unable to request sms code due to captcha required")
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
requestingCaptcha = true
} else if (processor.rateLimit()) {
Log.i(TAG, "Unable to request sms code due to rate limit")
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show()
findNavController().navigateUp()
} else {
Log.w(TAG, "Unable to request sms code", processor.error)
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show()
findNavController().navigateUp()
}
lifecycleDisposable += viewModel
.ensureDecryptionsDrained()
.andThen(viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER))
.observeOn(AndroidSchedulers.mainThread())
.subscribe { processor ->
if (processor.hasResult()) {
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
} else if (processor.localRateLimit()) {
Log.i(TAG, "Unable to request sms code due to local rate limit")
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_changeNumberEnterCodeFragment)
} else if (processor.captchaRequired()) {
Log.i(TAG, "Unable to request sms code due to captcha required")
findNavController().safeNavigate(R.id.action_changePhoneNumberVerifyFragment_to_captchaFragment, getCaptchaArguments())
requestingCaptcha = true
} else if (processor.rateLimit()) {
Log.i(TAG, "Unable to request sms code due to rate limit")
Toast.makeText(requireContext(), R.string.RegistrationActivity_rate_limited_to_service, Toast.LENGTH_LONG).show()
findNavController().navigateUp()
} else {
Log.w(TAG, "Unable to request sms code", processor.error)
Toast.makeText(requireContext(), R.string.RegistrationActivity_unable_to_connect_to_service, Toast.LENGTH_LONG).show()
findNavController().navigateUp()
}
)
}
}
}

View File

@@ -9,12 +9,14 @@ import androidx.lifecycle.ViewModel
import androidx.savedstate.SavedStateRegistryOwner
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.pin.KbsRepository
import org.thoughtcrime.securesms.pin.TokenData
import org.thoughtcrime.securesms.registration.SmsRetrieverReceiver
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.VerifyAccountResponseProcessor
import org.thoughtcrime.securesms.registration.VerifyAccountResponseWithoutKbs
@@ -37,6 +39,7 @@ class ChangeNumberViewModel(
password: String,
verifyAccountRepository: VerifyAccountRepository,
kbsRepository: KbsRepository,
private val smsRetrieverReceiver: SmsRetrieverReceiver = SmsRetrieverReceiver(ApplicationDependencies.getApplication())
) : BaseRegistrationViewModel(savedState, verifyAccountRepository, kbsRepository, password) {
var oldNumberState: NumberViewState = NumberViewState.Builder().build()
@@ -56,6 +59,13 @@ class ChangeNumberViewModel(
} catch (e: NumberParseException) {
Log.i(TAG, "Unable to parse number for default country code")
}
smsRetrieverReceiver.registerReceiver()
}
override fun onCleared() {
super.onCleared()
smsRetrieverReceiver.unregisterReceiver()
}
fun getLiveOldNumber(): LiveData<NumberViewState> {
@@ -107,21 +117,26 @@ class ChangeNumberViewModel(
}
}
fun ensureDecryptionsDrained(): Completable {
return changeNumberRepository.ensureDecryptionsDrained()
}
override fun verifyCodeWithoutRegistrationLock(code: String): Single<VerifyAccountResponseProcessor> {
return super.verifyCodeWithoutRegistrationLock(code)
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
.flatMap(this::attemptToUnlockChangeNumber)
}
override fun verifyCodeAndRegisterAccountWithRegistrationLock(pin: String): Single<VerifyCodeWithRegistrationLockResponseProcessor> {
return super.verifyCodeAndRegisterAccountWithRegistrationLock(pin)
.doOnSubscribe { SignalStore.misc().lockChangeNumber() }
.compose(ChangeNumberRepository::acquireReleaseChangeNumberLock)
.flatMap(this::attemptToUnlockChangeNumber)
}
private fun <T : VerifyProcessor> attemptToUnlockChangeNumber(processor: T): Single<T> {
return if (processor.hasResult() || processor.isServerSentError()) {
SignalStore.misc().unlockChangeNumber()
SignalStore.misc().clearPendingChangeNumberMetadata()
Single.just(processor)
} else {
changeNumberRepository.whoAmI()
@@ -129,6 +144,7 @@ class ChangeNumberViewModel(
if (Objects.equals(whoAmI.number, localNumber)) {
Log.i(TAG, "Local and remote numbers match, we can unlock.")
SignalStore.misc().unlockChangeNumber()
SignalStore.misc().clearPendingChangeNumberMetadata()
}
processor
}
@@ -172,7 +188,7 @@ class ChangeNumberViewModel(
val viewModel = ChangeNumberViewModel(
localNumber = localNumber,
changeNumberRepository = ChangeNumberRepository(context),
changeNumberRepository = ChangeNumberRepository(),
savedState = handle,
password = password,
verifyAccountRepository = VerifyAccountRepository(context),

View File

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

View File

@@ -1,20 +1,28 @@
package org.thoughtcrime.securesms.components.settings.app.chats.sms
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.ViewModelProvider
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.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.SmsUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
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) {
private lateinit var viewModel: SmsSettingsViewModel
private lateinit var smsExportLauncher: ActivityResultLauncher<Intent>
override fun onResume() {
super.onResume()
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.state.observe(viewLifecycleOwner) {
@@ -42,6 +57,32 @@ class SmsSettingsFragment : DSLSettingsFragment(R.string.preferences__sms_mms) {
private fun getConfiguration(state: SmsSettingsState): DSLConfiguration {
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")
clickPref(
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())
}
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(
val useAsDefaultSmsApp: 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.ViewModel
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.Util
@@ -9,6 +11,9 @@ import org.thoughtcrime.securesms.util.livedata.Store
class SmsSettingsViewModel : ViewModel() {
private val repository = SmsSettingsRepository()
private val disposables = CompositeDisposable()
private val store = Store(
SmsSettingsState(
useAsDefaultSmsApp = Util.isDefaultSmsProvider(ApplicationDependencies.getApplication()),
@@ -19,6 +24,16 @@ class SmsSettingsViewModel : ViewModel() {
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) {
store.update { it.copy(smsDeliveryReportsEnabled = enabled) }
SignalStore.settings().isSmsDeliveryReportsEnabled = enabled

View File

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

View File

@@ -15,12 +15,12 @@ import org.signal.ringrtc.CallManager
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.database.MegaphoneDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
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.SubscriptionReceiptRequestResponseJob
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.storage.StorageSyncHelper
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 java.util.Optional
import java.util.concurrent.TimeUnit
@@ -48,7 +50,7 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
private lateinit var viewModel: InternalSettingsViewModel
override fun bindAdapter(adapter: DSLSettingsAdapter) {
override fun bindAdapter(adapter: MappingAdapter) {
val repository = InternalSettingsRepository(requireContext())
val factory = InternalSettingsViewModel.Factory(repository)
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)
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(
title = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites),
summary = DSLSettingsText.from(R.string.preferences__internal_force_gv2_invites_description),
@@ -206,28 +199,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
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)
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()
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(
title = DSLSettingsText.from(R.string.preferences__internal_fetch_release_channel),
onClick = {
@@ -478,14 +462,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
sectionHeaderPref(R.string.ConversationListTabs__stories)
switchPref(
title = DSLSettingsText.from(R.string.preferences__internal_disable_stories),
isChecked = state.disableStories,
onClick = {
viewModel.toggleStories()
}
)
clickPref(
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),
@@ -494,6 +470,13 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
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(
val seeMoreUserDetails: Boolean,
val shakeToReport: Boolean,
val gv2doNotCreateGv2Groups: Boolean,
val gv2forceInvites: Boolean,
val gv2ignoreServerChanges: Boolean,
val gv2ignoreP2PChanges: Boolean,
val disableAutoMigrationInitiation: Boolean,
val disableAutoMigrationNotification: Boolean,
val allowCensorshipSetting: Boolean,
val callingServer: String,
val callingAudioProcessingMethod: CallManager.AudioProcessingMethod,
@@ -22,6 +19,5 @@ data class InternalSettingsState(
val removeSenderKeyMinimium: Boolean,
val delayResends: Boolean,
val disableStorageService: Boolean,
val disableStories: 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.keyvalue.InternalValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.livedata.Store
@@ -38,11 +39,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun setGv2DoNotCreateGv2Groups(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_DO_NOT_CREATE_GV2, enabled)
refresh()
}
fun setGv2ForceInvites(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.GV2_FORCE_INVITES, enabled)
refresh()
@@ -58,16 +54,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
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) {
preferenceDataStore.putBoolean(InternalValues.ALLOW_CENSORSHIP_SETTING, enabled)
refresh()
@@ -108,12 +94,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun toggleStories() {
val newState = !SignalStore.storyValues().isFeatureDisabled
SignalStore.storyValues().isFeatureDisabled = newState
store.update { getState().copy(disableStories = newState) }
}
fun addSampleReleaseNote() {
repository.addSampleReleaseNote()
}
@@ -125,12 +105,9 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
private fun getState() = InternalSettingsState(
seeMoreUserDetails = SignalStore.internalValues().recipientDetails(),
shakeToReport = SignalStore.internalValues().shakeToReport(),
gv2doNotCreateGv2Groups = SignalStore.internalValues().gv2DoNotCreateGv2Groups(),
gv2forceInvites = SignalStore.internalValues().gv2ForceInvites(),
gv2ignoreServerChanges = SignalStore.internalValues().gv2IgnoreServerChanges(),
gv2ignoreP2PChanges = SignalStore.internalValues().gv2IgnoreP2PChanges(),
disableAutoMigrationInitiation = SignalStore.internalValues().disableGv1AutoMigrateInitiation(),
disableAutoMigrationNotification = SignalStore.internalValues().disableGv1AutoMigrateNotification(),
allowCensorshipSetting = SignalStore.internalValues().allowChangingCensorshipSetting(),
callingServer = SignalStore.internalValues().groupCallingServer(),
callingAudioProcessingMethod = SignalStore.internalValues().callingAudioProcessingMethod(),
@@ -141,13 +118,13 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
removeSenderKeyMinimium = SignalStore.internalValues().removeSenderKeyMinimum(),
delayResends = SignalStore.internalValues().delayResends(),
disableStorageService = SignalStore.internalValues().storageServiceDisabled(),
disableStories = SignalStore.storyValues().isFeatureDisabled,
canClearOnboardingState = SignalStore.storyValues().hasDownloadedOnboardingStory && Stories.isFeatureEnabled()
)
fun onClearOnboardingState() {
SignalStore.storyValues().hasDownloadedOnboardingStory = false
SignalStore.storyValues().userHasSeenOnboardingStory = false
Stories.onStorySettingsChanged(Recipient.self().id)
refresh()
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.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.UnexpectedSubscriptionCancellation
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
class DonorErrorConfigurationFragment : DSLSettingsFragment() {
private val viewModel: DonorErrorConfigurationViewModel by viewModels()
private val lifecycleDisposable = LifecycleDisposable()
override fun bindAdapter(adapter: DSLSettingsAdapter) {
override fun bindAdapter(adapter: MappingAdapter) {
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}

View File

@@ -22,7 +22,6 @@ import androidx.preference.PreferenceManager
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.PreferenceModel
@@ -35,6 +34,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.util.RingtoneUtil
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
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(
LedColorPreference::class.java,
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 org.thoughtcrime.securesms.R
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.app.notifications.profiles.models.NotificationProfileAddMembers
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.RecipientId
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.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)
NotificationProfileRecipient.register(adapter)

View File

@@ -17,7 +17,6 @@ import org.signal.core.util.BreakIteratorCompat
import org.signal.core.util.EditTextUtil
import org.thoughtcrime.securesms.R
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.app.notifications.profiles.EditNotificationProfileViewModel.SaveNotificationProfileResult
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.LifecycleDisposable
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.text.AfterTextChanged
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
@@ -131,7 +131,7 @@ class EditNotificationProfileFragment : DSLSettingsFragment(layoutId = R.layout.
this.emojiView = emojiView
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
override fun bindAdapter(adapter: MappingAdapter) {
NotificationProfileNamePreset.register(adapter)
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.components.emoji.EmojiUtil
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.DSLSettingsIcon
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.util.LifecycleDisposable
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.navigation.safeNavigate
import org.thoughtcrime.securesms.util.orderOfDaysInWeek
@@ -65,7 +65,7 @@ class NotificationProfileDetailsFragment : DSLSettingsFragment() {
toolbar = null
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
override fun bindAdapter(adapter: MappingAdapter) {
NotificationProfilePreference.register(adapter)
NotificationProfileAddMembers.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.components.emoji.EmojiUtil
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.DSLSettingsIcon
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.NotificationProfiles
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
@@ -48,7 +48,7 @@ class NotificationProfilesFragment : DSLSettingsFragment() {
toolbar = null
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
override fun bindAdapter(adapter: MappingAdapter) {
NoNotificationProfiles.register(adapter)
LargeIconClickPreference.register(adapter)
NotificationProfilePreference.register(adapter)

View File

@@ -1,8 +1,11 @@
package org.thoughtcrime.securesms.components.settings.app.privacy
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Build
import android.provider.Settings
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.TextAppearanceSpan
@@ -16,18 +19,17 @@ import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import mobi.upod.timedurationpicker.TimeDurationPicker
import mobi.upod.timedurationpicker.TimeDurationPickerDialog
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.PassphraseChangeActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.ClickPreference
import org.thoughtcrime.securesms.components.settings.ClickPreferenceViewHolder
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.PreferenceModel
@@ -36,12 +38,8 @@ import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
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.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.ConversationUtil
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.TextSecurePreferences
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import java.lang.Integer.max
import java.util.Locale
@@ -76,17 +75,22 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
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))
PrivateStoryItem.register(adapter)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val repository = PrivacySettingsRepository()
val factory = PrivacySettingsViewModel.Factory(sharedPreferences, repository)
viewModel = ViewModelProvider(this, factory)[PrivacySettingsViewModel::class.java]
val args: PrivacySettingsFragmentArgs by navArgs()
var showPaymentLock = true
viewModel.state.observe(viewLifecycleOwner) { state ->
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),
isChecked = state.isObsoletePasswordTimeoutEnabled,
onClick = {
viewModel.setObsoletePasswordTimeoutEnabled(!state.isObsoletePasswordEnabled)
viewModel.setObsoletePasswordTimeoutEnabled(!state.isObsoletePasswordTimeoutEnabled)
}
)
@@ -297,56 +301,36 @@ class PrivacySettingsFragment : DSLSettingsFragment(R.string.preferences__privac
)
if (Stories.isFeatureAvailable()) {
dividerPref()
sectionHeaderPref(R.string.ConversationListTabs__stories)
if (!SignalStore.storyValues().isFeatureDisabled) {
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,
clickPref(
title = DSLSettingsText.from(R.string.preferences__stories),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__manage_your_stories),
onClick = {
viewModel.setStoriesEnabled(!state.isStoriesEnabled)
findNavController().safeNavigate(PrivacySettingsFragmentDirections.actionPrivacySettingsFragmentToStoryPrivacySettings(R.string.preferences__stories))
}
)
}
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(
title = DSLSettingsText.from(R.string.preferences__advanced),
summary = DSLSettingsText.from(R.string.PrivacySettingsFragment__signal_message_and_calls),
@@ -357,15 +341,37 @@ 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 {
val hours = TimeUnit.SECONDS.toHours(timeoutSeconds)
val minutes =
TimeUnit.SECONDS.toMinutes(timeoutSeconds) - TimeUnit.SECONDS.toHours(timeoutSeconds) * 60
val minutes = TimeUnit.SECONDS.toMinutes(timeoutSeconds) - hours * 60
return if (timeoutSeconds <= 0) {
getString(R.string.AppProtectionPreferenceFragment_none)
} else {
String.format(Locale.getDefault(), "%02d:%02d:00", hours, minutes)
String.format(Locale.getDefault(), "%02d:%02d", hours, minutes)
}
}

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.settings.app.privacy
import android.content.Context
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListPartialRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob
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() {
SignalExecutors.BOUNDED.execute {
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import org.signal.core.util.PendingIntentFlags
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.help.HelpFragment
@@ -90,7 +91,7 @@ object DonationErrorNotifications {
context,
0,
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> {
return when (verificationError) {
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,
positiveAction = callback.onOk(context),
negativeAction = null
)
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,
positiveAction = callback.onOk(context),
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.models.BadgePreview
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.DSLSettingsIcon
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.util.FeatureFlags
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import java.util.Currency
@@ -60,7 +60,7 @@ class ManageDonationsFragment : DSLSettingsFragment(), ExpiredGiftSheet.Callback
viewModel.refresh()
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
override fun bindAdapter(adapter: MappingAdapter) {
ActiveSubscriptionPreference.register(adapter)
IndeterminateLoadingCircle.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.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
@@ -26,6 +25,7 @@ import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import java.io.ByteArrayOutputStream
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)
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.BadgePreview
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
@@ -37,6 +36,7 @@ import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.subscription.Subscription
import org.thoughtcrime.securesms.util.LifecycleDisposable
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.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
@@ -82,7 +82,7 @@ class SubscribeFragment : DSLSettingsFragment(
viewModel.refreshActiveSubscription()
}
override fun bindAdapter(adapter: DSLSettingsAdapter) {
override fun bindAdapter(adapter: MappingAdapter) {
donationPaymentComponent = requireListener()
viewModel.refresh()

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.conversation
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
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.recyclerview.OnScrollAnimationHelper
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.DSLSettingsIcon
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.ContextUtil
import org.thoughtcrime.securesms.util.ExpirationUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
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.views.SimpleProgressDialog
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())
BioTextPreference.register(adapter)
@@ -230,7 +230,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
.withFixedSize(ViewUtil.dpToPx(80))
.load(state.recipient)
if (FeatureFlags.displayDonorBadges() && !state.recipient.isSelf) {
if (!state.recipient.isSelf) {
toolbarBadge.setBadgeFromRecipient(state.recipient)
}
@@ -286,7 +286,8 @@ class ConversationSettingsFragment : DSLSettingsFragment(
requireContext(),
StoryViewerArgs(
recipientId = state.recipient.id,
isInHiddenStoryMode = state.recipient.shouldHideStory()
isInHiddenStoryMode = state.recipient.shouldHideStory(),
isFromQuote = true
)
)
StoryDialogs.displayStoryOrProfileImage(
@@ -477,7 +478,11 @@ class ConversationSettingsFragment : DSLSettingsFragment(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_as_a_contact),
icon = DSLSettingsIcon.from(R.drawable.ic_plus_24),
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.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
@@ -23,12 +22,14 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage
import org.thoughtcrime.securesms.subscription.Subscriber
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.SpanUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.livedata.Store
import org.whispersystems.signalservice.api.push.ServiceId
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 ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
@@ -62,27 +63,26 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
)
if (!recipient.isGroup) {
if (recipient.isSelf) {
val aci: String = SignalStore.account().aci?.toString() ?: "null"
longClickPref(
title = DSLSettingsText.from("ACI"),
summary = DSLSettingsText.from(aci),
onLongClick = { copyToClipboard(aci) }
)
val pni: String = SignalStore.account().pni?.toString() ?: "null"
longClickPref(
title = DSLSettingsText.from("PNI"),
summary = DSLSettingsText.from(pni),
onLongClick = { copyToClipboard(pni) }
)
} else {
val serviceId: String = recipient.serviceId.map(ServiceId::toString).orElse("null")
longClickPref(
title = DSLSettingsText.from("ServiceId"),
summary = DSLSettingsText.from(serviceId),
onLongClick = { copyToClipboard(serviceId) }
)
}
val e164: String = recipient.e164.orElse("null")
longClickPref(
title = DSLSettingsText.from("E164"),
summary = DSLSettingsText.from(e164),
onLongClick = { copyToClipboard(e164) }
)
val serviceId: String = recipient.serviceId.map { it.toString() }.orElse("null")
longClickPref(
title = DSLSettingsText.from("ServiceId"),
summary = DSLSettingsText.from(serviceId),
onLongClick = { copyToClipboard(serviceId) }
)
val pni: String = recipient.pni.map { it.toString() }.orElse("null")
longClickPref(
title = DSLSettingsText.from("PNI"),
summary = DSLSettingsText.from(pni),
onLongClick = { copyToClipboard(pni) }
)
}
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()
}
)
}
}

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