Compare commits

...

1534 Commits

Author SHA1 Message Date
Greyson Parrelli
5537039e46 Bump version to 6.7.2 2023-01-05 15:03:41 -05:00
Greyson Parrelli
0a40432ed4 Fix crash that occurs during thread trims by date. 2023-01-05 14:52:51 -05:00
Cody Henthorne
73e46053f0 Fix voice note crash when future failed. 2023-01-05 12:12:03 -05:00
Greyson Parrelli
a835e5d143 Bump version to 6.7.1 2023-01-05 10:14:04 -05:00
Greyson Parrelli
073d5dfe8c Don't mark unauthorized unless we're registered. 2023-01-05 10:05:58 -05:00
Greyson Parrelli
bfba60b6b6 Improve reliability of SqlUtil.getNextAutoIncrementId() 2023-01-05 10:01:33 -05:00
Alex Hart
84d9e1d28e Drop proxy sheet events if fragment state has been saved. 2023-01-05 09:44:26 -04:00
Alex Hart
a14c61c370 Rotate chat-filters flag. 2023-01-05 09:38:54 -04:00
Greyson Parrelli
0db4630f58 Bump version to 6.7.0 2023-01-04 16:57:33 -05:00
Greyson Parrelli
6d2e51def6 Updated language translations. 2023-01-04 16:57:05 -05:00
Nicholas
091eb0aa2b Fix chaining of Stories FTUX for small screens. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
59f05e0815 Improve performance of marking chats read.
SQLite isn't always smart enough to use the best index for a query.
The main improvement here was to force it to use a better index than the
one it was using (which, on my device, happened to by the story index,
which was only minimally useful here).
2023-01-04 16:40:36 -05:00
Greyson Parrelli
a513e93d18 Fix log around sending SKDM's. 2023-01-04 16:40:36 -05:00
Alex Hart
91f6cff4df Do not display recovery card if entropy is not set. 2023-01-04 16:40:36 -05:00
Alex Hart
ec3ec969eb Add updated search hint text when filter is enabled. 2023-01-04 16:40:36 -05:00
Alex Hart
54d0df9a05 Ensure only a single story is displayed above the fold in bottom sheets. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
320669c54e Remove the unknown insert validation.
There's actually a legitimate case where this is ok: right after a
backup restore.

Restoring a backup means that you have possibly carried over some
unknownIds, and if you don't remember your PIN, those items wouldn't be
there remotely. And you _should_ insert them. Otherwise they're lost.

I don't think this validation is worth the trouble of carving out lots
of conditions to allow this usecase.
2023-01-04 16:40:36 -05:00
Alex Hart
bf491c25f7 Update view badge bottom sheet button copy. 2023-01-04 16:40:36 -05:00
Alex Hart
1e153e129c Ensure ShareViewModel#resolve is performed on the background thread.
Fixes #12696
2023-01-04 16:40:36 -05:00
Cody Henthorne
b546d661ba Update 1:1 call event copy. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
6a1a657451 Fix story database observer updates to ensure rings are updated live. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
ece087eaae Require non-null uri in UriAttachment.
Part of an effort to track down a different crash. This should make it
more obvious where the error is being made.
2023-01-04 16:40:36 -05:00
Alex Hart
a04590b658 Ensure pre-upload media is properly fanned out to group stories. 2023-01-04 16:40:36 -05:00
Alex Hart
eb6a14e686 Fix bad background on long text posts. 2023-01-04 16:40:36 -05:00
Alex Hart
c7bb0eadc2 Fix restore-state of filter pull view. 2023-01-04 16:40:36 -05:00
Nicholas
d70fe8f2cd Only notify for unauthorized response to messages endpoint. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
0fd8f73cca Show username if appropriate in group chips. 2023-01-04 16:40:36 -05:00
Greyson Parrelli
b9fc36be5a Prevent crash when adding group member by username. 2023-01-04 16:40:36 -05:00
Alex Hart
4de27482bb Update blocking get call to safeBlockingGet. 2023-01-04 16:40:36 -05:00
Nicholas
ba347301cf Keep media preview image order even for RTL locales.
Addresses #12574.
2023-01-04 16:40:36 -05:00
Alex Hart
296a113c65 Add "You can pull to filter" tip. 2023-01-04 16:40:36 -05:00
Cody Henthorne
43fe789807 Add support for general media attachments to release notes channel messages. 2023-01-04 16:40:36 -05:00
Alex Hart
98dfd5bfbf Lower fade out duration of chat pill to 150ms. 2023-01-04 16:40:36 -05:00
Alex Hart
f387785a46 Add chat filter pill fade out on slide close. 2023-01-04 16:40:36 -05:00
Cody Henthorne
7b3d8d01ae Prevent dismissal of notification.
Fixes #12681
2023-01-03 11:03:06 -05:00
Alex Hart
1712442560 Add chat filter pill color lerp at close apex. 2023-01-03 10:47:53 -04:00
Nicholas
e4ddedcc48 Log type of connected headsets. 2023-01-03 09:33:08 -05:00
Alex Hart
14503b952a Add helper text when dragging filter at a low velocity. 2023-01-03 10:31:31 -04:00
Nicholas
5cb3e1cd02 Launch "Keep Muted Chats Archived". 2023-01-03 09:05:57 -05:00
Nicholas
7959343661 Add local notification when client receives HTTP 403
Also corrects typo in method name.
2023-01-02 17:20:42 -05:00
Alex Hart
52062679d4 Update filter chip padding to match spec. 2023-01-02 15:05:07 -04:00
Alex Hart
9e8350e8c2 Implement chat filter design feedback. 2023-01-02 12:19:04 -04:00
Sgn-32
495c91ba86 Use the correct delivery time in the message details
CLoses #11655
2023-01-02 09:51:20 -05:00
Greyson Parrelli
92b9fda6c7 Convert GroupTable to kotlin.
Also required converting some tests to mockk.
2023-01-01 23:05:02 -05:00
Greyson Parrelli
fecfd7cd78 Remove the rest of MmsSmsTable. 2022-12-31 13:43:12 -05:00
Greyson Parrelli
6cd6073bc7 Migrate most of MmsSmsTable. 2022-12-30 18:24:00 -05:00
Greyson Parrelli
f149c0adb9 Remove MmsSmsColumns.
All the columns got moved to MessageTable.
I kept the types though and renamed the class to MessageTypes because
it's a lot of boring domain-specific code.
2022-12-30 16:54:49 -05:00
Greyson Parrelli
3708cc5583 Add additional protections around recipientIds and threadIds matching. 2022-12-30 16:54:49 -05:00
Greyson Parrelli
4dd8e81db7 Fix some situations where MessageTable actions were doubled. 2022-12-30 16:54:49 -05:00
Cody Henthorne
06b414f4ef Add call disposition syncing. 2022-12-30 16:54:49 -05:00
Nicholas
d471647e12 Animate swapping of play/pause buttons. 2022-12-30 16:54:49 -05:00
Greyson Parrelli
dd3bad858d Prevent scrolling when context menu is showing on story landing page. 2022-12-30 15:11:57 -05:00
Cody Henthorne
0fe6538ce4 Fix media viewer rail items jumping around while paging. 2022-12-30 15:11:57 -05:00
Alex Hart
1e2f7f0775 Add currency selection logic update. 2022-12-30 15:11:57 -05:00
Cody Henthorne
055b4691d7 Fix video playback starting when off screen in media viewer. 2022-12-30 15:11:57 -05:00
Cody Henthorne
ebdfa88882 Schedule ExpireStoriesManager when viewing Stories tab. 2022-12-30 15:11:57 -05:00
Alex Hart
d79c4775b6 Add chat filter animation. 2022-12-30 15:11:56 -05:00
Cody Henthorne
a13599ae2a Add payment activation capability. 2022-12-30 15:11:56 -05:00
Alex Hart
96b2051400 Rotate chat-filters flag for internal testing. 2022-12-30 15:11:56 -05:00
Nicholas
ad6d1a2e8d Update styling of the media rail selection states. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
eada1e96ee Improve emoji search rankings. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
91fbc236ce Rename pnp capability to pni. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
202f20893c Add internal setting for manually initializing PNP mode. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
f1d204b834 Update SQLCipher to 4.5.1-S1 2022-12-30 15:11:56 -05:00
Greyson Parrelli
73e19209ff Fix some issues with projections result from MessageTable migration. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
835bf3998f Perform large inserts in batches during MessageTable migration. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
d83ef56ab1 Remove SmsMessageRecord. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
a84a9c5381 Fetch isQuoted status in bulk during conversation load.
Improves overall time to load a page of messages by ~50%.
2022-12-30 15:11:56 -05:00
Greyson Parrelli
c6f29fc950 Migrate queued jobs during SMS migration. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
4d9dc42868 Improve the performance of the migration by ~4x. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
32b66643c5 Rename PushMediaSendJob -> IndividualSendJob. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
3850c9c89d Remove isMms from MessageId. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
60ae883df6 Rename SignalDatabase.sms/mms to SignalDatabase.messages 2022-12-30 15:11:56 -05:00
Greyson Parrelli
a7e3bdc892 Rename OutgoingMediaMessage -> OutgoingMessage. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
9b60bd9a4b Remove OutgoingTextMessage and PushTextSendJob. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
e9d98b7d39 Migrate SMS messages into the MMS table. 2022-12-30 15:11:56 -05:00
Greyson Parrelli
cb0e7ade14 Bump version to 6.6.3 2022-12-24 11:13:04 -05:00
Greyson Parrelli
268f5c807d Ensure that remapped records are valid.
Fixes #12691
2022-12-24 11:13:04 -05:00
Greyson Parrelli
f6003023bf Bump version to 6.6.2 2022-12-23 12:23:48 -05:00
Nicholas Tinsley
3f160f256a Prevent crash on pause for Media Preview with no fragments. 2022-12-23 12:20:56 -05:00
Greyson Parrelli
9846517075 Bump version to 6.6.1 2022-12-21 13:40:10 -05:00
Greyson Parrelli
0f1cc03dc0 Updated language translations. 2022-12-21 13:40:10 -05:00
Nicholas Tinsley
0e5031ab45 Revert "Switch to BT mic if available for voice memo recording."
This reverts commit 9f6eb142d2.
2022-12-21 13:25:56 -05:00
Greyson Parrelli
0e4926b5ec Bump version to 6.6.0 2022-12-19 18:42:05 -05:00
Greyson Parrelli
a25e7c6d3e Updated language translations. 2022-12-19 18:38:42 -05:00
Cody Henthorne
4081ac2a83 Fix video controls becoming unresponsive after quickly paging. 2022-12-19 14:30:37 -05:00
Alex Hart
98a528f595 Fix recording progress bar when animations are scaled. 2022-12-19 12:46:13 -04:00
Nicholas
680325b5ee Increase MediaPreviewV2 lifecycle logging. 2022-12-16 16:12:11 -05:00
Nicholas
16668574a9 Separate message for media decode failure. 2022-12-16 15:32:59 -05:00
Rashad Sookram
0d8f6de4c1 Refactor bandwidth mode setting. 2022-12-16 15:22:04 -05:00
Alex Hart
4c0a98d526 Add nullability check to video capture callback.
Fixes #12666
2022-12-16 15:22:04 -05:00
Greyson Parrelli
10f78d5daa Change spinner to lazily read database stuff.
Otherwise you get into situations where Spinner will force DB accesses
super early during Application#onCreate on the main thread, which can be
bad when testing large DB migrations.
2022-12-16 15:22:04 -05:00
Cody Henthorne
3ce5a7da67 Fix emoji toggle behavior when in emoji search mode.
When in emoji search, toggle would be set to "emoji" state or
act like in "emoji" state. Fix is to show "keyboard" state still
when in emoji search.
2022-12-16 15:22:04 -05:00
Greyson Parrelli
4d47b9c594 Round Spinner timings to 3 decimal places. 2022-12-16 15:22:04 -05:00
Nicholas
9f6eb142d2 Switch to BT mic if available for voice memo recording.
Addresses #12016.
2022-12-16 15:22:04 -05:00
Nicholas
0e08b4ee26 Correctly animate deletion when attaching multiple media. 2022-12-16 15:22:04 -05:00
Cody Henthorne
9b85907918 Fix flicker of local avatar in call view. 2022-12-16 15:22:04 -05:00
Cody Henthorne
6463dca2c6 Fix media selection dismissing when deselecting last item. 2022-12-16 15:22:04 -05:00
Alex Hart
498b7fee69 Remove SingleLiveEvent from EditAboutViewModel. 2022-12-16 15:22:04 -05:00
Cody Henthorne
3478e13d38 Fix progress dialog deprecation warnings.
Moves everything under our own class and ignores the deprecation. Also
gives us future ability to re-style all blocking UI dialogs in the
future for mat3 compat.
2022-12-16 15:22:04 -05:00
Alex Hart
5f0d37739a Remove SLE from EditProxyViewModel. 2022-12-16 15:22:04 -05:00
Cody Henthorne
c5b4f44ab8 Fix various compiler warnings. 2022-12-16 15:22:04 -05:00
Alex Hart
819c9f61dc Remove SingleLiveEvent from BlockedUsersActivity. 2022-12-16 15:22:04 -05:00
Alex Hart
4f167feaf5 Handle deprecated connectivity intent filter. 2022-12-16 15:22:04 -05:00
Alex Hart
de558bc87c Remove SingleLiveEvent from ConversationSettingsViewModel. 2022-12-16 15:22:04 -05:00
Alex Hart
4a5a65ff6c Remove usage of SingleLiveEvent from MediaCaptureViewModel. 2022-12-16 15:22:04 -05:00
Cody Henthorne
c56e63d62f Convert OutgoingMediaMessage and it's couterparts to kotlin. 2022-12-16 15:22:04 -05:00
Nicholas
8cd9a3cabe Map platform WIRED_HEADPHONES to our WIRED_HEADSET.
Fixes #12622.
2022-12-16 15:22:04 -05:00
Alex Hart
3a8c324c12 Clean up a bunch of warnings. 2022-12-16 15:22:04 -05:00
Cody Henthorne
ff882edeae Enable kotlin for libsignal-service project and convert SignalServiceDataMessage. 2022-12-16 15:22:04 -05:00
Cody Henthorne
fb0aa55cbb Fix instrumentation tests by forcing channel id usage to init channels. 2022-12-16 15:22:04 -05:00
Alex Hart
51015dc898 Clean up warnings in Gradle file. 2022-12-16 15:22:04 -05:00
Cody Henthorne
4af40e7861 Bump version to 6.5.6 2022-12-16 12:47:55 -05:00
Cody Henthorne
24fcc0c3b0 Updated language translations. 2022-12-16 12:40:19 -05:00
Nicholas Tinsley
993fc24dd3 Change inheritance of MediaPreviewV2Activity. 2022-12-16 12:03:01 -05:00
Greyson Parrelli
fddc6bcd5f Update maven endpoint for sqlcipher. 2022-12-16 12:02:42 -05:00
Cody Henthorne
558051086e Bump version to 6.5.5 2022-12-14 13:33:43 -05:00
Cody Henthorne
2c187bc55d Updated language translations. 2022-12-14 12:59:41 -05:00
Cody Henthorne
e947979169 Revert "Fix view flicker when switching between keyboard and attachment/emoji keyboards."
This reverts commit 1618141342.
2022-12-14 12:53:01 -05:00
Greyson Parrelli
08f1ddb212 Guard against potentially double-running a migration. 2022-12-14 11:15:23 -05:00
Cody Henthorne
4c318d8d82 Bump version to 6.5.4 2022-12-13 16:52:10 -05:00
Cody Henthorne
3e6ebfabb0 Updated language translations. 2022-12-13 16:41:01 -05:00
Alex Hart
55f4692d99 Add logging for response fields when an error happens. 2022-12-13 16:36:36 -05:00
Greyson Parrelli
ebe82cf3e6 Add back missing reaction triggers. 2022-12-13 16:36:36 -05:00
Greyson Parrelli
21a8434e4d Attempt to fix SQLite crash in migration. 2022-12-13 10:59:27 -05:00
Greyson Parrelli
4990778a97 Fix recipient remapping of sms/mms records. 2022-12-13 09:54:53 -05:00
Alex Hart
303e5c7996 Remove PayPal order complete sheet. 2022-12-12 16:05:54 -04:00
Alex Hart
599caee229 Add error handling to re-throw Stripe POST errors. 2022-12-12 15:59:34 -04:00
Cody Henthorne
e6f28c6cdd Bump version to 6.5.3 2022-12-12 12:48:59 -05:00
Cody Henthorne
fd3b0ee375 Updated language translations. 2022-12-12 12:41:54 -05:00
Greyson Parrelli
bd11ed9f17 Fix table drop order during backup import.
Fixes #12671
2022-12-12 11:54:05 -05:00
Alex Hart
a6a185004d Only brighten screen when flash is ON and camera is FRONT. 2022-12-12 12:53:25 -04:00
Alex Hart
3cc556d803 Fix issue with cache entry access. 2022-12-12 12:51:57 -04:00
Alex Hart
c3f9984346 Update error handling to include customized action when user cancels PayPal flow. 2022-12-12 11:54:56 -04:00
Cody Henthorne
10df4ee0d1 Add additional info when backup verification fails. 2022-12-12 10:49:55 -05:00
Greyson Parrelli
c03a183904 Fix transaction issue on backup restore. 2022-12-12 10:03:15 -05:00
Greyson Parrelli
a2893fbec7 Fix possible null column crash in V166 migration.
Fixes #12672
2022-12-12 09:30:49 -05:00
Alex Hart
19cbace33d Fix group search predicate causing crashing when creating group story. 2022-12-12 10:26:22 -04:00
Alex Hart
8a78481cca Bump version to 6.5.2 2022-12-09 14:34:51 -04:00
Alex Hart
e1fd254d15 Updated language translations. 2022-12-09 14:16:58 -04:00
Alex Hart
019219f1e1 Rotate paypal one-time flag. 2022-12-09 14:02:27 -04:00
Greyson Parrelli
ad3c04cb52 Fix ambiguous column in query. 2022-12-09 11:10:30 -05:00
Greyson Parrelli
61f9dc7498 Fix possible issue with reproducible builds.
- Needed to update apkdiff.py to ignore some new app-signing-related
  files.
- While I was in there, I cleaned up the script a lot to make it easier
  to read as well as extract files that didn't match.
- We also need to guarantee git hashes are the same length -- the script
  we were calling might provide hashes of different length depending on
  how you checked out the code.

Co-authored-by: inthewaves<26474149+inthewaves@users.noreply.github.com>
2022-12-09 08:53:17 -05:00
Alex Hart
4deb16a37a Bump version to 6.5.1 2022-12-08 14:20:33 -04:00
Alex Hart
4129151bd2 Updated language translations. 2022-12-08 14:17:26 -04:00
Cody Henthorne
10cf431537 Revert " Enable kotlin for libsignal-service project and convert SignalServiceDataMessage."
This reverts commit fc2b67aa0f.
2022-12-08 13:07:24 -05:00
Alex Hart
011dd2d973 Fix issue where gift receipt showed boost badge. 2022-12-08 13:45:44 -04:00
Alex Hart
c85c4c5020 Bump version to 6.5.0 2022-12-08 12:20:36 -04:00
Alex Hart
5f1439df00 Updated language translations. 2022-12-08 12:11:48 -04:00
Cody Henthorne
e76bec63a3 Remote ring small groups feature flag. 2022-12-08 12:07:02 -04:00
Cody Henthorne
fc2b67aa0f Enable kotlin for libsignal-service project and convert SignalServiceDataMessage. 2022-12-08 12:07:02 -04:00
Alex Hart
bcd0360dd0 Remove obselete unused dexOptions. 2022-12-08 12:07:02 -04:00
Cody Henthorne
04bf2cd0c2 Ignore decomissioned KBS enclaves when encountered during getToken. 2022-12-08 12:07:02 -04:00
Nicholas
aba51da932 Ensure view binding is valid after Media Preview animations. 2022-12-08 12:07:02 -04:00
Nicholas
f8520d83be Add null checks for FABs in conversation list.
Fixes #12651.
2022-12-08 12:07:02 -04:00
Greyson Parrelli
69003dfbe2 Convert IdentityTable to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
380b377ed8 Ensure we rotate storage id when applying hidden story state or username. 2022-12-08 12:07:02 -04:00
fm-sys
4c5db983e3 Make voice messages long-clickable.
Fixes #12658
2022-12-08 12:07:02 -04:00
Greyson Parrelli
48c887ac03 Add gradle test devices. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
f207a82d2f Show smaller quote chains within larger quote chains. 2022-12-08 12:07:02 -04:00
Cody Henthorne
56f6888d49 Update kotlin to 1.7.20 2022-12-08 12:07:02 -04:00
Alex Hart
66ece479f6 Update access modifiers. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
c1cc2b064c Convert SenderKeyTable to kotlin. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
98980b8192 Convert SenderKeySharedTable to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
79ec76f11f Update tooltip to behave better when content is at edge of screen. 2022-12-08 12:07:02 -04:00
Cody Henthorne
45a1c5c369 Fix mention crash with overlapping ranges. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
2dc41f319c Convert RemappedRecordTables to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
2cdb1b8300 Fix issue where story thumb could show as a chat image preview. 2022-12-08 12:07:02 -04:00
Alex Hart
e846b4e20a Add better onBack handling for donations webviews. 2022-12-08 12:07:02 -04:00
Alex Hart
961057f620 Implement PayPal confirm donation sheet. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
e686a09ce4 Convert GroupReceiptTable to kotlin. 2022-12-08 12:07:02 -04:00
Greyson Parrelli
fc8cf2957f Convert DraftTable to kotlin. 2022-12-08 12:07:02 -04:00
Alex Hart
0bef37bfc1 Add minimum amount error for boosts. 2022-12-07 13:03:02 -05:00
Cody Henthorne
1618141342 Fix view flicker when switching between keyboard and attachment/emoji keyboards. 2022-12-07 13:03:02 -05:00
Alex Hart
d7fb05f596 Fix integration tests. 2022-12-07 13:03:02 -05:00
Greyson Parrelli
2eb15cc8e3 Convert SearchTable to kotlin. 2022-12-07 13:03:02 -05:00
Alex Hart
424a0233c2 Implement refactor to utilize new donation configuration endpoint. 2022-12-07 13:03:02 -05:00
Alex Hart
40cf87307a Add improved handling for credit card errors. 2022-12-07 13:03:02 -05:00
Sgn-32
643206b946 SubmitDebugLogActivity progress dialog make-over.
Fixes #12656
2022-12-07 13:03:02 -05:00
Varsha
cc95041519 Fix navigation after sending payment from conversation. 2022-12-07 13:03:02 -05:00
Cody Henthorne
45b498f62f Remove unused resources. 2022-12-07 13:03:02 -05:00
Sgn-32
9e6d78ba5f Enable hyphenation on conversation settings buttons.
Closes #12609
2022-12-07 13:03:02 -05:00
Greyson Parrelli
95eba78d9c Improve constraints on thread and message tables. 2022-12-07 13:03:02 -05:00
Alex Hart
5d9f00b268 Fix issue when copying attachment data. 2022-12-07 13:03:02 -05:00
Alex Hart
6a01388e82 Ignore start/end clipping when directed to do so by transform properties. 2022-12-07 13:03:02 -05:00
Ehren Kret
2ef6f78d39 Remove some unused code in ConversationAdapter. 2022-12-07 13:03:02 -05:00
Alex Hart
a754c39599 Bump version to 6.4.2 2022-12-07 10:29:19 -04:00
Alex Hart
14622cd06c Updated language translations. 2022-12-07 10:29:02 -04:00
Cody Henthorne
3132cd1198 Drop group call rings for large groups. 2022-12-06 22:21:14 -05:00
Cody Henthorne
94c35d86e2 Update post translation qa tasks. 2022-12-06 15:11:20 -05:00
Cody Henthorne
3c2c6d782a Revert "Clear formatting when pasting text."
This reverts commit 77be721f5a.

If pasting an image will crash the application, does not handle pasting
via multiple other methods like quick suggestion or via a clipboard
manager like provided by Samsung via their keyboard.
2022-12-06 13:38:39 -05:00
Cody Henthorne
1764b21214 Fix crash when opening notification settings. 2022-12-06 13:11:22 -05:00
Greyson Parrelli
260e572071 Fix bug where disappearing timer was applied to sent group stories. 2022-12-05 17:36:57 -05:00
Greyson Parrelli
54251a27a8 Do not show stories for inactive groups. 2022-12-05 17:20:58 -05:00
Alex Hart
88a8430c31 Bump version to 6.4.1 2022-12-02 13:54:13 -04:00
Alex Hart
678b653873 Updated language translations. 2022-12-02 13:50:44 -04:00
Greyson Parrelli
21592ca5c0 Do not include archived messages in unread count. 2022-12-02 12:38:23 -05:00
gitstart
1bca2f06bd Pause voice memos when you open a video.
Fixes #11156.

Signed-off-by: Nicholas Tinsley <nicholas@signal.org>
2022-12-02 10:47:38 -05:00
Alex Hart
9f166105a6 Remove tinting when forwarding content. 2022-12-02 11:04:31 -04:00
Alex Hart
ea08b59e6b Fix error routing for credit cards. 2022-12-02 11:00:22 -04:00
Alex Hart
9aca0af22c Fix issue with poor sent video viewing behavior. 2022-12-02 10:43:31 -04:00
Alex Hart
591d8c3d1a Separate PayPal flags into one-time and recurring. 2022-12-02 09:13:58 -04:00
Nicholas
22b73494a7 Rename *Database androidTest classes to *Table. 2022-12-01 18:15:37 -05:00
Nicholas
9bb80077c6 Fix jumping from media to message in group converstations. 2022-12-01 18:15:09 -05:00
Cody Henthorne
646f41663f Fix in-chat payment message rendering with long note. 2022-12-01 10:20:27 -05:00
Cody Henthorne
63cca2de66 Bump version to 6.4.0 2022-11-30 20:16:51 -05:00
Cody Henthorne
16361ac489 Updated language translations. 2022-11-30 20:05:03 -05:00
Cody Henthorne
e8f39e8f71 Fix in-chat payment view not updating properly. 2022-11-30 19:58:47 -05:00
Alex Hart
7945b3c971 Fix story sync message behaviour between iOS and Android. 2022-11-30 17:10:36 -05:00
Cody Henthorne
e5d196c642 Log backup verify failure independently from file not found. 2022-11-30 17:10:36 -05:00
Alex Hart
979f87db78 Add initial PayPal implementation behind a feature flag. 2022-11-30 17:10:36 -05:00
Alex Hart
b70b4fac91 Inline gift receive flag. 2022-11-30 17:10:36 -05:00
Nicholas
031d7b9cb0 Remove shrinking animation from opening media preview bottom bar. 2022-11-30 17:10:36 -05:00
Nicholas
c68859c606 Convert registration button to Tonal colorway. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
23804046c6 Always use new foreground service utils. 2022-11-30 17:10:36 -05:00
Alex Hart
7b13550086 Add entry points for adding to a group story. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
7949996c5c Renamed database classes to table classes.
Because they're not databases. They're tables.
2022-11-30 17:10:36 -05:00
Nicholas
b190f9495a Only show "Delete Everywhere" with linked devices.
This applies to Note To Self.
2022-11-30 17:10:36 -05:00
Jim Gustafson
b4c0635a63 Update to RingRTC v2.22.0
Co-authored-by: Jordan Rose <jrose@signal.org>
2022-11-30 17:10:36 -05:00
Nicholas
21bd8a308b Add jump to message shortcut for media viewer. 2022-11-30 17:10:36 -05:00
Nicholas
800405fc3e Add background drawable for play/pause buttons. 2022-11-30 17:10:36 -05:00
Alex
bf18db354c Explicitly declare permissions in Github workflows.
Closes #12476

Signed-off-by: Alex <aleksandrosansan@gmail.com>
2022-11-30 17:10:36 -05:00
Greyson Parrelli
e0b89bedd4 Removed some unused log classes. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
504b7ad5b3 Remove unsupported languages. 2022-11-30 17:10:36 -05:00
Nicholas
0558808370 Unmute Stories when ringer mode changed. 2022-11-30 17:10:36 -05:00
Nicholas
cff3840c51 Show AlertDialogs for registration errors. 2022-11-30 17:10:36 -05:00
Nicholas
a46fc96ff1 Improve media album rail entrance animation. 2022-11-30 17:10:36 -05:00
gitstart
77be721f5a Clear formatting when pasting text.
Fixes #8058
Closes #12614
2022-11-30 17:10:36 -05:00
Greyson Parrelli
023b181917 Update backup passphrase layout spacing.
Fixes #12623
2022-11-30 17:10:36 -05:00
Sgn-32
311ef0d65b Fix video call icons in ConversationListItem.
Closes #12618
2022-11-30 17:10:36 -05:00
Cody Henthorne
74314e08ac Show notification when mentioned in a group story reply. 2022-11-30 17:10:36 -05:00
Rashad Sookram
81df9fcddb Default to staging SFU on staging builds. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
ff64c2a911 Add more locking around attachment deletions. 2022-11-30 17:10:36 -05:00
Cody Henthorne
8a9605ade8 Fix crash when handling expired call offers. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
7a449a971f Update rate limit handling for CDS. 2022-11-30 17:10:36 -05:00
Cody Henthorne
258951dea8 Show excluded count in Story privacy settings overview. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
cdff0a61f2 Change chat badge to show total unread message count. 2022-11-30 17:10:36 -05:00
Alex Hart
2200af9c31 Remove background highlighting from empty lines in image editor.
Co-Authored-By: GitStart <1501599+gitstart@users.noreply.github.com>

Fixes #12612
2022-11-30 17:10:36 -05:00
Cody Henthorne
dfb913cb98 Fix thread update with drafts bugs.
* Fix thread not updating correctly when drafts are present.
* Fix thread delete bug during first message drafting.
2022-11-30 17:10:36 -05:00
Varsha
9ee10512fb Update enclave measurements to v3.0.0 for testnet. 2022-11-30 17:10:36 -05:00
Greyson Parrelli
81c10a1eae Lazily initialize NotificationChannels. 2022-11-30 17:10:36 -05:00
Nicholas Tinsley
3e8b5ca91d Allow remote delete from Media Preview menu. 2022-11-30 17:10:35 -05:00
Cody Henthorne
ba0b0cdefa Bump version to 6.3.6 2022-11-30 17:00:07 -05:00
Alex Hart
f00ee0a226 Fix issue preventing subscriptions from processing. 2022-11-30 16:48:22 -05:00
Cody Henthorne
bd4a69eddc Bump version to 6.3.5 2022-11-29 14:08:09 -05:00
Cody Henthorne
8c95b37826 Updated language translations. 2022-11-29 14:05:17 -05:00
Alex Hart
133d3145d1 Fix error with syncing of remote deletion of stories. 2022-11-29 14:48:08 -04:00
Cody Henthorne
4a0db31103 Bump version to 6.3.4 2022-11-29 11:49:32 -05:00
Cody Henthorne
ce85bb1575 Updated language translations. 2022-11-29 11:39:38 -05:00
Alex Hart
eee4ff3f87 Add new error strings for credit cards. 2022-11-29 11:01:07 -04:00
Greyson Parrelli
f6356c9720 Never show stories from blocked users. 2022-11-28 20:40:50 -05:00
Alex Hart
42d2d415d6 Clean up keyboard fragment when view is detached from window. 2022-11-28 13:01:58 -04:00
Alex Hart
683247bf98 Cleanly exit on KeepAlive 409. 2022-11-28 12:47:25 -04:00
Alex Hart
d7404cf32f Prevent empty or all-whitespace string from being sent as a gift message. 2022-11-28 12:02:25 -04:00
Greyson Parrelli
ec1f771364 Bump version to 6.3.3 2022-11-24 22:35:51 -05:00
Greyson Parrelli
95ac9628fb Updated language translations. 2022-11-24 22:35:30 -05:00
Cody Henthorne
ba68d795af Fix megaphone donate crash. 2022-11-24 22:33:14 -05:00
Alex Hart
245f7d3e03 Bump version to 6.3.2 2022-11-18 17:02:44 -04:00
Alex Hart
972ce41689 Updated language translations. 2022-11-18 16:54:16 -04:00
Alex Hart
be12a17ff7 Add handling for payment_intent with missing status. 2022-11-18 13:22:30 -04:00
Alex Hart
0c615e2fc2 Bump version to 6.3.1 2022-11-17 16:43:49 -04:00
Alex Hart
6829257a83 Updated language translations. 2022-11-17 16:39:38 -04:00
Nicholas
b7b7a04fad Improve animations for video seekbar. 2022-11-17 15:33:15 -05:00
Cody Henthorne
50084f8f73 Fix debuglog system info formatting bug. 2022-11-17 11:54:51 -05:00
Alex Hart
04e8235cfc Add group stories education sheet. 2022-11-17 12:35:17 -04:00
Alex Hart
0df3096241 Fix issue where gallery image was overlapped by count. 2022-11-17 12:18:32 -04:00
Alex Hart
29f22d515a Set story image post minimum duration to 5s. 2022-11-17 12:13:07 -04:00
Alex Hart
9931496b0f Fix crash when toggling pills. 2022-11-17 12:06:36 -04:00
Alex Hart
950363a4e9 Don't wrap donation errors. 2022-11-17 11:07:20 -04:00
Alex Hart
3469e8d0e0 Set brightness to 66% when taking a selfie. 2022-11-17 10:02:02 -04:00
Alex Hart
586339575f Fix menu visibility for chat filters. 2022-11-16 16:53:27 -04:00
Varsha
807a0e02a2 Fix memory leak in payment transfer fragment. 2022-11-16 15:11:09 -05:00
Cody Henthorne
afb2b1a1a2 Do not include self in exported SMS threads. 2022-11-16 14:18:57 -05:00
Alex Hart
a8946961d5 Bump version to 6.3.0 2022-11-16 15:14:49 -04:00
Alex Hart
026aaac451 Updated language translations. 2022-11-16 15:10:26 -04:00
Alex Hart
159f319d77 Update caption bar readability in stories. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
cf00995b6f Guarantee table export order is valid. 2022-11-16 15:05:47 -04:00
Cody Henthorne
7c60c32918 Add re-export SMS support and hard code Phase 0. 2022-11-16 15:05:47 -04:00
Cody Henthorne
fd1d2ec8fc Ignore group ring requests if we are already in the call. 2022-11-16 15:05:47 -04:00
Alex Hart
a11c40e4fe Add credit card support to badge gifting. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
1eb2f51398 Convert AVIF files to jpegs. 2022-11-16 15:05:47 -04:00
Nicholas
13ed122c3e Null check RecyclerView references in search bar callbacks. 2022-11-16 15:05:47 -04:00
Alex Hart
fa02ee1d3d Skip re-emission of duplicate StoryPosts. 2022-11-16 15:05:47 -04:00
Alex Hart
4908e39308 Skip prefetch call if no stories need to be cached. 2022-11-16 15:05:47 -04:00
Alex Hart
ad001d585e Utilize center-inside transform to ensure proper downsampling of cached images. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
3fd5e55363 Improve RecipientDatabase tests. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
ebc1bc3f7f Fix issue where non-ascii characters didn't show inline emoji suggestions.
Fixes #12579
2022-11-16 15:05:47 -04:00
Cody Henthorne
c51e13fd30 Ignore rings from non-admins in announcement only groups and rev feature flag. 2022-11-16 15:05:47 -04:00
Nicholas
fd37613f2f Don't fade in media preview controls if hidden. 2022-11-16 15:05:47 -04:00
Greyson Parrelli
eb921f3103 Don't show megaphones in landscape. 2022-11-16 15:05:47 -04:00
Varsha
d5b6c47670 Fix memory leak in payments home. 2022-11-16 15:05:47 -04:00
Varsha
a4494b58f0 Fix memory leaks in payments home and confirm payment view models. 2022-11-16 15:05:47 -04:00
Varsha
b0c68b12ed Fix memory leak in create payment fragment. 2022-11-16 15:05:47 -04:00
Varsha
b47e5f2fa9 Fix memory leak in contact selection list. 2022-11-16 15:05:47 -04:00
Alex Hart
bba1315906 Add chat filter support behind a flag. 2022-11-16 15:05:47 -04:00
Alex Hart
3e2ecdaaa9 Add blur hashes behind videos. 2022-11-15 16:26:19 -04:00
Nicholas
fb8e81cf50 Center selected item in media rail.
Fixes #12582
2022-11-15 16:26:19 -04:00
Cody Henthorne
52a5fb8ea2 Fix crash when showing a message with a button without media. 2022-11-15 16:26:19 -04:00
Alex Hart
b2f3867b0b Add dynamic duration to stories with captions. 2022-11-15 16:26:19 -04:00
Alex Hart
45ca3bd7cf Show default gallery icon if permissions is disabled or media is not available. 2022-11-15 16:26:19 -04:00
Alex Hart
74b7057608 Brighten camera screen if under 66%. 2022-11-15 16:26:19 -04:00
Robotwombat
3a060c7a79 Update some info on the README.
* Removed the mention of SMS/MMS support.
* Replaced the Signal description with some direct text from either Signal's Play Store listing or from signal.org
* Fixed some capitalization errors
* Replaced "Open Whisper Systems" with "Signal" in the 'Contributing Ideas' section

Closes #12597
2022-11-15 16:26:19 -04:00
Jim Gustafson
de426d22bf Update to RingRTC v2.21.5 2022-11-15 16:26:19 -04:00
Alex Hart
14549fd401 Fix issue where SystemWindwInsetsSetter didn't respect type on older API levels. 2022-11-15 16:26:19 -04:00
Alex Hart
1ff16a2c18 Bump version to 6.2.3 2022-11-15 16:08:06 -04:00
Alex Hart
0174af7b9b Updated language translations. 2022-11-15 16:06:48 -04:00
Alex Hart
e7f1d3fc1a Add JsonCreator annotation to data class constructors. 2022-11-15 15:14:55 -04:00
Alex Hart
09afb1be41 Bump version to 6.2.2 2022-11-14 12:48:03 -04:00
Alex Hart
ad2ebfb389 Updated language translations. 2022-11-14 12:45:03 -04:00
Alex Hart
85d7a5c6cc Rotate Credit Cards flag for Beta. 2022-11-14 12:23:10 -04:00
Nicholas Tinsley
4fbbc9d395 Show thumbnail rail when viewing a thread's media. 2022-11-14 11:22:34 -05:00
Alex Hart
e3954ab5e8 Utilize logic from lottie to determine animation scale. 2022-11-14 10:49:55 -04:00
Alex Hart
c1b19390a2 Add 48dp padding to end of gift add message input. 2022-11-14 10:04:02 -04:00
Alex Hart
f7e4e9c855 Fix crash when user does not have a subscription. 2022-11-14 09:59:09 -04:00
Alex Hart
5c6f709faa Do not pre-select my story privacy state. 2022-11-14 09:52:46 -04:00
Alex Hart
47f1d3f594 Add default values to global duration scale resolution. 2022-11-11 13:57:24 -04:00
Greyson Parrelli
2b10f93718 Update hint text for group story replies. 2022-11-11 12:29:36 -05:00
Greyson Parrelli
ccee7577f7 Do not double-insert change number events. 2022-11-11 12:14:13 -05:00
Greyson Parrelli
4e871e2dd8 Bump version to 6.2.1 2022-11-11 10:42:24 -05:00
Greyson Parrelli
455da6649b Updated language translations. 2022-11-11 10:41:52 -05:00
Greyson Parrelli
dc4acd83e8 Fix typo in string. 2022-11-11 10:41:14 -05:00
Alex Hart
0e3a9a3130 Finalize credit card copy. 2022-11-11 10:35:55 -05:00
Greyson Parrelli
ed2edc1ebb Do no double-process the CDSI response. 2022-11-11 10:34:40 -05:00
Greyson Parrelli
6f4de36c6f Bump version to 6.2.0 2022-11-10 17:01:32 -05:00
Greyson Parrelli
e10696b44e Updated language translations. 2022-11-10 17:00:51 -05:00
Alex Hart
6ed1c21a66 Add in new donations strings for credit card support. 2022-11-10 16:58:25 -05:00
Alex Hart
263f7ebac5 Trim zeroes in subscription row items. 2022-11-10 16:58:25 -05:00
Alex Hart
c3063b721d Allow restricted users to update or cancel their subscription. 2022-11-10 16:58:25 -05:00
Cody Henthorne
1dc29fda12 Add in-chat payment messages. 2022-11-10 16:58:25 -05:00
Cody Henthorne
28193c2f61 Allow all notifications to be cancelled when bubbles are disabled. 2022-11-10 16:58:25 -05:00
Alex Hart
9d71c4df81 Refactor a large portion of the payments code to prep it for PayPal support. 2022-11-10 16:58:25 -05:00
Greyson Parrelli
c563ef27da Add UX for handling CDS rate limits. 2022-11-10 16:58:25 -05:00
Cody Henthorne
8eb3a1906e Fully delete remotely deleted stories after sending or on receive. 2022-11-10 10:47:14 -05:00
Cody Henthorne
0309f9ea89 Change destination for remote donation megaphones. 2022-11-10 10:46:57 -05:00
Nicholas
d678341399 Wrap DefaultAudioSink to tolerate init errors. 2022-11-10 10:15:10 -05:00
Nicholas
99f8ba5e0c Enable icons in overflow menu. 2022-11-10 09:37:32 -05:00
Alex Hart
c69b91c4db Add blocked regions from global config for donations payments. 2022-11-10 10:17:13 -04:00
Alex Hart
8f56c1baa5 Add new CC icon for dark mode. 2022-11-09 19:26:48 -05:00
Alex Hart
a0d4026e40 Enable screenshot security for CC fragment. 2022-11-09 19:26:48 -05:00
Cody Henthorne
975b242a08 Fix notification not showing after thread with custom notification is deleted. 2022-11-09 19:26:48 -05:00
Nicholas
f1fafa6516 Gain temporary audio focus during voice memo recording. 2022-11-09 19:26:48 -05:00
Nicholas
fca412b47d Pause videos/GIFs when sharing or forwarding. 2022-11-09 19:26:48 -05:00
Cody Henthorne
18c32a7a80 Only allow active groups to start ringing. 2022-11-09 19:26:48 -05:00
Nicholas
f96c31b38f Always allow remote delete in note to self. 2022-11-09 19:26:48 -05:00
Alex Hart
65a4ef2f70 Update donation strings. 2022-11-09 19:26:48 -05:00
Alex Hart
2b685ea89f Inline the stories flag. 2022-11-09 19:26:48 -05:00
Cody Henthorne
b55954380d Bump various Google Play Services dependencies. 2022-11-09 19:26:48 -05:00
Greyson Parrelli
739a8e9451 Add PNP change number insert events and tests. 2022-11-09 19:26:48 -05:00
Alex Hart
433b5ebc13 Flip case for donation method availability. 2022-11-09 19:26:48 -05:00
Alex Hart
018bb49a03 Cancel boost animations in onStop. 2022-11-09 19:26:48 -05:00
Alex Hart
fc145d7367 Increase height of boost items to align with subscriptions. 2022-11-09 19:26:48 -05:00
Alex Hart
c5f05f322f Rotate Credit Card payments flag. 2022-11-09 19:26:48 -05:00
Greyson Parrelli
b419eb4cd5 Inline internal-only strings. 2022-11-09 19:26:48 -05:00
Alex Hart
9bdf65c4e4 Add androidTest for inserting a direct reply via MessageContentProcessor. 2022-11-09 19:26:48 -05:00
Alex Hart
dbbae7f13f Fix a few flaky instrumentation tests to ensure suite passes. 2022-11-09 19:26:48 -05:00
Alex Hart
513228b366 Update text spacing on donations page. 2022-11-09 19:26:48 -05:00
Greyson Parrelli
a2415261bd Pair usernames flag with the PNP flag. 2022-11-09 19:26:48 -05:00
Alex Hart
8f06381239 Centralize where we make decisions about donations availability. 2022-11-09 19:26:48 -05:00
Alex Hart
f6f1fdb87d Mark unexpected cancellation when silenced so we do not keep hammering the logs. 2022-11-09 19:26:48 -05:00
Alex Hart
b8e16353ab Donations credit card formatting. 2022-11-09 19:26:48 -05:00
Alex Hart
16cbc971a5 Add small amount of unit testing for MessageContentProcessor. 2022-11-09 19:26:47 -05:00
Alex Hart
d1df069669 Add support for Credit Card 3DS during subscriptions. 2022-11-09 19:26:47 -05:00
Greyson Parrelli
844480786e Bump version to 6.1.3 2022-11-09 18:15:01 -05:00
Greyson Parrelli
77aa0424fd Updated language translations. 2022-11-09 18:15:01 -05:00
Alex Hart
4d94d9d968 Utilize areAnimatorsEnabled on API levels that support it. 2022-11-09 18:15:01 -05:00
Greyson Parrelli
89fca76327 Bump version to 6.1.2 2022-11-08 17:38:55 -05:00
Greyson Parrelli
14b9518a48 Updated language translations. 2022-11-08 17:38:55 -05:00
Greyson Parrelli
512ba2b0a8 Show bottom sheet when you tap an avatar in the story viewer. 2022-11-08 17:38:55 -05:00
Cody Henthorne
9851bc300e Fix mentions with share to single group story flow. 2022-11-08 17:38:55 -05:00
Nicholas
a81a4cdb53 Adjust stories view receipts button destination. 2022-11-08 17:38:55 -05:00
Alex Hart
b1d1aee373 Fix infinite animation loop when system animations are disabled. 2022-11-08 17:38:55 -05:00
Cody Henthorne
2cfa31a9b0 Fix crash when typing @ in story add message. 2022-11-07 22:39:54 -05:00
Nicholas
67b6cd164e Manually dismiss keyboard on forwarding messages. 2022-11-07 12:23:26 -05:00
Greyson Parrelli
f241a51fe1 Hopeful fix for crash on API 19. 2022-11-07 11:22:31 -05:00
Nicholas
74c542099a Autoplay all videos. 2022-11-07 09:15:39 -05:00
Nicholas
5d76f13c51 Increase touch target height of seekbar. 2022-11-07 09:15:24 -05:00
Nicholas Tinsley
c6d38600ec Restore wrap_content for album rail. 2022-11-04 17:39:33 -04:00
Cody Henthorne
fc3db538bc Bump version to 6.1.1 2022-11-04 16:38:01 -04:00
Cody Henthorne
acbccc32a6 Updated language translations. 2022-11-04 16:12:29 -04:00
Cody Henthorne
97a502c8c7 Restrict max threads used for large group profile fetching. 2022-11-04 16:08:31 -04:00
Greyson Parrelli
bdba048bc4 Remove possible transaction from identity cache read. 2022-11-04 16:08:30 -04:00
Greyson Parrelli
f7adf2ee5a Fix a typo in a group string. 2022-11-04 16:08:30 -04:00
Alex Hart
dcc9b8ca66 Fix issue with window insets in API30.
Fixes #12525
2022-11-04 16:08:30 -04:00
Nicholas
7ad6d95b27 Fade in media detail view. 2022-11-04 16:08:30 -04:00
Greyson Parrelli
2856697109 Fix string apostrophe. 2022-11-04 16:08:30 -04:00
Nicholas
af89d85696 Fade out video player controls on playback.
2 second delay, cancelable if the video is paused or finished.
2022-11-04 16:08:30 -04:00
Alex Voloshyn
c218e22566 Update MobileCoin enclave measurements for v3.0.0 2022-11-04 16:08:30 -04:00
Varsha
b38ac44d0f Prompt update on MobileCoin enclave failure. 2022-11-03 12:04:51 -04:00
Cody Henthorne
2709f0ee0d Bump version to 6.1.0 2022-11-02 15:51:37 -04:00
Cody Henthorne
c1f84adb2f Updated language translations. 2022-11-02 15:45:33 -04:00
Greyson Parrelli
8c6b7ecc4c Rotate stories feature flags. 2022-11-02 15:40:44 -04:00
Nicholas
5e25e8d0a2 Keep muted chats archived option. 2022-11-02 15:40:44 -04:00
Greyson Parrelli
c674d5b674 Fix bad compose box state if you leave while recording a voice note. 2022-11-02 15:31:52 -04:00
Alex Hart
377841db26 Update some keyboard action handling. 2022-11-02 15:31:52 -04:00
Alex Hart
5da7052da3 Fix blocked refresh and format argument. 2022-11-02 15:31:52 -04:00
Alex Hart
ffeb60fcdd Update tooltip to Material3 spec. 2022-11-02 15:31:52 -04:00
Cody Henthorne
e610ee419f Add internal user remote megaphone conditional. 2022-11-02 15:31:52 -04:00
Greyson Parrelli
d61a35b118 Fix layering issue with action buttons in the contact list. 2022-11-02 15:31:52 -04:00
Alex Hart
ac189865b9 Update pluralization of payments recovery dialog. 2022-11-02 15:31:52 -04:00
Alex Hart
8056aafc9d Pluralize gateway string. 2022-11-02 15:31:52 -04:00
Greyson Parrelli
8ab16164eb Fix PNP issue around thread merging. 2022-11-02 15:31:52 -04:00
Greyson Parrelli
473c8b199e Fix PNP CDS sync bug. 2022-11-02 15:31:52 -04:00
Greyson Parrelli
3692d87531 Add timeout for registering SMS listener during registration. 2022-11-02 15:31:52 -04:00
Cody Henthorne
99a516f8e5 Fix gv1 migration reminder bug.
Fixes #12554
2022-11-02 15:31:52 -04:00
Alex Hart
0ff4175538 Update design for the donation thanks dialog. 2022-11-02 15:31:52 -04:00
Nicholas Tinsley
4e8208c468 Restore LTR ordering for media preview control icons. 2022-11-02 15:31:52 -04:00
Alex Hart
84f0548966 Center story viewport on tall devices. 2022-11-02 15:31:52 -04:00
Cody Henthorne
77beeda62a Add in-chat payment activation requests.
Co-authored-by: Varsha <varsha@mobilecoin.com>
2022-11-02 15:31:52 -04:00
Greyson Parrelli
8c915572fb Fetch own username from the whoami endpoint. 2022-11-02 15:31:52 -04:00
Nicholas
53883ee3d3 Update MediaPreviewV2 design values. 2022-11-02 15:31:52 -04:00
Greyson Parrelli
40ca16bd06 Don't use ordinals when persisting PNP enum. 2022-11-02 15:31:52 -04:00
Jim Gustafson
60dcfb2fe6 Update to RingRTC v2.21.3 2022-11-02 15:31:52 -04:00
Alex Hart
123fb95916 Allow stripe error codes to be upgraded to decline codes. 2022-11-02 15:31:52 -04:00
Greyson Parrelli
1a657a7a19 Put info about data saver in the logs. 2022-11-02 15:31:52 -04:00
elena
f119496da4 Fix back button behaviour in bubbles.
Fixes #12563
2022-11-02 15:31:52 -04:00
Alex Hart
6999d1fbf1 Enforce max gif playback using unreserved count from exo pool. 2022-11-02 15:31:52 -04:00
Alex Hart
c1ff2aeeff Print pool stats whenever we fail to get an ExoPlayer instance. 2022-11-02 15:31:52 -04:00
Alex Hart
4220395649 Tie into dispatcher instead of popBackStack() 2022-11-02 15:31:52 -04:00
Alex Hart
806409b329 Fix crash when entering avatar picker on kitkat. 2022-11-02 15:31:52 -04:00
Greyson Parrelli
3e3296da5b Convert ThreadDatabase to kotlin. 2022-11-02 15:31:52 -04:00
Alex Hart
4bbe01cbc3 Hide stories file size header for text stories. 2022-10-31 13:47:33 -04:00
Cody Henthorne
c357c35303 Add remote megaphone snooze capabilities. 2022-10-31 13:47:33 -04:00
Alex Hart
2ea5c7e3bc Update google pay button to match new styling. 2022-10-31 13:39:33 -04:00
Alex Hart
5b8a729afc Add credit card icon. 2022-10-31 13:39:33 -04:00
Greyson Parrelli
4077dc829a Improve contact pull-to-refresh performance. 2022-10-31 13:39:33 -04:00
Alex Hart
2cfa685ae2 Add basic 3DS support for credit cards. 2022-10-31 13:39:33 -04:00
Cody Henthorne
c686d33a46 Bump version to 6.0.6 2022-10-31 13:16:27 -04:00
Cody Henthorne
e00ed81e7c Fix bad unread mentions database migration. 2022-10-31 12:52:50 -04:00
Cody Henthorne
06c9dbe6ec Bump version to 6.0.5 2022-10-31 11:38:48 -04:00
Cody Henthorne
05377d26de Updated language translations. 2022-10-31 11:27:05 -04:00
Cody Henthorne
b781de2c17 Fix sms megaphone bug. 2022-10-31 10:49:48 -04:00
Nicholas
9f2c7a65ac Fix lifecycle state for media preview.
After a fragment is destroyed, the media remains loaded in the view model, and it is up to the re-created fragment to take that loaded data and make it ready to be viewed.
2022-10-31 10:08:14 -04:00
Nicholas
bae070e60e Remove old MediaPreviewActivity. 2022-10-31 09:23:11 -04:00
Nicholas
34f6d52758 Finish media preview activity if no media present. 2022-10-31 09:07:25 -04:00
Alex Hart
72aac0732c Bump version to 6.0.4 2022-10-28 17:54:15 -03:00
Alex Hart
5da6321c67 Updated language translations. 2022-10-28 17:53:22 -03:00
Alex Hart
4b9e4d739f Ignore warning for androidx transition. 2022-10-28 17:49:50 -03:00
Alex Hart
5d4d6db197 Fix story viewed state retention. 2022-10-28 17:49:50 -03:00
Cody Henthorne
4e3bfadfbe Fix media preview launched from conversation settings crash. 2022-10-28 17:49:50 -03:00
Alex Hart
abb0a25b81 Fix crash with disposable lifecycle. 2022-10-28 17:49:50 -03:00
Alex Hart
e369f56eab Fix various bugs with KitKat preventing stories from launching. 2022-10-28 17:49:50 -03:00
Alex Hart
a066271766 Bump version to 6.0.3 2022-10-27 18:03:36 -03:00
Alex Hart
c6eb241261 Updated language translations. 2022-10-27 18:00:31 -03:00
Greyson Parrelli
906441c90c Revert "Convert ThreadDatabase to kotlin."
This reverts commit 1e88fb428d.
2022-10-27 16:54:06 -04:00
Alex Hart
6f46e9000b Permanent attachment failure.
Co-authored-by: Cody Henthorne <cody@signal.org>
2022-10-27 16:33:33 -04:00
Alex Hart
9ef58516e2 Ensure donation error dialogs are shown from main thread. 2022-10-27 15:50:39 -03:00
Alex Hart
10950756d3 Add proper fallback photo for mystory. 2022-10-27 14:39:58 -03:00
Nicholas
7c4c146189 Add edit button for media preview. 2022-10-27 13:30:54 -04:00
Alex Hart
2f0f4f94a2 Set onboarding duration to 10s per story. 2022-10-27 14:18:10 -03:00
Alex Hart
3600a4818c Update first time navigation screen. 2022-10-27 13:43:52 -03:00
Nicholas
d003dc435a Design and animation updates for Media Preview. 2022-10-27 10:54:14 -04:00
Alex Hart
8e1ec5ab5b Bump version to 6.0.2 2022-10-26 17:01:03 -03:00
Alex Hart
14781c3aed Updated language translations. 2022-10-26 16:55:41 -03:00
Cody Henthorne
490e29f758 Remember scroll position for internal settings. 2022-10-26 16:50:54 -03:00
Alex Hart
a0c48bed6e Fix issue where story video does not stop playback when app is backgrounded. 2022-10-26 13:05:02 -03:00
Alex Hart
1011e4b7f5 Fix tinting of story privacy toolbar action in dark mode. 2022-10-26 12:13:13 -03:00
Nicholas
9602084125 Add auto-mirror param to vector assets. 2022-10-26 11:00:44 -04:00
Alex Hart
36fddbb79a Fix comparison causing hot loop on API25. 2022-10-26 11:09:47 -03:00
Alex Hart
85d5ea0382 Fix story reaction notification summary. 2022-10-26 10:32:28 -03:00
Alex Hart
b4d3690d3a Fix issue where incognito mode was not enabled in text story creation. 2022-10-26 10:23:52 -03:00
Nicholas Tinsley
529211c3a5 Fix bottom bar judder when hiding UI. 2022-10-25 17:06:35 -04:00
Nicholas Tinsley
2b4c01c106 Fix autoplay for videos in Media Preview. 2022-10-25 17:06:08 -04:00
Greyson Parrelli
168832c138 Fix stories index migration. 2022-10-25 16:42:16 -04:00
Alex Hart
07915db7bc Bump version to 6.0.1 2022-10-25 16:59:08 -03:00
Alex Hart
3cc1c39f81 Updated language translations. 2022-10-25 16:53:35 -03:00
Cody Henthorne
59de56439a Fix backup fails when running in background. 2022-10-25 16:48:42 -03:00
Nicholas
7759ad283d Media Preview V2 Visual Redesign. 2022-10-25 16:48:39 -03:00
Alex Hart
b8174c5e00 Remove duplicate key from FeatureFlags set. 2022-10-25 16:48:39 -03:00
Greyson Parrelli
9de6c44b16 Fix an issue with sharing file attachments into the app. 2022-10-25 16:48:39 -03:00
Rashad Sookram
738676ea5f Add calling dev server URL. 2022-10-25 16:48:37 -03:00
Alex Hart
09361b2d40 Fix crash when viewing views of a group story. 2022-10-25 09:46:56 -03:00
Alex Hart
6055515be9 Bump version to 6.0.0 2022-10-24 21:41:27 -03:00
Alex Hart
37ff750261 Updated language translations. 2022-10-24 21:38:02 -03:00
Alex Hart
bc97058ced Rotate story feature flags. 2022-10-24 21:22:50 -03:00
Greyson Parrelli
1e88fb428d Convert ThreadDatabase to kotlin. 2022-10-24 21:22:50 -03:00
Greyson Parrelli
d2b72fc8b7 Stop checking the change number capability.
It's been out for a year, no need to check at this point.
2022-10-24 21:03:12 -03:00
Nicholas
469cab284e Media Preview V2 Visual Redesign 2022-10-24 21:03:12 -03:00
Jordan Rose
6c0b63d72c Update libsignal-client to 0.21.1 2022-10-24 21:02:22 -03:00
Greyson Parrelli
1007b4d635 Reduce flakiness of our dependencies. 2022-10-24 21:02:22 -03:00
Nicholas
fb8b230442 Add scrolling to rationale prompt. 2022-10-24 21:02:22 -03:00
Nicholas
371267a1d3 Do not close gallery picker once 0 items selected.
This change only takes effect if the user navigates directly to the gallery picker.
2022-10-24 21:02:22 -03:00
Nicholas
084e806c25 Disable pager scrolling during 2 finger gestures. 2022-10-24 21:02:22 -03:00
Nicholas
32fbbf2b55 Add seek buttons for videos longer than 30s. 2022-10-24 21:02:22 -03:00
Nicholas
7f4e964ec8 Enable Media Preview V2. 2022-10-24 21:02:17 -03:00
Alex Hart
3fefc17582 Add new fade color for expiration sheet. 2022-10-24 21:01:41 -03:00
Greyson Parrelli
62d5777c39 Inline the RecipientMergeV2 flag. 2022-10-24 21:01:41 -03:00
Greyson Parrelli
367ff7c75c Always use CDSI. 2022-10-24 21:01:37 -03:00
Alex Hart
1174bc8e07 Credit card validator implementations and spec tests. 2022-10-24 21:00:18 -03:00
Nicholas
27c3607099 Ktformat QrMainActivity. 2022-10-24 21:00:18 -03:00
Nicholas
7088b1a302 Fix last media preview V2 UI glitches. 2022-10-24 21:00:18 -03:00
Greyson Parrelli
3826ac553d Remove the unused/deprecated WRITE_PROFILE permission. 2022-10-24 21:00:17 -03:00
Cody Henthorne
0819c8d2b9 Add inline selected emojis to the recently used list.
Fixes #12514
2022-10-24 21:00:17 -03:00
Cody Henthorne
341b8effcf Add unread mention badging to conversation list. 2022-10-24 21:00:17 -03:00
Cody Henthorne
ea9bf0ccd5 Fix QR processing resolution and allow front camera use for device linking. 2022-10-24 21:00:17 -03:00
Nicholas
3d14c05114 Surround phone numbers with LTR unicode mark.
This also removes the previous TextView LTR flag that I had initially added for a one-off.
2022-10-24 21:00:17 -03:00
Nicholas
3a78031a71 Show album rail when entering media preview from All Media. 2022-10-24 21:00:17 -03:00
Alex Hart
daa3721145 Add new joined donations screen. 2022-10-24 21:00:17 -03:00
Nicholas
c829fba332 Only show album rail on album messages. 2022-10-24 21:00:17 -03:00
Cody Henthorne
7fafa4d5e6 Ensure network call resources are closed. 2022-10-24 21:00:17 -03:00
Nicholas
1f581c074d Close Cursor in Media Preview V2. 2022-10-24 21:00:17 -03:00
Nicholas
556d267084 Fix Z-ordering of preview media. 2022-10-24 21:00:17 -03:00
Alex Hart
3a7be812eb Bump version to 5.53.6 2022-10-24 20:57:19 -03:00
Alex Hart
807e6d4e71 Updated language translations. 2022-10-24 20:52:52 -03:00
Alex Hart
c1c138ce49 Rotate story flags. 2022-10-24 20:48:22 -03:00
Alex Hart
a15e97cc06 Filter story info to just the relevant people in that specific dlist.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2022-10-24 20:48:20 -03:00
Alex Hart
48e0a00a8a Stop observing state updates after post sequence is completed. 2022-10-24 20:06:02 -03:00
Greyson Parrelli
b46e129c23 Fix issue with sending story viewed receipts. 2022-10-24 18:47:10 -04:00
Alex Hart
064f7abd92 Remove stories beta dialog. 2022-10-24 16:19:52 -03:00
Alex Hart
428ab65d8a Prevent expiry timers from being sent to distribution lists. 2022-10-24 14:07:44 -03:00
Alex Hart
94f072c5aa Ensure content is stopped during video player cleanup. 2022-10-24 12:58:53 -03:00
Cody Henthorne
91f0b75a80 Fix incorrect Phase 0 implementations. 2022-10-24 10:17:12 -04:00
Cody Henthorne
cb65347bb3 Bump version to 5.53.5 2022-10-20 22:06:17 -04:00
Cody Henthorne
41aad39c62 Updated language translations. 2022-10-20 21:58:46 -04:00
Cody Henthorne
390f6c2462 Fix lint issue with ViewBinderDelegate. 2022-10-20 21:53:45 -04:00
Cody Henthorne
163c7de327 Fix blur width for landscape oriented stories. 2022-10-20 21:52:55 -04:00
Cody Henthorne
08b7dcb1ee Add check and request for SMS read permission to perform export. 2022-10-20 21:52:55 -04:00
Alex Hart
dfdf68b7b5 Add support for separate story view receipt control.
This reverts commit 1046265d23.
2022-10-20 21:52:55 -04:00
Greyson Parrelli
9941ffe79c Updated KBS settings. 2022-10-20 21:52:54 -04:00
Greyson Parrelli
418083d0c7 Fix delete-for-everyone issue with stories. 2022-10-20 21:52:54 -04:00
Alex Hart
25ac462921 Update story explanation text. 2022-10-20 21:52:54 -04:00
Alex Hart
f401ee00a1 Update tap to add text. 2022-10-20 21:52:54 -04:00
Alex Hart
94bd3101c9 Add support for stories "seen" state. 2022-10-20 21:52:54 -04:00
Alex Hart
995a4ad6ec Add new name field design. 2022-10-20 21:52:54 -04:00
Alex Hart
36206dfa9a Remove Stories header. 2022-10-20 21:52:54 -04:00
Alex Hart
a176188c7d Add new iconography for custom stories. 2022-10-20 21:52:54 -04:00
Alex Hart
44f551acc5 Request layout after text changes to ensure content is properly sized. 2022-10-20 21:52:54 -04:00
Alex Hart
39c1939470 Rename private story as custom story. 2022-10-20 21:52:54 -04:00
Cody Henthorne
ba2d84005d Fix wrong hint flashing into the compose box. 2022-10-20 21:52:54 -04:00
Greyson Parrelli
f54f9b7011 Update libsignal-client to 0.21.0 2022-10-20 21:52:54 -04:00
Alex Hart
6600857259 Add new bottom bar icons. 2022-10-20 21:52:54 -04:00
Alex Hart
e465f35e50 Add new stories icon to settings. 2022-10-20 21:52:54 -04:00
Alex Hart
20eda03a5a Swap add to to just new. 2022-10-20 21:52:54 -04:00
Alex Hart
2315a1c632 Remove isFeatureAvailable and remove compatibility check. 2022-10-20 21:52:54 -04:00
Alex Hart
ca36eaacce Add support for separate story view receipt control.
This reverts commit 1046265d23.
2022-10-20 21:52:54 -04:00
Cody Henthorne
2f2711c9a3 Require valid link url for story test posts. 2022-10-20 17:50:09 -04:00
Greyson Parrelli
94f135ac38 Include emoji rendering info in debug log. 2022-10-20 17:50:09 -04:00
Greyson Parrelli
3687021051 Update logging to match desktop and iOS. 2022-10-20 17:50:09 -04:00
Cody Henthorne
a535b4f97c Reimplement Phase 0 for SMS removal. 2022-10-20 17:50:09 -04:00
Cody Henthorne
690e1e60ba Improve error reporting for SMS export. 2022-10-19 22:11:31 -04:00
Cody Henthorne
262f762d7f Bump version to 5.53.4 2022-10-18 17:07:00 -04:00
Cody Henthorne
59fe196fe0 Updated language translations. 2022-10-18 17:02:09 -04:00
Cody Henthorne
7fccbd44c0 Fix infinite export loop and improve general error handling. 2022-10-18 16:06:37 -04:00
Cody Henthorne
0d715d2c18 Cherry pick mentions badge database migration. 2022-10-18 14:26:55 -04:00
Cody Henthorne
f0e94ebbad Fix sms export crash with missing sms thread recipient. 2022-10-18 12:34:30 -04:00
Greyson Parrelli
b324db53d3 Fix crash listening to PSTN pickups on some devices.
We do this as a helpful thing to end a signal call if the user gets a
PSTN call, but some Xiaomi devices are crashing because they require
unique permissions. But we can just do it optimistically.
2022-10-18 09:59:36 -04:00
Cody Henthorne
a456c3fa32 Fix invite banner background color in wallpaper conversations. 2022-10-17 21:08:48 -04:00
Cody Henthorne
57151145d3 Update sms export copy to clarify intent in a few places. 2022-10-17 20:41:35 -04:00
Alex Hart
ff7dcd26c8 Fix issue where forwarded link preview would overwrite original message id. 2022-10-17 13:41:18 -03:00
Alex Hart
87c024e968 Add lifecycle observation to group replies state observer. 2022-10-17 13:41:18 -03:00
Alex Hart
b8665e41e8 Fix re-launch bug with ViewAllConnections dialog fragment. 2022-10-17 13:41:18 -03:00
Alex Hart
1d5a83668b Fix story name display in removal dialog. 2022-10-17 13:41:18 -03:00
Greyson Parrelli
ea6f6bf47d Bump version to 5.53.3 2022-10-17 11:46:18 -04:00
Greyson Parrelli
da4c9926cf Updated language translations. 2022-10-17 11:40:12 -04:00
Cody Henthorne
3b3dcdcb14 Fix crash from treating mms groups like Signal groups after sms is disabled.
Fixes #12534
2022-10-17 11:39:00 -04:00
Alex Hart
ba3dd79d4e Fix possible crash where tooltip is dismissed after fragment is detached. 2022-10-17 11:39:00 -04:00
Cody Henthorne
1006af7d8a Fix sms export crash with missing thread recipients. 2022-10-17 11:39:00 -04:00
fm-sys
0daed8f7d7 Disable scrolling in contact list while context menu is shown.
Fixes #12530
Closes #12531
2022-10-17 11:39:00 -04:00
Greyson Parrelli
741eb55562 Fix some SMS strings. 2022-10-17 11:39:00 -04:00
Alex Hart
2b0bf032d7 Do not autoplay videos. 2022-10-17 11:39:00 -04:00
Greyson Parrelli
00e70212c5 Catch possible phone permission exception on specific devices. 2022-10-17 11:39:00 -04:00
Greyson Parrelli
14a9e22b5e Recreate a storageId for self if one doesn't exist. 2022-10-17 11:39:00 -04:00
Alex Hart
7ce1f9463e Fix stories icon in dark mode.
Closes #12532

Co-authored-by: fm-sys <64581222+fm-sys@users.noreply.github.com>
2022-10-17 11:38:48 -04:00
Alex Hart
9bb834e9f5 Remove RTL swap of tapping edges in story viewer. 2022-10-14 10:40:26 -03:00
Alex Hart
957f8754e1 Fix poor handling of single tap touches. 2022-10-14 10:31:55 -03:00
Greyson Parrelli
6673df2514 Fix translation comments. 2022-10-13 20:51:59 -04:00
Greyson Parrelli
cd619833d1 Bump version to 5.53.2 2022-10-13 16:32:52 -04:00
Greyson Parrelli
ba7bfd7171 Updated language translations. 2022-10-13 16:32:51 -04:00
Cody Henthorne
033004719a Fix bug when scheudling backups. 2022-10-13 16:32:51 -04:00
Alex Hart
a0172ddb2f Autoscale story text so all content fits in viewer. 2022-10-13 16:32:51 -04:00
Cody Henthorne
b6db7e7af6 Add phased SMS removal UX. 2022-10-13 16:32:51 -04:00
Greyson Parrelli
8a238a66e7 Do not flag SKDMs for group stories as stories. 2022-10-13 16:32:51 -04:00
Alex Hart
de29fc047e Fix issue where last item in contact selection collection would not display. 2022-10-13 16:32:51 -04:00
Alex Hart
7cdaf988f2 Allow users to save text stories. 2022-10-13 10:21:00 -03:00
Alex Hart
43caec69e3 Update new story button. 2022-10-13 09:57:40 -03:00
Alex Hart
f533219bad Move stories setting to top level. 2022-10-13 09:27:34 -03:00
Alex Hart
2bbce6ad47 Revert "Do not remove onboarding story when disabling stories."
This reverts commit 8c76cead58.
2022-10-13 09:17:34 -03:00
Greyson Parrelli
a04c2c30b9 Bump version to 5.53.1 2022-10-12 15:46:19 -04:00
Greyson Parrelli
68c5f8e9ae Updated language translations. 2022-10-12 15:46:19 -04:00
Alex Hart
246fbc4ee9 Fix My Story row overlap. 2022-10-12 15:46:19 -04:00
Alex Hart
9480cd1b7b Add ability to hide sms tag in contact search config. 2022-10-12 15:46:19 -04:00
Alex Hart
220931d3df Pass through clip information to video player. 2022-10-12 15:46:19 -04:00
Alex Hart
8c76cead58 Do not remove onboarding story when disabling stories. 2022-10-12 15:46:19 -04:00
Alex Hart
da3623d7e6 Fix fast story tapping. 2022-10-12 15:46:18 -04:00
Alex Hart
f72c44c7c3 Do not unregister typeface result which can lead to a crash. 2022-10-12 15:46:18 -04:00
Alex Hart
d2523c2661 Disallow view-once if share selection is stories. 2022-10-12 15:46:18 -04:00
Cody Henthorne
7139f91997 Fix unread count separator and mark read when viewing behavior.
Fixes #12510
2022-10-12 10:40:50 -04:00
Greyson Parrelli
371d9e8f01 Update to targetSdk 31. 2022-10-12 10:03:38 -04:00
Cody Henthorne
a8e03e9bf2 Fix backup job background start restricitions with API31+. 2022-10-12 09:48:40 -04:00
Alex Hart
e1c6dfb73b Move story post display logic into a single fragment. 2022-10-12 10:02:27 -03:00
Cody Henthorne
96d60e11b0 Fix call participants button showing under status bar after returning from PIP. 2022-10-11 16:59:02 -04:00
Cody Henthorne
5662473c18 Fix bubble color wallpaper preview not updating for individuals. 2022-10-11 16:10:58 -04:00
Cody Henthorne
14dd71bf78 Prevent crash from poor implementation of camera apis. 2022-10-11 15:20:55 -04:00
Cody Henthorne
55d437e54b Make group updates not mark a thread as unread. 2022-10-11 15:13:17 -04:00
Greyson Parrelli
3d8f62ce9d Bump version to 5.53.0 2022-10-11 14:48:29 -04:00
Greyson Parrelli
19d029a643 Updated language translations. 2022-10-11 14:47:44 -04:00
Greyson Parrelli
e85ba03756 Rotate the stories flag. 2022-10-11 14:46:35 -04:00
Alex Hart
7315c991d5 Fix IndexOutOfBoundsException when rapidly tapping through stories. 2022-10-11 14:46:35 -04:00
Alex Hart
83d1ab2eb5 Fix crash in processing of distribution list recipients. 2022-10-11 14:46:35 -04:00
Alex Hart
1e491d0b51 Fix story link previews for broken urls. 2022-10-11 14:46:35 -04:00
Alex Hart
50a7c2ba5c Disable link preview button with empty input. 2022-10-11 14:46:35 -04:00
Alex Hart
4cc6bb4fbe Do not allow forwarding of content more than 13 lines long to stories. 2022-10-11 14:46:35 -04:00
Alex Hart
7477f3c319 Increse bottom gradient height to improve readability. 2022-10-11 14:46:35 -04:00
Alex Hart
1046265d23 Fix receipt handling issue for stories. 2022-10-11 14:46:35 -04:00
Alex Hart
7cc2029cd3 Finish the activity instead of delegating to onback dispatcher. 2022-10-11 14:46:35 -04:00
Alex Hart
71ca39fd4a Fix crash when calling dismissNow in onDestroy. 2022-10-11 14:46:35 -04:00
Greyson Parrelli
bfd2686610 Fix issue where some threads were invisibly unread.
Problem 1: We weren't marking threads read when we shared into them.
Problem 2: We hid the unread status of threads whose last message was
outgoing.

This addresses both. It's possible that 'fixing' problem 2 could result
in more threads being marked as read, but really that should just make
us aware so we can properly mark the thread as read.
2022-10-11 14:46:35 -04:00
Veniamin Vynohradov
c131fb500d Add 'detailed' conversation style to show full file names.
Fixes #12442
Closes #12463
2022-10-11 14:46:35 -04:00
Sgn-32
cdb7f07368 Add ability to clear the proxy address.
Closes #12499
2022-10-11 14:46:35 -04:00
Cody Henthorne
de329166d2 Always show remote participant when entering PIP mode. 2022-10-11 14:46:35 -04:00
Sgn-32
83ae613e9a Use MaterialAlertDialogBuilder in RatingManager.
Closes #12501
2022-10-11 14:46:35 -04:00
Nicholas
b342ce6874 Load media in preview earlier than target attachment. 2022-10-11 14:46:35 -04:00
Cody Henthorne
bef83e4c0c Remove unused context arguments in RecipientUtil. 2022-10-11 14:46:35 -04:00
Greyson Parrelli
db0bca00ec Bump version to 5.52.5 2022-10-11 14:30:45 -04:00
Greyson Parrelli
e3c38e635a Updated language translations. 2022-10-11 14:30:07 -04:00
Cody Henthorne
10d4063ecf Remove unused story string. 2022-10-11 14:23:47 -04:00
Cody Henthorne
c6e3c9dd35 Fix thread not updating after group creation. 2022-10-10 15:43:36 -04:00
Cody Henthorne
02d9cbe01b Fix export flow on small screens. 2022-10-10 14:37:18 -04:00
Cody Henthorne
68237df321 Fix ongoing call notification bug. 2022-10-10 13:25:10 -04:00
Greyson Parrelli
c82bf826e0 Bump version to 5.52.4 2022-10-07 18:45:12 -04:00
Greyson Parrelli
8fb404a492 Updated language translations. 2022-10-07 18:45:12 -04:00
Greyson Parrelli
437d6c7a52 Flag deletes and replies to group stories as stories. 2022-10-07 18:45:12 -04:00
Greyson Parrelli
30b635cca2 Allow SKDM's if story=true. 2022-10-07 18:45:12 -04:00
Greyson Parrelli
5fb0956c16 Improve an registration error log. 2022-10-07 18:45:12 -04:00
Greyson Parrelli
a9f654a520 Disable okhttp automatic retries for CDSI. 2022-10-07 18:45:12 -04:00
Alex Hart
4b10ec8f02 Allow story search in forward fragment to be case insensitive. 2022-10-07 18:45:12 -04:00
Alex Hart
02db5f74e9 Allow non-default emoji to animate in group replies. 2022-10-07 18:45:12 -04:00
Alex Hart
842626e96c Add viewer count and list to 'All Signal Connections'. 2022-10-07 18:45:12 -04:00
Alex Hart
c239ba1e35 Fix crash after replying to a group story. 2022-10-07 18:45:11 -04:00
Alex Hart
9aa7543f2f Do not display stories as valid selections when sending view-once media. 2022-10-07 18:45:11 -04:00
Alex Hart
5c77c33dff Fix flow colors. 2022-10-07 18:45:11 -04:00
Alex Hart
3dd31432c8 Allow getMessageDestination to handle Story messages. 2022-10-07 18:45:11 -04:00
Alex Hart
3de75f48cf Add padding to bottom of selection recycler. 2022-10-07 18:45:11 -04:00
Greyson Parrelli
be98ff3508 Fix bottom bar color in group story selector. 2022-10-07 18:45:11 -04:00
Greyson Parrelli
04b0c01015 Catch a foreground service start exception. 2022-10-07 18:45:11 -04:00
Cody Henthorne
50ded5c92a Rotate SMS exporter flag. 2022-10-07 18:45:11 -04:00
Alex Hart
2041756513 Story info page should mirror message details. 2022-10-07 18:45:11 -04:00
Greyson Parrelli
742d1bece0 Bump version to 5.52.3 2022-10-06 16:38:16 -04:00
Greyson Parrelli
4ee8218194 Updated language translations. 2022-10-06 16:38:16 -04:00
AsamK
22e97457a3 Fix sending normal group messages when falling back to socket.
In the sendGroupMessage message the socket fallback for sending normal
group messages always set the story parameter to true.
This causes the message to be discarded by the receivers, because it has
a story envelope, but no story content
> Envelope was flagged as a story, but it did not have any story-related content! Dropping.

Issue was introduced in 3895578d51

Closes #12496
2022-10-06 16:38:16 -04:00
Alex Hart
9d469db7ae Move stories above app security section. 2022-10-06 16:38:16 -04:00
Alex Hart
72347af967 Disassociate direct replies when remote-deleting a story. 2022-10-06 16:38:16 -04:00
Greyson Parrelli
e3dff46136 Rotate AccountRecord.storiesDisabled
iOS had a bug and we need to try again.
2022-10-06 16:38:16 -04:00
Alex Hart
891c99a148 Do not allow users to attempt to send story replies to an inactive group. 2022-10-06 16:38:16 -04:00
Greyson Parrelli
8a452ddf11 Allow remote deletes to be tagged with story=true. 2022-10-06 16:38:16 -04:00
Alex Hart
aef0ed828c Add proper colorization to send button in stories flow. 2022-10-06 16:38:16 -04:00
Alex Hart
9ad55e2360 Fix issue where images were not properly rendered for previews. 2022-10-06 13:45:17 -03:00
Alex Hart
f687840891 Do not display story media in settings media rail. 2022-10-06 13:22:37 -03:00
Greyson Parrelli
bb323dc575 Bump version to 5.52.2 2022-10-06 11:58:56 -04:00
Greyson Parrelli
c0e11fbd23 Updated language translations. 2022-10-06 11:58:23 -04:00
Alex Hart
0d94794ece Fix issue where quote view would display base64 encoded text story. 2022-10-06 11:56:09 -04:00
Greyson Parrelli
14e8f5cf98 Fix sending group stories when you're the only group member. 2022-10-06 11:56:09 -04:00
Alex Hart
b78f06f064 Update colors on create button in private story creation flow. 2022-10-06 11:56:09 -04:00
Alex Hart
0b978dd9d7 Update private story creation screens to match material3 spec. 2022-10-06 11:56:09 -04:00
Alex Hart
da9dcc794f Close search if open on back pressed in stories landing page. 2022-10-06 11:56:09 -04:00
Alex Hart
f3fabcbe6a Fix issue where onboarding text could be cut off. 2022-10-06 11:56:09 -04:00
Alex Hart
95801dbdc7 Remove long-press action from my story items. 2022-10-06 11:56:09 -04:00
Alex Hart
0a33574f1d Do not unarchive threads when story is received. 2022-10-06 11:56:09 -04:00
Alex Hart
35f1baf965 Add group story removal dialog. 2022-10-06 11:56:09 -04:00
Alex Hart
cc5aab6be3 Fix display of story disable dialog. 2022-10-06 10:04:14 -03:00
Alex Hart
486e172aee Fix crash when naturally finishing story set. 2022-10-06 10:02:19 -03:00
Alex Hart
ec46d6039d Fix crash when trying to share a text story. 2022-10-06 09:55:49 -03:00
Greyson Parrelli
1fe4c45c44 Bump version to 5.52.1 2022-10-05 19:49:51 -04:00
Greyson Parrelli
9946da2cec Fix crash when fetching messages. 2022-10-05 19:49:29 -04:00
Greyson Parrelli
f9a4b7cf12 Bump version to 5.52.0 2022-10-05 18:15:10 -04:00
Greyson Parrelli
1fc119e027 Fix lifespan of RefreshAttributesJob. 2022-10-05 18:15:10 -04:00
Greyson Parrelli
293bc2da47 Rotate the stories feature flag. 2022-10-05 18:15:10 -04:00
Jim Gustafson
44d4075636 Update to RingRTC v2.21.2 2022-10-05 18:15:10 -04:00
Greyson Parrelli
23ba5c874a Improve styling of ChooseGroupStoryBottomSheet. 2022-10-05 18:15:10 -04:00
Cody Henthorne
26709177d2 Fix out-of-sync local state after rejoining a group via invite link. 2022-10-05 18:15:10 -04:00
Greyson Parrelli
3895578d51 Always use sealed sender when sending stories. 2022-10-05 18:15:10 -04:00
Nicholas
a9a64a3f60 Update MediaPreviewV2 to use thumbnail rail & menu items. 2022-10-05 18:15:10 -04:00
Alex Hart
2edb9eeb52 Add stories beta dialog. 2022-10-05 18:15:10 -04:00
Alex Hart
4b94509a7a Add dialog protection and remote deletion to disabling stories and deleting lists. 2022-10-05 15:04:54 -03:00
Greyson Parrelli
ad1801108d Fix issues with story thread when processing a sync message. 2022-10-05 11:52:57 -04:00
Alex Hart
ee00e931eb Fix possible RxStore memory leak. 2022-10-05 12:06:47 -03:00
Nicholas
4f3910e3ae Add toolbar to MediaPreviewV2 implementation. 2022-10-04 17:32:57 -04:00
Alex Hart
79b3b9190a Add blocklist for mixed-mode capture. 2022-10-04 17:32:57 -04:00
Greyson Parrelli
afedbf40e3 Prepare the websocket keepalive for API 31. 2022-10-04 17:32:57 -04:00
Alex Hart
437c3ffd66 Add logging to forward fragment closes. 2022-10-04 17:32:57 -04:00
Alex Hart
083219888c Add logging around attachment id update. 2022-10-04 17:32:57 -04:00
Cody Henthorne
c1f3e27101 Fix missed call notification when busy on another device. 2022-10-04 17:32:57 -04:00
Greyson Parrelli
52965da8a5 Stop checking very old capabilities. 2022-10-04 17:32:57 -04:00
Varsha
afe36b982f Prompt to setup payment bioauth, require to disable payment lock. 2022-10-04 17:32:57 -04:00
Nicholas
f63ce79f16 Create new Media Preview infrastructure, behind feature flag. 2022-10-04 17:32:57 -04:00
Alex Hart
1af576c157 Always display the date in story viewer. 2022-10-04 17:32:57 -04:00
Alex Hart
86a345a4f3 Add proper treatment for story pager sending state bar. 2022-10-04 17:32:57 -04:00
Greyson Parrelli
13bd003564 Improve quote model generation. 2022-10-04 17:32:57 -04:00
Greyson Parrelli
b3672273e8 Update BodyRange to use unsigned ints. 2022-10-04 17:32:57 -04:00
Alex Hart
e2a842b440 Fix inability to forward videos to stories. 2022-10-04 17:32:57 -04:00
Greyson Parrelli
1999db97f2 Add support for system names on the ContactRecord. 2022-10-04 17:32:57 -04:00
Greyson Parrelli
6e5f28339d Bump version to 5.51.7 2022-10-04 17:32:15 -04:00
Greyson Parrelli
ce55f6d1c2 Updated language translations. 2022-10-04 17:32:15 -04:00
Alex Hart
b8ec43f466 Add correct icon to message action item. 2022-10-04 17:32:15 -04:00
Nicholas Tinsley
5b7875b763 Set default audio to speaker on incoming ring.
This comes from FullSignalAudioManager.
2022-10-04 17:32:15 -04:00
Alex Hart
dfcc14963d Fix issue with insets on API < 30. 2022-10-04 17:32:15 -04:00
Greyson Parrelli
e1c3583702 Bump version to 5.51.6 2022-10-03 11:33:18 -04:00
Greyson Parrelli
9cea4931d4 Updated language translations. 2022-10-03 11:32:51 -04:00
Nicholas
6c56ef470f Nullability safety for getCommunicationDevice(). 2022-10-03 10:50:55 -04:00
Greyson Parrelli
04822bacdc Use tryOnError in CdsiSocket. 2022-10-03 10:50:12 -04:00
Cody Henthorne
3b1ecc7015 Bump version to 5.51.5 2022-09-29 19:30:15 -04:00
Cody Henthorne
36bd7dae60 Updated language translations. 2022-09-29 19:25:51 -04:00
Cody Henthorne
1b784d6522 Fix incorrect emoji style from being used on some devices. 2022-09-29 19:21:43 -04:00
Nicholas
063f4d2994 Refactor API31 impl to match FullSignalAudioManager. 2022-09-29 19:21:43 -04:00
Cody Henthorne
4325d96a5a Fix crash when checking phone call state. 2022-09-29 11:19:41 -04:00
Greyson Parrelli
88c36e1ff6 Bump version to 5.51.4 2022-09-29 11:09:08 -04:00
Greyson Parrelli
c86b34bb46 Update build tools version to 32.0.0 2022-09-29 10:55:14 -04:00
Cody Henthorne
6708089777 Bump version to 5.51.3 2022-09-29 10:44:14 -04:00
Cody Henthorne
33d108cde3 Updated language translations. 2022-09-29 10:36:53 -04:00
Nicholas
612ce5d0a8 Set UpdateApkReadyListener receivers to not exported. 2022-09-29 10:17:39 -04:00
Alex Hart
0d8ff0ead0 Update window insets logic for gallery and review screens. 2022-09-29 10:17:07 -04:00
Nicholas Tinsley
d413f0041b Revert "Update to targetSdkVersion 32."
This reverts commit 7451ee1403.
2022-09-29 10:15:23 -04:00
Cody Henthorne
678d1c9549 Bump version to 5.51.2 2022-09-28 16:28:40 -04:00
Cody Henthorne
7dc149ddbc Fix non-fcm web socket monitor crash loop. 2022-09-28 16:28:16 -04:00
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
Greyson Parrelli
440d041402 Bump version to 5.45.0 2022-08-02 14:37:06 -04:00
Greyson Parrelli
11211ee205 Updated language translations. 2022-08-02 14:37:06 -04:00
Greyson Parrelli
692006dcd8 Be more defensive when starting the FCM foreground service. 2022-08-02 14:37:06 -04:00
Alex Hart
c4632dc4a3 Add new section to help diagnose story issues. 2022-08-02 14:37:06 -04:00
Greyson Parrelli
a42c3d7ce8 Fix handling of early receipts.
We were storing the early content under the wrong recipient.
2022-08-02 14:37:06 -04:00
Alex Hart
370c2b941c Remove unnecessary logging. 2022-08-02 14:37:06 -04:00
Alex Hart
8be7fa8655 Improve accessibility of SMS code keyboard. 2022-08-02 14:36:30 -04:00
Cody Henthorne
c2b5407911 Change batch identity check timing behavior. 2022-08-02 14:36:30 -04:00
Cody Henthorne
dc04c8ed98 Add urgency flag to message sends. 2022-08-02 14:36:30 -04:00
Alex Hart
c7cd261641 Add polish to stories link previews. 2022-08-02 14:36:30 -04:00
Cody Henthorne
19af68a27c Add inline emoji search. 2022-08-02 14:36:30 -04:00
Alex Hart
ba7319e215 Respect proper media upload requirements for stories. 2022-08-02 14:36:30 -04:00
Greyson Parrelli
92201dcd90 Properly set the isRecipientUpdate flag on story sends. 2022-08-02 14:36:30 -04:00
Alex Hart
855d74bbbf Drop state update for unattached fragment. 2022-08-02 14:36:30 -04:00
Jim Gustafson
201f314cfb Update to RingRTC v2.20.13 2022-08-02 14:36:30 -04:00
Alex Hart
2eef2e1636 Mark internal preferences string as non-translatable. 2022-08-02 14:36:30 -04:00
Alex Hart
f05f9287c1 Update LibMobileCoin to 1.2.2.1
Fixes #12354

Co-authored-by: Bernie Dolan <bernie@mobilecoin.com>
Co-authored-by: Varsha <varsha@mobilecoin.com>
2022-08-02 14:36:30 -04:00
Alex Hart
49cc962bde Fix bug where share intent data would be redisplayed. 2022-08-02 14:36:30 -04:00
Greyson Parrelli
d0420ba51d Add support for the changeSelf param in getAndPossiblyMergePnp. 2022-08-02 14:36:30 -04:00
Greyson Parrelli
0e7cffedc9 Fix compilation issue with androidTests. 2022-08-02 14:36:30 -04:00
Greyson Parrelli
22688789d2 Fix multidex issue with image editor sample app. 2022-08-02 14:36:30 -04:00
Greyson Parrelli
4eb2f16ef1 Keep logs concerning decryption errors longer. 2022-08-02 14:36:30 -04:00
Alex Hart
ef950bdbb5 Stick buttons to bottom of subscription page. 2022-08-02 14:36:30 -04:00
Greyson Parrelli
cb9a219c4b Re-enable the 'read more' text in see replies mode. 2022-08-02 14:36:30 -04:00
Greyson Parrelli
9cd1971329 Fix issue where conversations started on linked devices didn't show a phone number. 2022-08-02 14:36:30 -04:00
Cody Henthorne
a51754e207 Fix premature call termination during safety number change. 2022-08-02 14:36:30 -04:00
Greyson Parrelli
df3399bde5 Remove processing of inbound GV1 messages. 2022-08-02 14:36:29 -04:00
Greyson Parrelli
5140353722 Fix situation where two keyboards could be showing in media editor.
Fixes #11618
2022-08-02 14:36:29 -04:00
Greyson Parrelli
26bb52fd60 Prevent popup menu from covering bottom items in the media overview screen. 2022-08-02 14:36:29 -04:00
Alex Hart
f50bf3e9c2 Remove blocking get from donation jobs. 2022-08-02 14:36:29 -04:00
Alex Hart
8f12b2041a Allow users to remove viewers directly from stories. 2022-08-02 14:36:29 -04:00
Alex Hart
2674fd2df4 Fix issue where postponed transition would not start at the right time. 2022-08-02 14:36:29 -04:00
Evan Hahn
39d07c0081 Change default to disabled for contact joined notifications. 2022-08-02 14:36:29 -04:00
Alex Hart
1eb253562b Re-enable CameraX for Pixel 4 devices. 2022-08-02 14:36:29 -04:00
Alex Hart
bc7908a4a5 Add blockingGet linter. 2022-08-02 14:36:29 -04:00
Alex Hart
a52b64281c Upgrade CameraX to 1.1.0 and fork removal. 2022-08-02 14:36:29 -04:00
Alex Hart
e3e9f90094 Use db as SSOT for unread counter. 2022-07-27 13:26:28 -04:00
Cody Henthorne
a7a5f2e8c6 Add batch identity checks to stories and share/forward flows. 2022-07-27 13:26:28 -04:00
Alex Hart
87cb2d6bf8 Add new story send final screen. 2022-07-27 13:26:28 -04:00
Alex Hart
3c78d8619a Add sending state to story viewer. 2022-07-27 13:26:28 -04:00
Alex Hart
60e9763f7a Fix story caption protection. 2022-07-27 13:26:28 -04:00
Alex Hart
ab897953bf Add padding to bottom of stories landing recycler. 2022-07-27 13:26:28 -04:00
Alex Hart
d2c2952ccf Fix bug when sending to a single contact and single dlist at the same time. 2022-07-27 13:26:28 -04:00
Cody Henthorne
36c882e318 Bump version to 5.44.3 2022-07-27 13:22:21 -04:00
Cody Henthorne
18106c1eab Updated language translations. 2022-07-27 13:15:56 -04:00
Cody Henthorne
9f4d8ac12c Fix contact discovery refresh crash. 2022-07-27 13:12:58 -04:00
Alex Hart
8cb4034c80 Invert media flow button colors. 2022-07-27 13:12:58 -04:00
Alex Hart
ad0acc640b Handle multishare of text. 2022-07-27 13:12:58 -04:00
Alex Hart
c907a01077 Fix null pointer exception when presenting latest media thumbnail. 2022-07-27 13:12:58 -04:00
Alex Hart
053b0eabde Fix bad argument for multiselect full screen dialog. 2022-07-27 12:30:07 -03:00
Cody Henthorne
5b7ac84e7c Bump version to 5.44.2 2022-07-26 09:39:44 -04:00
Cody Henthorne
fee3af42af Updated language translations. 2022-07-26 09:36:44 -04:00
Cody Henthorne
eaa2d58518 Partitialy revert read more fix for See Replies. 2022-07-26 09:33:45 -04:00
Alex Hart
6c42ded2b1 Update recyclerview dependency version to 1.2.1 2022-07-26 09:33:45 -04:00
Alex Hart
cb7b2d90d5 Only display outgoing messages when entering viewer through my stories. 2022-07-26 09:33:45 -04:00
Alex Hart
d40be0abf8 Maintain send button tinting in media preview. 2022-07-26 09:33:45 -04:00
Alex Hart
d6cc4acf5c Set send foreground to white if using a custom color. 2022-07-26 09:33:45 -04:00
Alex Hart
fa2d3e93ae Remove 0 items toast. 2022-07-26 09:33:45 -04:00
Alex Hart
7511a9ae8c Remove low profile mode. 2022-07-26 09:33:45 -04:00
Alex Hart
b20658c829 Allow media selection recipient selection fragment to display in user's chosen app theme. 2022-07-26 09:33:45 -04:00
Alex Hart
09b92a6559 Add logging to share activity. 2022-07-26 09:33:45 -04:00
Alex Hart
b0d75a8a5a Disallow opening archived chats if in multiselect. 2022-07-26 09:33:45 -04:00
Alex Hart
234f4b4b41 Update x asset with tint. 2022-07-26 09:33:45 -04:00
Alex Hart
3f59425579 Add subscribeOn call for getSecurityInfo. 2022-07-26 09:33:44 -04:00
Jim Gustafson
a50597445a Update to RingRTC v2.20.12 2022-07-26 09:33:44 -04:00
Alex Hart
a49e781c8d Respect autodownload settings when opening stories. 2022-07-26 09:33:44 -04:00
Alex Hart
570b143582 Update base stories recipient selection fragment with material 3 spec. 2022-07-26 09:33:44 -04:00
Alex Hart
e6829a1b7a Add add to story handling and icon in my story row. 2022-07-26 09:33:44 -04:00
Alex Hart
14f9a3c155 Ensure sent group stories are included in the My Stories item. 2022-07-26 09:33:44 -04:00
Alex Hart
b32fe003b2 Update group name display in stories landing page. 2022-07-26 09:33:44 -04:00
Alex Hart
c77718f4c7 Make next/continue buttons in send flow more consistent. 2022-07-26 09:33:44 -04:00
Alex Hart
a50e49e4e6 Update tooltip to a more material look. 2022-07-26 09:33:44 -04:00
Alex Hart
ffd60af3ff Add new background for tooltip and always display. 2022-07-26 09:33:44 -04:00
Alex Hart
d62ff6ca06 Add new chevron asset to story reply bar. 2022-07-26 09:33:44 -04:00
Alex Hart
277cfe2d6f Set story reaction bar height to 56dp. 2022-07-26 09:33:44 -04:00
Alex Hart
9b669009df Reduce story direct reply composer corner radius to 18dp. 2022-07-26 09:33:44 -04:00
Alex Hart
9f069bea7b Add proper background color to group replies. 2022-07-26 09:33:44 -04:00
Alex Hart
c0f00eff25 Add reactions overlay to reply bottom sheets. 2022-07-26 09:33:44 -04:00
Alex Hart
b183a38f3c Add proper thread summary for reactions to stories. 2022-07-26 09:33:44 -04:00
Alex Hart
d64aa3bc43 Apply 150ms delay to story chrome fadeout. 2022-07-26 09:33:44 -04:00
Alex Hart
28e10dbb43 Disable user input during state based page jump. 2022-07-22 14:39:47 -03:00
Cody Henthorne
36b1f2816c Bump version to 5.44.1 2022-07-22 13:23:41 -04:00
Cody Henthorne
931693f5fa Updated language translations. 2022-07-22 13:19:01 -04:00
Cody Henthorne
9bade7ed4b Fix telecom system freeze in poor network. 2022-07-22 13:14:42 -04:00
Alex Hart
1d6b62d8ca Stop storing state in ConversationParentFragment. 2022-07-22 13:14:42 -04:00
Alex Hart
b9a225f6c6 Fix blank screen issue when entering through a quote. 2022-07-22 13:14:42 -04:00
Alex Hart
c8612d5502 Fix several conversation fragment issues. 2022-07-22 13:14:42 -04:00
Alex Hart
837f86bdd3 Fix NPE when launching conversation bubble. 2022-07-22 13:14:42 -04:00
Alex Hart
6801b5a1a3 Fix gallery item aspect ratio in avatar picker. 2022-07-22 13:14:42 -04:00
Alex Hart
c9b6287702 Adjust media gallery folder overlay. 2022-07-21 15:42:31 -03:00
Cody Henthorne
6cce9ed00f Bump version to 5.44.0 2022-07-21 14:08:16 -04:00
Cody Henthorne
cc1a65952b Updated language translations. 2022-07-21 14:02:59 -04:00
Alex Hart
0b44935ae2 Utilize database-backed unread message count in thread. 2022-07-21 14:57:51 -03:00
Cody Henthorne
fe6058e0df Improve cold start performance. 2022-07-21 13:18:20 -04:00
Alex Hart
d159a0482a Apply new wallpaper bubble color. 2022-07-21 13:18:20 -04:00
Alex Hart
b046eca0fb Do not allow loading state to prevent crossfader from transitioning. 2022-07-21 13:18:20 -04:00
Alex Hart
c27ca9ad52 Fix nav bar color on replies bottom sheet. 2022-07-21 13:18:20 -04:00
Alex Hart
0f2afa814d Fix bad context use for pin verification toast.
Fixes #11353
2022-07-21 13:18:20 -04:00
Alex Hart
561c1a883f Add proper scaling for badge images. 2022-07-21 13:18:20 -04:00
Evan Hahn
0e8a598985 Remove call for public translations. 2022-07-21 13:18:20 -04:00
Alex Hart
6bd8bc08d8 Add new gift opening animation and confirmation haptic. 2022-07-21 13:18:20 -04:00
Alex Hart
d49c8d5184 Localization tweaks for stories and gift badges. 2022-07-21 13:18:20 -04:00
Alex Hart
bcd2763c34 Rotate gifting flag. 2022-07-21 13:18:20 -04:00
Alex Hart
b696a0f758 Move mms and security checks into ViewModel/Repository. 2022-07-21 13:18:20 -04:00
Alex Hart
c5f4a9c89e Implement feedback for Material3 Gallery refresh. 2022-07-21 13:18:20 -04:00
Alex Hart
8767f775e9 Recreate fragment whenever we handle onNewIntent instead of restarting whole activity. 2022-07-21 13:18:20 -04:00
Rashad Sookram
88b895f5ea Notify when calls start to be routed over cellular data.
Only when the device thinks that it's also connected to a WiFi network.
2022-07-21 13:18:20 -04:00
Cody Henthorne
e024541b8a Add telecom integration allow list and change processing for outgoing audio calls. 2022-07-21 13:18:20 -04:00
Alex Hart
e69d944f11 Add logging for unread thread ids. 2022-07-21 13:18:20 -04:00
Alex Hart
359a39ddaf Material 3 media gallery refresh. 2022-07-21 13:18:20 -04:00
Alex Hart
b78633f9a7 Fix several issues with stories. 2022-07-21 13:18:20 -04:00
Alex Hart
aa75f1f8a7 Fix story touch interception which prevented moving between stories. 2022-07-21 13:18:20 -04:00
Alex Hart
eb18c073c6 Set contentIsReady flag if story attachment failed to download. 2022-07-21 13:18:20 -04:00
Alex Hart
3c09655949 Fix camera rotation for newer API levels. 2022-07-21 13:18:20 -04:00
Alex Hart
17b00734ac Update contact name editor. 2022-07-21 13:18:20 -04:00
Alex Hart
00d5724cec Move androidTest into instrumentation build variant. 2022-07-21 13:18:20 -04:00
Alex Hart
4c5a88c6ca Add logging around wakelock usage for voice notes player. 2022-07-21 13:18:20 -04:00
Alex Hart
2e8ebe8b74 Add info sheet for stories. 2022-07-21 13:18:20 -04:00
Alex Hart
caab91cdc3 Update UI elements of contact share activity. 2022-07-21 13:18:20 -04:00
Cody Henthorne
9c914ab715 Reduce disk hits when accessing shared preferences.
While the same instance of SharedPreferences is returned each time, in
order to get it, the system has to do a file check each time it's with
a new context. We can safely cache the instance instead of paying that
file check each time and pay it only once.
2022-07-21 13:18:20 -04:00
Cody Henthorne
819f7a170f Reduce profile avatar disk reads. 2022-07-21 13:18:20 -04:00
Alex Hart
2f17963b2b Fix hot loop when trying to delete stories but only onboarding exists. 2022-07-21 13:18:20 -04:00
Alex Hart
15111b2792 Omit blocked contacts from recents. 2022-07-21 13:18:20 -04:00
Alex Hart
ecbc2d30ca Float onboarding story to top of the list. 2022-07-21 13:18:20 -04:00
Alex Hart
34379b8d3a Add internal setting item to clear onboarding state. 2022-07-21 13:18:19 -04:00
Alex Hart
b18542a839 Ensure images sent to stories respect media quality settings.
Stories should always use "Standard" quality, not L3 (high quality). This change ensures that we:

1. Always send stories at the appropriate quality
2. Do not corrupt or overwrite pre-existing image attachments
3. Close several streams when done (thanks StrictMode!)
2022-07-21 13:18:19 -04:00
Cody Henthorne
c4bef8099f Add GV2 accept by PNI invite. 2022-07-21 13:18:19 -04:00
Alex Hart
b223ebe95e Prevent remote deletion of gift badges. 2022-07-21 13:18:19 -04:00
Alex Hart
02ea5ac806 Prevent overlay from opening for unopened gifts. 2022-07-21 13:18:19 -04:00
Cody Henthorne
e03b54ac0f Bump version to 5.43.7 2022-07-21 13:18:00 -04:00
Cody Henthorne
9daa57675d Updated language translations. 2022-07-21 13:12:21 -04:00
Cody Henthorne
e113973358 Fix decline code infinite loop. 2022-07-21 12:13:09 -04:00
Cody Henthorne
a845a020d6 Prevent crash on clients with bad data. 2022-07-21 12:10:52 -04:00
Alex Hart
041bde3fd9 Bump version to 5.43.6 2022-07-18 16:06:43 -03:00
Alex Hart
5927ba9843 Updated language translations. 2022-07-18 16:06:00 -03:00
Alex Hart
2e7e165f8a Always relaunch conversation activity. 2022-07-18 16:00:55 -03:00
Alex Hart
4bed90fa37 Bump version to 5.43.5 2022-07-18 14:11:08 -03:00
Alex Hart
408a6f662d Updated language translations. 2022-07-18 14:11:08 -03:00
Alex Hart
c9e1607987 Ensure share intents are not re-used for draft data. 2022-07-18 14:11:08 -03:00
Alex Hart
f9c0156757 Fix crash when outcomeReason is null. 2022-07-18 09:28:57 -03:00
Alex Hart
43f4bc5abe Bump version to 5.43.4 2022-07-15 16:48:41 -03:00
Alex Hart
0ea6ddfe80 Updated language translations. 2022-07-15 16:47:53 -03:00
Alex Hart
e9cff68e0d Add support for kk and ka language codes. 2022-07-15 16:47:52 -03:00
Cody Henthorne
64b78117c1 Use mat3 dialog for save attachments. 2022-07-15 16:47:52 -03:00
Alex Hart
c1ed8bc37b Fix RTL bug in message quote headers. 2022-07-15 16:47:52 -03:00
Cody Henthorne
93d370146e Revert "Fix url trailing symbol."
This reverts commit 86227fbd67.
2022-07-13 20:30:19 -04:00
Alex Hart
96539d70df Bump version to 5.43.3 2022-07-13 15:59:28 -03:00
Alex Hart
07570bbfec Updated language translations. 2022-07-13 15:59:28 -03:00
Alex Hart
71a54ae278 Add proper copy for safety number bottom sheet when completed check. 2022-07-13 15:59:28 -03:00
Alex Hart
2d29298ec4 Fix row selection in new bottom sheet. 2022-07-13 15:59:28 -03:00
Alex Hart
42d2799264 Bump version to 5.43.2 2022-07-12 16:24:19 -03:00
Alex Hart
c1f3e6351c Updated language translations. 2022-07-12 16:23:20 -03:00
Cody Henthorne
40386c910c Fix bug with SMS and disappearing messages. 2022-07-12 16:17:42 -03:00
Cody Henthorne
c95fd7cf0c Fix stale send type when reloading a conversation. 2022-07-12 16:17:41 -03:00
Alex Hart
453affbe28 Remove unnecessary character. 2022-07-12 09:31:44 -03:00
Alex Hart
02b8b4a295 Bump version to 5.43.1 2022-07-11 15:27:09 -03:00
Alex Hart
870d024cbf Updated language translations. 2022-07-11 15:25:17 -03:00
Alex Hart
05bcfcc43f Fix untrusted records check. 2022-07-11 15:20:26 -03:00
Alex Hart
efb82369b6 Bump version to 5.43.0 2022-07-11 14:04:57 -03:00
Alex Hart
088ce0077b Updated language translations. 2022-07-11 13:56:04 -03:00
Alex Hart
c169dd308d Add support for AF. 2022-07-11 13:56:04 -03:00
Alex Hart
631958e1a6 Update callee text color. 2022-07-11 13:35:53 -03:00
Alex Hart
7a0f4fafe2 Implement new Safety Number Changes bottom sheeet. 2022-07-11 13:35:53 -03:00
Cody Henthorne
b0dc7fe6df Add batch identity key check call for improved safety number change performance. 2022-07-11 13:35:53 -03:00
Alex Hart
524adcb6a4 Configure pooled players for video playback by default. 2022-07-11 13:35:53 -03:00
Cody Henthorne
748dbc2ba5 Fix incorrect notification sound when channel is set to silent.
Fixes #12317
2022-07-11 13:35:53 -03:00
Cody Henthorne
e80df64698 Fix block box hiding conversation search navigation.
Fixes #12329
2022-07-11 13:35:53 -03:00
Cody Henthorne
65965e8ac5 Add Moto G20 to camerax blacklist. 2022-07-11 13:35:53 -03:00
Cody Henthorne
60e366e98a Fix delete group from message request state bug.
Fixes #12193
2022-07-11 13:35:53 -03:00
Cody Henthorne
1a80cb7c42 Fix not unarchiving on sent message sync bug. 2022-07-11 13:35:53 -03:00
Alex Hart
a20c2ec63f Fix stories check to account for registration. 2022-07-11 13:35:53 -03:00
Sgn-32
4656cf4bef Shorten disappearing countdown description in message details.
Fixes #10217
Closes #11265
2022-07-11 13:35:53 -03:00
Cody Henthorne
d01df9f053 Fix message details expires in countdown. 2022-07-11 13:35:53 -03:00
Greyson Parrelli
5af9872806 Add a simple PNP-backed implementation of getAndPossiblyMerge. 2022-07-11 13:35:53 -03:00
Greyson Parrelli
3beb730edb Prefer ServiceIds over SignalServiceAddresses. 2022-07-11 13:35:53 -03:00
Alex Hart
6d4dadea48 Fix devices activity crash on KitKat.
Fixes #12338
2022-07-11 13:35:53 -03:00
Greyson Parrelli
f08521ab55 Removed unused test scaffolding. 2022-07-11 13:35:53 -03:00
Greyson Parrelli
04cf8676cc Remove concept of 'highTrust' that is no longer necessary. 2022-07-11 13:35:53 -03:00
Alex Hart
d17896ea09 Reuse video preupload for unclipped media. 2022-07-11 13:35:53 -03:00
Cody Henthorne
7dfebdca32 Fix keyboard auto-close bug. 2022-07-11 13:35:53 -03:00
Alex Hart
5b781c45f3 Abort story send if any of the messages do not have an attachment. 2022-07-11 13:35:53 -03:00
Alex Hart
c906abdb37 Prevent crash when subscriber is invoked after view is destroyed. 2022-07-11 13:35:53 -03:00
Cody Henthorne
78d4d9a3dd Add first time My Story privacy configuration. 2022-07-11 13:35:53 -03:00
Greyson Parrelli
3eac397263 Basic implementation of writing a PnpChangeSet to disk. 2022-07-11 13:35:53 -03:00
Alex Hart
32312da384 Implement several caching improvements for the Story Viewer. 2022-07-11 13:35:53 -03:00
Alex Hart
8f85b58612 Utilize debouncer instead of animator timeout for video capture end time. 2022-07-11 13:35:53 -03:00
Alex Hart
6aa4706e9b Fix bad behaviour for long group replies. 2022-07-05 15:46:06 -04:00
Alex Hart
adbdb97a28 Fix crash when trying to reply when there is no post to reply to. 2022-07-05 15:46:06 -04:00
Sgn-32
a51dfa1470 Use MaterialAlertDialogBuilder in RegistrationLockV2Dialog.
Closes #12326
2022-07-05 15:46:06 -04:00
Alex Hart
36ccf9ca54 Implement Story onboarding download job and message insertion. 2022-07-05 15:46:06 -04:00
Alex Hart
2270dfaf21 Update story notifications to match spec. 2022-07-05 15:46:06 -04:00
Alex Hart
bd5907ea04 Do not notify for reactions if not the group story sender. 2022-07-05 15:46:06 -04:00
Alex Hart
6d24c342d2 Fix link preview issue with text stories. 2022-07-05 15:46:06 -04:00
Alex Hart
4a3fe771d1 Display views off in my stories fragment when receipts are disabled. 2022-07-05 15:46:06 -04:00
Alex Hart
ed063b4b95 Prevent pre-upload for videos that require clipping if stories is enabled. 2022-07-05 15:46:06 -04:00
Alex Hart
370640eaef Force voice note player to always be LTR. 2022-07-05 15:46:06 -04:00
Alex Hart
2a5d385152 Fix audio view in overview in RTL languages. 2022-07-05 15:46:06 -04:00
Greyson Parrelli
be2ed8989f Fix possible crash in ProfileKeySendJob if given an invalid threadId. 2022-07-05 15:46:06 -04:00
Jim Gustafson
e413ee4ed9 Update to RingRTC v2.20.11 2022-07-05 15:46:06 -04:00
Alex Hart
3913166461 Add minheight to media count indicator. 2022-07-05 15:46:06 -04:00
Greyson Parrelli
314ef3452f Improving logging of 401 errors. 2022-07-05 15:46:06 -04:00
Alex Hart
e412cac419 Implement Stories read receipt off state. 2022-07-05 15:46:06 -04:00
Sgn-32
f3873c8a7c Prevent various operations on blocked users from conversation.
Fix #10973
Closes #11979
2022-07-05 15:46:05 -04:00
Sgn-32
f8d459829e Fix one more place where Note to Self should be used.
Closes #12321
2022-07-05 15:46:05 -04:00
Greyson Parrelli
9d8e9a3a14 Bump version to 5.42.7 2022-07-05 14:06:05 -04:00
Greyson Parrelli
abb4f33299 Updated language translations. 2022-07-05 13:01:39 -04:00
Greyson Parrelli
a1d444fc19 Improve resiliance of FCM fetch. 2022-07-05 11:32:42 -04:00
Greyson Parrelli
a3802d0af0 Avoid potential false positive in DeadlockDetector. 2022-07-05 10:38:21 -04:00
Greyson Parrelli
f441b3d0f1 Use more performant method to check if message is quoted. 2022-07-04 12:46:18 -04:00
Greyson Parrelli
99f1c9fd65 Do not show the quoted indicator in multiselect mode. 2022-07-04 12:35:48 -04:00
Greyson Parrelli
041a019439 Bump version to 5.42.6 2022-07-02 15:54:23 -04:00
Greyson Parrelli
7a34c6ee80 Updated language translations. 2022-07-02 15:54:00 -04:00
Greyson Parrelli
50701dd292 Add support for GIF playback in 'see replies' bottom sheet. 2022-07-02 15:48:36 -04:00
Greyson Parrelli
3336d92cb1 Hide 'Add to Contacts' option for the Note to Self chat. 2022-07-02 15:11:29 -04:00
Greyson Parrelli
66886dfd7b Make the 'see replies' bottom sheet respond to new/deleted messages. 2022-07-02 14:55:31 -04:00
Cody Henthorne
358d9ca58c Bump version to 5.42.5 2022-07-01 15:58:52 -04:00
Cody Henthorne
85ce85de07 Updated language translations. 2022-07-01 15:54:42 -04:00
Cody Henthorne
cce0a5e820 Fix clickable state bug with CircularProgressMaterialButton. 2022-07-01 15:20:36 -04:00
Cody Henthorne
0318c4f080 Fix line wrap on Request to Join bottom sheet dialog. 2022-07-01 14:54:24 -04:00
Greyson Parrelli
39288dbcbf Only condense images in the original message in 'see replies' bottom sheet. 2022-07-01 13:39:08 -04:00
Greyson Parrelli
daab296172 Show the full reply chain in the 'see replies' bottom sheet. 2022-07-01 13:36:54 -04:00
Cody Henthorne
a44c3c5c2f Fix disable state bug with CircularProgressMaterialButton. 2022-07-01 12:56:08 -04:00
Greyson Parrelli
089a3d386f Fix inconsistent message bubble padding in RTL. 2022-07-01 12:01:25 -04:00
Greyson Parrelli
f523529338 Fix 'see replies' indicator animation in RTL. 2022-07-01 11:30:25 -04:00
Greyson Parrelli
b5a99a6b3f Bump version to 5.42.4 2022-06-30 18:58:27 -04:00
Greyson Parrelli
159d0109b9 Fix crash when long-pressing a non-media message. 2022-06-30 18:58:20 -04:00
Cody Henthorne
4eddeb74c5 Bump version to 5.42.3 2022-06-30 17:10:25 -04:00
Cody Henthorne
28de1f5c3d Updated language translations. 2022-06-30 17:02:45 -04:00
Greyson Parrelli
85f38bdea8 Fix corners of images in quote bottom sheet. 2022-06-30 16:58:40 -04:00
Cody Henthorne
12a7f36bec Update copy and icon for release channel boost button. 2022-06-30 16:58:40 -04:00
Cody Henthorne
7b805e4041 Remove use of PNI Credential. 2022-06-30 15:51:59 -04:00
Greyson Parrelli
fc55b5d1ea Hide 'see replies' button during long press. 2022-06-30 13:03:29 -04:00
Cody Henthorne
1b58164bf3 Bump version to 5.42.2 2022-06-30 12:09:18 -04:00
Cody Henthorne
42c32adf8c Updated language translations. 2022-06-30 12:04:58 -04:00
Greyson Parrelli
53663b5ebd Don't open images directly from the quote bottom sheet. 2022-06-30 12:01:47 -04:00
Greyson Parrelli
a87fe78c33 Show reactions in quote bottom sheet. 2022-06-30 12:01:47 -04:00
Cody Henthorne
3e3ccd4b96 Fix distribution list sync crash. 2022-06-30 12:01:47 -04:00
Cody Henthorne
0e9344c8e3 Bump version to 5.42.1 2022-06-29 19:34:53 -04:00
Cody Henthorne
d562ba090e Updated language translations. 2022-06-29 19:32:21 -04:00
Jim Gustafson
9c665d3a71 Update to RingRTC v2.20.10.1 2022-06-29 19:28:21 -04:00
Cody Henthorne
f2e919f39f Bump version to 5.42.0 2022-06-29 15:43:53 -04:00
Cody Henthorne
19080a8a5e Updated language translations. 2022-06-29 15:36:18 -04:00
Greyson Parrelli
61ce39b5b6 Improve implementation and testing on PNP contact merging. 2022-06-29 15:32:26 -04:00
Alex Hart
c64be82710 Add context menus to story contacts in contact selection. 2022-06-29 15:32:25 -04:00
Alex Hart
7bd34d2b99 Reimplement contact chips with a recyclerview. 2022-06-29 15:32:25 -04:00
Cody Henthorne
4215b0391d Fix leak in Message Details for disappearing messages. 2022-06-29 15:32:25 -04:00
Cody Henthorne
96ea4c0cc2 Fix gift plurals resource. 2022-06-29 15:32:25 -04:00
Cody Henthorne
1129ca28fb Revert "Disable voice note proximity sensor when using bluetooth headset. (#2448)"
This reverts commit 9c7a5e3cc8.
2022-06-29 15:32:25 -04:00
Alex Hart
ba6e1b5dd5 Fix attachment deduplication issue with Stories. 2022-06-29 15:32:25 -04:00
Cody Henthorne
ed25be2e23 Fix couple more places where Note to Self should be used. 2022-06-29 15:32:25 -04:00
Cody Henthorne
7a0bd3315b Update release channel with material 3 changes. 2022-06-29 15:32:25 -04:00
Alex Hart
8b806a8ac5 Isolate and add unit testing to new link logic.
Co-Authored-By: ylpoonlg <56300571+ylpoonlg@users.noreply.github.com>
2022-06-29 15:32:25 -04:00
Alex Hart
0ac5782f1f Ensure stub is never resolved if not needed. 2022-06-29 15:32:25 -04:00
Alex Hart
e10c20ffd7 Fix issue with getUnreadStories query. 2022-06-29 15:32:25 -04:00
ylpoonlg
86227fbd67 Fix url trailing symbol.
Fixes #12309
Fixes #10898
Fixes #11310
2022-06-29 15:32:25 -04:00
Alex Hart
1cfa5c31f2 Implement correct video story sound behaviour. 2022-06-29 15:32:25 -04:00
Alex Hart
521bd2cce4 Implement first-time-nav screen for stories. 2022-06-29 15:32:25 -04:00
Alex Hart
858c7a7f2e Implement "unviewed only" mode for story viewer. 2022-06-29 15:32:25 -04:00
Cody Henthorne
89a6730efe Add Storage Service plugin to Spinner. 2022-06-29 15:32:25 -04:00
Cody Henthorne
9bc25132c3 Add new My Story privacy settings. 2022-06-29 15:32:25 -04:00
Alex Hart
ebc556801e Ensure story media is only uploaded once. 2022-06-29 15:32:25 -04:00
Alex Hart
6b745ba58a Allow swipe up to close viewer when viewing last story. 2022-06-29 15:32:25 -04:00
Alex Hart
6ddb5b983f Implement proper error handling for charge failure on initial subscription attempt. 2022-06-29 15:32:25 -04:00
Alex Hart
8efd07b3e2 Fix diplay issue with note to self banner. 2022-06-29 15:32:25 -04:00
Alex Hart
e85adad2b4 Add safety net for when the user has disabled their contacts app. 2022-06-29 15:32:25 -04:00
Alex Hart
678a6f86ab Change several creations of alertdialogs to use materialalertdialogbuilder. 2022-06-29 15:32:25 -04:00
Jim Gustafson
9dc061e64f Update to RingRTC v2.20.10 2022-06-29 15:32:25 -04:00
Frazer Smith
2fed3f7e90 Update github actions with latest versions.
Closes #12294
2022-06-29 15:32:25 -04:00
Alex Hart
af362736de Update help categories. 2022-06-29 15:32:25 -04:00
Cody Henthorne
d39a4b14e7 Only add one sustainer request message per release notes update. 2022-06-29 15:32:25 -04:00
Alex Hart
6a385c7a22 Implement video length enforcement for Stories. 2022-06-28 15:42:15 -04:00
Alex Hart
2c3d8337c3 Include self in recents section. 2022-06-28 15:42:15 -04:00
Alex Hart
28feba6a6c Add proper catch for ISE in video thumb extractor. 2022-06-28 15:42:15 -04:00
Greyson Parrelli
6ec7834046 Add the ability to see replies. 2022-06-28 15:42:15 -04:00
Alex Hart
ee4f3abf22 Add unit testing for pinned last message deletion fix. 2022-06-28 15:42:14 -04:00
Alex Hart
dc66583ef1 Update camera UX to match Material3 Spec. 2022-06-28 15:42:14 -04:00
Alex Hart
d30714bfd4 Update coloring of capture first flow toggle.: 2022-06-28 15:42:14 -04:00
Alex Hart
d04d2f7e93 Fix bad centering of emoji button in add message fragment. 2022-06-28 15:42:14 -04:00
Alex Hart
1328aab939 Add material3 coloring to story reply dialog. 2022-06-28 15:42:14 -04:00
Alex Hart
2a9d2cf580 Remove bottomsheet elevation tinting. 2022-06-28 15:42:14 -04:00
Jim Gustafson
a316650aee Update to RingRTC v2.20.9 2022-06-28 15:42:14 -04:00
Alex Hart
4d1e8b8f75 Update several story ui elements for Material3. 2022-06-28 15:42:14 -04:00
Alex Hart
9c7a5e3cc8 Disable voice note proximity sensor when using bluetooth headset. (#2448) 2022-06-28 15:42:14 -04:00
Alex Hart
2022dae37a Draw pulse outliner in onDrawForeground instead of in onDraw. 2022-06-28 15:42:14 -04:00
Chris Eager
05b7055678 Update device-transfer app build to work with the latest libsignal 2022-06-28 15:42:14 -04:00
Alex Hart
53c60e1f6d Add proper coloring to send buttons. 2022-06-28 15:42:14 -04:00
Alex Hart
cd8fa58d7e Fix voice note playback bar for RTL regions. 2022-06-28 15:42:14 -04:00
Cody Henthorne
c2ffc8332d Bump version to 5.41.11 2022-06-28 15:41:43 -04:00
Cody Henthorne
343a49fa26 Updated language translations. 2022-06-28 15:41:20 -04:00
Cody Henthorne
2c700c7e0e Fix broken Material3 changes on Android 6. 2022-06-28 15:20:19 -04:00
Alex Hart
105d0c778c Bump version to 5.41.10 2022-06-23 17:10:08 -03:00
Alex Hart
d8bf2392ae Updated language translations. 2022-06-23 17:09:32 -03:00
Cody Henthorne
4585b439d5 Remove notification creation in WebRtcCallSerivce onCreate. 2022-06-23 15:55:57 -04:00
Alex Hart
587aa49db8 Bump version to 5.41.9 2022-06-22 14:51:18 -03:00
Alex Hart
1ef576f6f8 Updated language translations. 2022-06-22 14:50:37 -03:00
Greyson Parrelli
d070ebcd2f Fix splash screen when app theme mismatches system theme. 2022-06-22 09:02:15 -04:00
Alex Hart
2c779e700d Adjust message request padding for better localization support. 2022-06-22 09:52:01 -03:00
Alex Hart
feadde8737 Bump version to 5.41.8 2022-06-21 19:16:38 -03:00
Alex Hart
64dca6f60b Updated language translations. 2022-06-21 19:16:17 -03:00
Greyson Parrelli
4c4cfe917d Always ensure the send type matches the send button. 2022-06-21 19:11:27 -03:00
Alex Hart
852989ce48 Manually set nav bar background to 50 transparent black with wallpaper. 2022-06-21 19:11:27 -03:00
Alex Hart
3cecd503ab Bump version to 5.41.7 2022-06-21 15:45:16 -03:00
Alex Hart
d8b97d8f87 Updated language translations. 2022-06-21 15:44:50 -03:00
Alex Hart
611950a589 Utilize translucent navigation bar for now. 2022-06-21 14:40:35 -03:00
Alex Hart
73be74dac1 Bump version to 5.41.6 2022-06-20 16:37:30 -03:00
Alex Hart
94fc7ad3c0 Updated language translations. 2022-06-20 16:36:37 -03:00
Greyson Parrelli
290fbbb9ee Update backoff logic of ClearFallbackKbsEnclaveJob. 2022-06-20 15:30:50 -04:00
Alex Hart
c0735c8119 Clear snippet when the last message in a pinned thread is deleted. 2022-06-20 16:15:24 -03:00
Greyson Parrelli
8f5fc83529 Remove inactive KBS fallback. 2022-06-20 12:20:23 -04:00
Alex Hart
ac2cbba067 Fix pin reminder dialog submit button. 2022-06-20 10:15:27 -03:00
Alex Hart
1bcfbaf16e Fix bottom nav overlay issue with react-with-any sheet. 2022-06-20 10:10:51 -03:00
Greyson Parrelli
c950c2bdd2 Fix reaction overlay issue in dark theme. 2022-06-19 11:18:53 -04:00
Greyson Parrelli
38cecf68b5 Bump version to 5.41.5 2022-06-17 19:27:34 -04:00
Greyson Parrelli
f932ed6c6a Updated language translations. 2022-06-17 19:21:40 -04:00
Greyson Parrelli
0209db4531 Show/hide attachment keyboard with the reaction overlay. 2022-06-17 19:13:14 -04:00
Greyson Parrelli
2e9f43cf94 Fix gap in reaction overlay.
I use the term 'fix' lightly. Used a stupid hack that we should revisit.
2022-06-17 18:57:52 -04:00
Alex Hart
897e176f0d Revert shade removal and add nav bar coloring. 2022-06-17 14:22:14 -03:00
Greyson Parrelli
fcb4c627e4 Bump version to 5.41.4 2022-06-17 11:06:19 -04:00
Greyson Parrelli
c85076138a Updated language translations. 2022-06-17 11:05:44 -04:00
Greyson Parrelli
8877603e13 Fix another possible crash with available message types. 2022-06-17 11:05:44 -04:00
Alex Hart
ae6ca49e4e Fix toolbar overlap in all media screen. 2022-06-17 11:05:44 -04:00
Greyson Parrelli
2620a8fc51 Address corner case where contact details may not be synced.
Relates to #12293
2022-06-17 11:05:44 -04:00
Alex Hart
008f153b66 Adjust wallpaper preview. 2022-06-17 11:05:44 -04:00
Alex Hart
539a0182e0 Fix navigation bar color issues. 2022-06-17 11:05:44 -04:00
Alex Hart
ff64f7368b Update background color for attachment keyboard. 2022-06-17 11:05:44 -04:00
Greyson Parrelli
211361684d Bump version to 5.41.3 2022-06-16 13:22:39 -04:00
Greyson Parrelli
268b00bbf9 Updated language translations. 2022-06-16 13:22:39 -04:00
Alex Hart
a593bc0b7a Implement composer tweaks to allow for better contrast. 2022-06-16 13:22:39 -04:00
Greyson Parrelli
b6d1af3760 Add possible fix for weird send button state. 2022-06-16 12:02:36 -04:00
Alex Hart
3acbcf54db Fix wrong color flashing when scrolling conversation settings. 2022-06-16 12:02:36 -04:00
Greyson Parrelli
673a8f540b Fix some lifecycle-related crashes. 2022-06-16 12:02:36 -04:00
Alex Hart
69e2a138d9 Fix in-call audio output picker dialog. 2022-06-16 12:02:36 -04:00
Sgn-32
11c6e748f7 Use MaterialAlertDialogBuilder in AddToGroupsActivity.
Closes #12291
2022-06-16 10:59:42 -04:00
Greyson Parrelli
33187ea12f Update the color preview to tint the send button. 2022-06-16 10:47:19 -04:00
Greyson Parrelli
6c5ceab4e5 Bump version to 5.41.2 2022-06-15 11:57:02 -04:00
Greyson Parrelli
f2dc454727 Updated language translations. 2022-06-15 11:57:02 -04:00
Greyson Parrelli
8cb0898f1f Capitalize log field. 2022-06-15 11:57:02 -04:00
Greyson Parrelli
2a2809c17c Update send button color after chat color change. 2022-06-15 11:57:02 -04:00
Greyson Parrelli
9eeecaa73d Initialize WAL mode earlier. 2022-06-15 11:57:02 -04:00
Alex Hart
c83a888ed0 Fix banner input overlap in some situations. 2022-06-15 11:57:02 -04:00
Alex Hart
6854632fec Allow separate specification of status and toolbar active/inactive coloring. 2022-06-15 09:45:37 -03:00
Greyson Parrelli
e6cc49368e Update some dialogs to MaterialAlertDialog. 2022-06-15 08:32:20 -04:00
Greyson Parrelli
18bf00eb7a Bump version to 5.41.1 2022-06-14 17:47:08 -04:00
Greyson Parrelli
fcef6f965d Fix crash that can occur when using non-standard font sizes. 2022-06-14 17:32:26 -04:00
Greyson Parrelli
fb9a9b7c96 Bump version to 5.41.0 2022-06-14 15:20:43 -04:00
Greyson Parrelli
d662bddeb1 Updated language translations. 2022-06-14 15:20:43 -04:00
Greyson Parrelli
c5afeb6d71 Update contact photo syncing for linked devices. 2022-06-14 15:20:43 -04:00
Greyson Parrelli
c66a2b8c61 Add autoVerify to some intent filters. 2022-06-14 15:20:43 -04:00
Alex Hart
88a66b49ff Apply new story list ordering rules.
Co-authored-by: Cody Henthorne <cody@signal.org>
2022-06-14 15:20:43 -04:00
Alex Hart
3b07f4a8ca Do not wait on content to launch story viewer. 2022-06-14 15:20:43 -04:00
Alex Hart
f6fd1e1c91 Fix strange scale behaviour on long press of conversation item. 2022-06-14 15:20:42 -04:00
Alex Hart
2412f6f63a Fix outgoing quote over media. 2022-06-14 15:20:42 -04:00
Greyson Parrelli
ce1983a3b1 Updated libphonenumber to 8.12.50 2022-06-14 15:20:42 -04:00
Greyson Parrelli
523f9c7409 Be more resistent to android disallowing service starts. 2022-06-14 15:20:42 -04:00
Cody Henthorne
d5d7c73ebf Remove bad quantity strings. 2022-06-14 15:20:42 -04:00
Cody Henthorne
ce93537fee Update incoming call handling.
* Fix crash with incoming ringer when custom ringtone isn't found.
* Stop notification profiles from terminating calls on linked devices.
2022-06-14 15:20:42 -04:00
Cody Henthorne
5df20d755a Fix FCM not initialized crash. 2022-06-14 15:20:42 -04:00
Alex Hart
2eb933c2d4 Implement animated color lerp for material toolbars. 2022-06-14 15:20:42 -04:00
Alex Hart
ef3c776b4b Fix reaction pill background color. 2022-06-14 15:20:42 -04:00
Alex Hart
bf156ad7d2 Apply Material3 spec to dialogs. 2022-06-14 15:20:42 -04:00
Alex Hart
56a2b27745 Refactor reactions dialog to match Material3 spec. 2022-06-14 15:20:42 -04:00
Rashad Sookram
0e7ace0da4 Remove unused libsignal files from APK. 2022-06-09 12:00:24 -04:00
Alex Hart
6743861630 Account for archival and meaningful message status in unread count query. 2022-06-09 12:40:35 -03:00
Alex Hart
92c6a84075 Ensure shared background for all generated text stories in a set. 2022-06-09 09:20:04 -03:00
Alex Hart
b8a7748dc1 Update verify safety number display fragment. 2022-06-08 17:00:51 -03:00
Alex Hart
8b5c630303 Adjust padding below indicator. 2022-06-08 16:42:08 -03:00
Alex Hart
9ac5db2f0c Remove more solid icons. 2022-06-08 16:37:56 -03:00
Alex Hart
a7380b33c7 Add proper padding and outline to invite sheet. 2022-06-08 16:30:55 -03:00
Alex Hart
4779096ac5 Update reaction pill colors. 2022-06-08 16:26:05 -03:00
Alex Hart
43be54ec42 Fix padding on expired messages save button. 2022-06-08 15:41:08 -03:00
Alex Hart
7010985be8 Update colors for scroll-to buttons to match material3 spec. 2022-06-06 13:20:19 -03:00
Alex Hart
5080dd4c4b Update emoji keyboard to be aligned with Material3 spec. 2022-06-06 13:08:17 -03:00
Alex Hart
1b1acf0aa5 Modify colorOnSurfaceVariant for dark themes. 2022-06-06 12:23:19 -03:00
Alex Hart
5527269283 Update quoteview background colors. 2022-06-06 12:21:39 -03:00
Alex Hart
af32e156c2 Update message details fragment with material3 spec. 2022-06-06 12:12:13 -03:00
Felix Nüsse
9c7c94b2d4 Allow camera to rotate even when screen is locked
Fixes #8611
Closes #12247

Signed-off-by: Felix Nüsse <felix.nuesse@t-online.de>
2022-06-06 08:51:03 -04:00
Alex Hart
796e5f6f86 Add proper tinting to typing indicator. 2022-06-06 09:45:31 -03:00
Sgn-32
b282b775d0 Add LogSectionSMS to debug log.
Closes #12273
2022-06-05 12:25:26 -04:00
Greyson Parrelli
4da422fd3c Refactor how message send types are selected. 2022-06-03 18:07:29 -04:00
Jim Gustafson
bf90909496 Update to RingRTC v2.20.8 2022-06-03 08:22:23 -07:00
Alex Hart
7aa99ce9a7 Remove a few more unnecessary styles. 2022-06-02 11:52:10 -04:00
Alex Hart
b6767b02ed Remove several old and unnecessary styles. 2022-06-02 11:52:10 -04:00
Alex Hart
cf5f7ef634 Update styles on several group bottom sheets. 2022-06-02 11:52:10 -04:00
Alex Hart
3ca4ff9a94 Update tonal buttons to utilize primaryContainer. 2022-06-02 11:52:10 -04:00
Alex Hart
28edd18e55 Fix quote text sizing. 2022-06-02 11:52:10 -04:00
Alex Hart
7bf2ae3d5e Fix-up iconography in recipient bottom sheet. 2022-06-02 11:52:10 -04:00
Alex Hart
7896a525f2 Fix devicelistfragment and remove two dependencies. 2022-06-02 11:52:10 -04:00
Alex Hart
f2d5bfe51d Fix overlap of multiselect in toolbar. 2022-06-02 11:52:10 -04:00
Alex Hart
b2b6f98294 Fix launch responsiveness of story viewer. 2022-06-02 11:52:10 -04:00
Alex Hart
4758369f79 Fix background of sticker management row item. 2022-06-02 11:52:10 -04:00
Alex Hart
e10e629d13 Add proper text size to LabelMedium. 2022-06-02 11:52:10 -04:00
Alex Hart
0e9e39a4eb Bump ConversationListItem avatar down by 4dp. 2022-06-02 11:52:10 -04:00
Alex Hart
93e5052d6b Fix bad donor badge input behaviour. 2022-06-02 11:52:10 -04:00
Alex Hart
1b471e163d Implement new Material3 spec. 2022-06-02 11:52:10 -04:00
Greyson Parrelli
556e480b06 Bump version to 5.40.4 2022-06-02 11:50:22 -04:00
Greyson Parrelli
d08bee3413 Updated language translations. 2022-06-02 11:49:57 -04:00
Cody Henthorne
e83cb6fa8b Fix QR scanning bug when using camerax. 2022-06-02 11:42:25 -04:00
Greyson Parrelli
499cdd9f29 Make Github action build a specific variant. 2022-06-02 08:50:21 -04:00
Greyson Parrelli
13aa150206 Bump version to 5.40.3 2022-06-02 00:32:17 -04:00
Greyson Parrelli
63d6bab7d6 Updated language translations. 2022-06-02 00:31:39 -04:00
Cody Henthorne
d6108fbbf3 Add force legacy QR scanning switch. 2022-06-01 16:38:15 -04:00
Cody Henthorne
4c44f1ee02 Tweak new QR processing some more. 2022-06-01 14:39:42 -04:00
Greyson Parrelli
f4c728f57c Bump version to 5.40.2 2022-05-31 10:16:30 -04:00
Greyson Parrelli
58cebf7346 Updated language translations. 2022-05-31 10:15:40 -04:00
Cody Henthorne
2446792c62 Tweak QR code capture configuration. 2022-05-30 15:37:01 -04:00
Cody Henthorne
259a86b605 Fix lost scroll position in conversation list bug. 2022-05-30 14:33:04 -04:00
Jim Gustafson
fafe795f39 Update to RingRTC v2.20.7 2022-05-27 13:50:00 -07:00
Alex Hart
9a9636b58f Bump version to 5.40.1 2022-05-27 16:57:05 -03:00
Alex Hart
d48a686d98 Updated language translations. 2022-05-27 16:56:13 -03:00
Cody Henthorne
d69d1c8967 Fix IAE crash in link device transition. 2022-05-27 09:51:20 -04:00
Alex Hart
60b6a9ff3f Prevent crash when ConversationListFragment list is nullified. 2022-05-27 09:55:12 -03:00
Alex Hart
f85803c1fe Bump version to 5.40.0 2022-05-26 14:24:57 -03:00
Alex Hart
9dea815fce Updated language translations. 2022-05-26 14:24:57 -03:00
Cody Henthorne
4e01336b2f Fix unclosed streams during backup export. 2022-05-26 14:24:57 -03:00
Cody Henthorne
9fbc7c0f65 Fix stories viewed not updating in UI. 2022-05-26 14:24:57 -03:00
Cody Henthorne
4d028d1867 Improve messaging around story send failures. 2022-05-26 14:24:57 -03:00
Cody Henthorne
95a46f1ce5 Show user a toast when an unexpected send text story fails. 2022-05-26 14:24:57 -03:00
Cody Henthorne
26a84c5546 Show calling service notification immediately. 2022-05-26 14:24:57 -03:00
Cody Henthorne
652d0d46ed Add foreground service type to WebRtcCallService. 2022-05-26 14:24:57 -03:00
Cody Henthorne
e0f3e34899 Attempt to get service name in start foreground exception stack trace. 2022-05-26 14:24:57 -03:00
Cody Henthorne
4a8083f7b1 Fix Vivo NPE quirk. 2022-05-26 14:24:57 -03:00
Cody Henthorne
08556b111b Fix ISE crash. 2022-05-26 14:24:57 -03:00
Cody Henthorne
7e7bc13b62 Swallow too many pending intents exception. 2022-05-26 14:24:56 -03:00
Cody Henthorne
5115eb125d Fix conversation search not showing after entering via settings. 2022-05-26 14:24:56 -03:00
Ahmad Alturki
3ec55b24f8 Fix swapped group title & avatar on rtl layout.
Fixes #12612
2022-05-26 14:24:56 -03:00
Cody Henthorne
5dba1067d6 Fix conversation list memory leak. 2022-05-26 14:24:56 -03:00
Cody Henthorne
2a91c67c51 Add sending and error states for story group replies. 2022-05-26 14:24:56 -03:00
Alex Hart
a29bc1da8c Add proper hyphenation break to badge name. 2022-05-26 14:24:56 -03:00
Alex Hart
32b4d11a82 Fix crash in onPlaying if fragment is detached. 2022-05-26 14:24:56 -03:00
Jim Gustafson
f013f7357f Update to RingRTC v2.20.6 2022-05-26 14:24:56 -03:00
Alex Hart
e37150e98a Update capabilities logging. 2022-05-26 14:24:56 -03:00
Alex Hart
eaa7262b2f Add debug log entry for video player pool usage. 2022-05-26 14:24:56 -03:00
Alex Hart
63f4f0bcec Fix note to self label in conversation settings. 2022-05-26 14:24:56 -03:00
Alex Hart
6dec6cef27 Add decline code messages into expiration sheet. 2022-05-24 15:03:54 -03:00
Greyson Parrelli
4d8faffb75 Notify recipient changes after bulk registration update. 2022-05-24 15:03:54 -03:00
Alex Hart
fa6bb07e8a Update strings for unclear translations. 2022-05-24 15:03:54 -03:00
Cody Henthorne
d260c48393 Fix device linking issues on newer devices. 2022-05-24 15:03:54 -03:00
Cody Henthorne
cc31417c97 Fix desugar crash on spinner builds. 2022-05-24 15:03:54 -03:00
Alex Hart
4d2af5b536 Rotate gift badges flag. 2022-05-24 15:03:54 -03:00
Alex Hart
6029c8ae4a Bump version to 5.38.3 2022-05-24 14:31:18 -03:00
Alex Hart
3bc18c3300 Updated language translations. 2022-05-24 14:30:15 -03:00
Cody Henthorne
a652bc65cc Fix font version check timeout. 2022-05-23 22:06:18 -04:00
Cody Henthorne
53252aa797 Bump version to 5.39.2 2022-05-19 16:36:06 -04:00
Cody Henthorne
956c1d96af Updated language translations. 2022-05-19 16:31:46 -04:00
Greyson Parrelli
d0ecbda962 Hide attachment keyboard if system keyboard shows. 2022-05-19 08:53:12 -04:00
Greyson Parrelli
5d880e2b2a Fix bug where searching emoji would dismiss the view. 2022-05-19 08:50:40 -04:00
Greyson Parrelli
bb13be1e7a Bump version to 5.39.1 2022-05-18 17:57:56 -04:00
Greyson Parrelli
05975a0068 Fix scrolling to last seen. 2022-05-18 17:51:00 -04:00
Greyson Parrelli
153feb002e Update R8 to 3.3.28 2022-05-18 17:34:04 -04:00
Cody Henthorne
f63ed8f269 Bump version to 5.39.0 2022-05-18 14:18:31 -04:00
Cody Henthorne
139a503403 Updated language translations. 2022-05-18 14:11:46 -04:00
Cody Henthorne
db4d072bd9 Upgrade kotlin to 1.6.21
Also fix a collection of warnings.
2022-05-18 14:05:17 -04:00
Cody Henthorne
42b0842aab Fix ANR when showing media in notifications. 2022-05-18 11:54:17 -04:00
Greyson Parrelli
9ab275195f Add support for CDSI. 2022-05-18 11:54:17 -04:00
Alex Hart
8407f2ff69 Add guard against out of bounds indices in story viewer. 2022-05-18 11:54:17 -04:00
Greyson Parrelli
9d518879dd Update libsignal-client to 0.17.0 2022-05-18 11:54:17 -04:00
Alex Hart
588663b3c2 Add better handling for unexpected cancellations. 2022-05-18 11:54:17 -04:00
Alex Hart
77f8489e51 Scroll to top on chat press when already on that tab. 2022-05-18 11:54:17 -04:00
Cody Henthorne
3c08b070fc Fetch PNI Credential during own profile refresh. 2022-05-18 11:54:17 -04:00
Greyson Parrelli
dda5ce4809 Add basic CDSv2 database writes and unit tests. 2022-05-18 11:54:17 -04:00
Alex Hart
307be5c75e Ensure callback is registered for shaking gifts. 2022-05-18 11:54:17 -04:00
Alex Hart
a0b89051cf Add duration info to gift row item. 2022-05-18 11:54:17 -04:00
Alex Hart
a1025a8e9a Add expiry information to gift conversation items. 2022-05-18 11:54:17 -04:00
Alex Hart
ce2418ce9f Consolidate local badge writes. 2022-05-18 11:54:17 -04:00
Cody Henthorne
ec3540e200 Fix long text in Safety Number Change dialog. 2022-05-18 11:54:17 -04:00
Alex Hart
ff4311d114 Add outline around sent gift reply thumb. 2022-05-18 11:54:17 -04:00
Alex Hart
425a13e68c Mark sent gift viewed when opened. 2022-05-18 11:54:17 -04:00
Alex Hart
15af1d3bd1 Add default animations to gift flow. 2022-05-18 11:54:17 -04:00
Alex Hart
2d57cb4ed0 Enqueue profile refresh sync after badge redemption. 2022-05-18 11:54:17 -04:00
Alex Hart
25788ef751 Do not include self in recents list for gift badging. 2022-05-18 11:54:17 -04:00
Greyson Parrelli
b3086e595f Fix abbreviations with some emoji.
Fixes #12212
2022-05-18 11:54:17 -04:00
Greyson Parrelli
57e233413a Update string for group invite. 2022-05-18 11:54:17 -04:00
Greyson Parrelli
f5777d58fc Fix situation where two keyboards could be showing. 2022-05-18 11:54:17 -04:00
Alex Hart
6b55cd0128 Always pop open keyboard when opening group reply sheet. 2022-05-18 11:54:17 -04:00
Alex Hart
a03c49e12c Implement group story notifications. 2022-05-18 11:54:17 -04:00
Alex Hart
01543dd52b Utilize round outline for deleted messages. 2022-05-18 11:54:17 -04:00
Alex Hart
987f69227a Add polish to story replies button and direct reply sheet. 2022-05-18 11:54:17 -04:00
Alex Hart
e51841a28b Fix freeze of first story first post. 2022-05-18 11:54:17 -04:00
Alex Hart
bcfe2fef72 Hide gift badge row if user does not have capability set and rotate flag. 2022-05-18 11:54:17 -04:00
Alex Hart
9ed3f95ab8 Ignore duplicate stories in sync messages. 2022-05-18 11:54:17 -04:00
3333 changed files with 461483 additions and 336157 deletions

View File

@@ -8,17 +8,21 @@ on:
- '4.**'
- '5.**'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
- name: Validate Gradle Wrapper
@@ -32,7 +36,7 @@ jobs:
- name: Archive reports for failed build
if: ${{ failure() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: reports
path: '*/build/reports'

View File

@@ -4,15 +4,18 @@ on:
schedule:
- cron: '0 5 * * *'
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
- name: Build image
run: cd reproducible-builds && docker build -t signal-android . && cd ..
- name: Test build
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assembleRelease
run: docker run --rm -v $(pwd):/project -w /project signal-android ./gradlew clean assemblePlayProdRelease

1
.gitignore vendored
View File

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

View File

@@ -15,11 +15,6 @@ Truths which we believe to be self-evident:
1. **There is no such thing as time.** Protocol ideas that require synchronized clocks are doomed to failure.
## Translations
Thanks to a dedicated community of volunteer translators, Signal is now available in more than one hundred languages. We use Transifex to manage our translation efforts, not GitHub. Any suggestions, corrections, or new translations should be submitted to the [Signal localization project for Android](https://www.transifex.com/signalapp/signal-android/).
## Issues
### Useful bug reports
@@ -75,10 +70,6 @@ There are several other ways to get involved:
* Redirect support questions to support@signal.org and the [Signal Support Center](https://support.signal.org/).
* Redirect non-bug discussions to the [community forum](https://community.signalusers.org).
* Improve documentation in the [wiki](https://github.com/signalapp/Signal-Android/wiki).
* Join the community of volunteer translators on Transifex:
* [Android](https://www.transifex.com/signalapp/signal-android/)
* [iOS](https://www.transifex.com/signalapp/signal-ios/)
* [Desktop](https://www.transifex.com/signalapp/signal-desktop/)
* Find and mark duplicate issues.
* Try to reproduce issues and help with troubleshooting.
* Discover solutions to open issues and post any relevant findings.

View File

@@ -1,10 +1,10 @@
# Signal Android
Signal is a messaging app for simple private communication with friends.
Signal is a simple, powerful, and secure messenger.
Signal uses your phone's data connection (WiFi/3G/4G) to communicate securely, optionally supports plain SMS/MMS to function as a unified messenger, and can also encrypt the stored messages on your phone.
Signal uses your phone's data connection (WiFi/3G/4G/5G) to communicate securely. Millions of people use Signal every day for free and instantaneous communication anywhere in the world. Send and receive high-fidelity messages, participate in HD voice/video calls, and explore a growing set of new features that help you stay connected. Signals advanced privacy-preserving technology is always enabled, so you can focus on sharing the moments that matter with the people who matter to you.
Currently available on the Play store and [signal.org](https://signal.org/android/apk/).
Currently available on the Play Store and [signal.org](https://signal.org/android/apk/).
<a href='https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height='80px'/></a>
@@ -18,14 +18,9 @@ Want to live life on the bleeding edge and help out with testing?
You can subscribe to Signal Android Beta releases here:
https://play.google.com/apps/testing/org.thoughtcrime.securesms
If you're interested in a life of peace and tranquility, stick with the standard releases.
## Contributing Translations
Interested in helping to translate Signal? Contribute here:
https://www.transifex.com/projects/p/signal-android/
## Contributing Code
If you're new to the Signal codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar. Also please have a look at the [CONTRIBUTING.md](https://github.com/signalapp/Signal-Android/blob/main/CONTRIBUTING.md), that might answer some of your questions.
@@ -33,7 +28,7 @@ If you're new to the Signal codebase, we recommend going through our issues and
For larger changes and feature ideas, we ask that you propose it on the [unofficial Community Forum](https://community.signalusers.org) for a high-level discussion with the wider community before implementation.
## Contributing Ideas
Have something you want to say about Open Whisper Systems projects or want to be part of the conversation? Get involved in the [community forum](https://community.signalusers.org).
Have something you want to say about Signal projects or want to be part of the conversation? Get involved in the [community forum](https://community.signalusers.org).
Help
====

View File

@@ -1,3 +1,5 @@
import com.android.build.api.dsl.ManagedVirtualDevice
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'com.google.protobuf'
@@ -11,20 +13,11 @@ apply from: 'static-ips.gradle'
repositories {
maven {
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
content {
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
url "https://raw.githubusercontent.com/signalapp/maven/master/sqlcipher/release/"
content {
includeGroupByRegex "org\\.signal.*"
}
}
maven {
url "https://www.jitpack.io"
}
google()
mavenCentral()
@@ -35,10 +28,6 @@ repositories {
jcenter {
content {
includeVersion "mobi.upod", "time-duration-picker", "1.1.3"
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
includeVersion "com.codewaves.stickyheadergrid", "stickyheadergrid", "0.9.4"
includeVersion "com.google.android", "flexbox", "0.3.0"
}
}
}
@@ -63,8 +52,8 @@ ktlint {
version = "0.43.2"
}
def canonicalVersionCode = 1055
def canonicalVersionName = "5.38.5"
def canonicalVersionCode = 1188
def canonicalVersionName = "6.7.2"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
@@ -82,10 +71,12 @@ def selectableVariants = [
'playProdDebug',
'playProdSpinner',
'playProdPerf',
'playProdInstrumentation',
'playProdRelease',
'playStagingDebug',
'playStagingSpinner',
'playStagingPerf',
'playStagingInstrumentation',
'playStagingRelease',
'websiteProdSpinner',
'websiteProdRelease',
@@ -97,16 +88,13 @@ android {
flavorDimensions 'distribution', 'environment'
useLibrary 'org.apache.http.legacy'
testBuildType 'instrumentation'
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = ["-Xallow-result-return-type"]
}
dexOptions {
javaMaxHeapSize "4g"
}
signingConfigs {
if (keystores.debug != null) {
debug {
@@ -124,6 +112,17 @@ android {
unitTests {
includeAndroidResources = true
}
managedDevices {
devices {
pixel3api30 (ManagedVirtualDevice) {
device = "Pixel 3"
apiLevel = 30
systemImageSource = "google-atd"
require64Bit = false
}
}
}
}
lintOptions {
@@ -157,8 +156,13 @@ android {
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
exclude 'libsignal_jni.dylib'
exclude 'signal_jni.dll'
}
buildFeatures {
viewBinding true
}
defaultConfig {
versionCode canonicalVersionCode * postFixSize
@@ -170,7 +174,9 @@ android {
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "Signal");
project.ext.set("archivesBaseName", "Signal")
manifestPlaceholders = [mapsKey:"AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"]
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
@@ -179,12 +185,13 @@ 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_CDSH_URL", "\"https://cdsh.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\""
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\"}"
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\"}"
buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\""
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}"
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\", \"https://sfu.staging.test.voip.signal.org\"}"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String[]", "SIGNAL_SERVICE_IPS", service_ips
@@ -196,19 +203,16 @@ android {
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
buildConfigField "String", "CDSH_CODE_HASH", "\"2f79dc6c1599b71c70fc2d14f3ea2e3bc65134436eb87011c88845b137af673a\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
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[] {" +
"new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" +
"}"
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+ikU5gBXsRmoF94GXQ==\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
@@ -237,7 +241,7 @@ android {
}
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunner "org.thoughtcrime.securesms.testing.SignalTestRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
@@ -269,8 +273,21 @@ android {
testProguardFiles 'proguard/proguard-automation.pro',
'proguard/proguard.cfg'
manifestPlaceholders = [mapsKey:getMapsKey()]
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
}
instrumentation {
initWith debug
isDefault false
minifyEnabled false
matchingFallbacks = ['debug']
applicationIdSuffix ".instrumentation"
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Instrumentation\""
}
spinner {
initWith debug
isDefault false
@@ -339,18 +356,17 @@ 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[] {" +
"new org.thoughtcrime.securesms.KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" +
"}"
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+UwyIlIT9L7KLS19Aw8r4sPrXQ==\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUj\""
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\""
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
@@ -404,10 +420,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
@@ -420,7 +437,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
@@ -456,6 +475,10 @@ dependencies {
implementation project(':image-editor')
implementation project(':donations')
implementation project(':contacts')
implementation project(':qr')
implementation project(':sms-exporter')
implementation project(':sticky-header-grid')
implementation project(':photoview')
implementation libs.libsignal.android
implementation libs.google.protobuf.javalite
@@ -476,22 +499,18 @@ dependencies {
implementation libs.emilsjolander.stickylistheaders
implementation libs.jpardogo.materialtabstrip
implementation libs.apache.httpclient.android
implementation libs.photoview
implementation libs.glide.glide
implementation libs.roundedimageview
implementation libs.materialish.progress
implementation libs.greenrobot.eventbus
implementation libs.waitingdots
implementation libs.floatingactionbutton
implementation libs.google.zxing.android.integration
implementation libs.time.duration.picker
implementation libs.google.zxing.core
implementation libs.google.flexbox
implementation (libs.subsampling.scale.image.view) {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation (libs.numberpickerview) {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation (libs.android.tooltips) {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
@@ -500,16 +519,9 @@ dependencies {
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation libs.stream
implementation (libs.colorpicker) {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
implementation libs.lottie
implementation libs.stickyheadergrid
implementation libs.circular.progress.button
implementation libs.signal.android.database.sqlcipher
implementation libs.androidx.sqlite
@@ -536,11 +548,22 @@ dependencies {
force = true
}
testImplementation testLibs.hamcrest.hamcrest
testImplementation testLibs.mockk
testImplementation(testFixtures(project(":libsignal-service")))
androidTestImplementation testLibs.androidx.test.ext.junit
androidTestImplementation testLibs.espresso.core
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
@@ -552,7 +575,7 @@ dependencies {
implementation libs.rxjava3.rxkotlin
implementation libs.rxdogtag
androidTestUtil 'androidx.test:orchestrator:1.4.1'
androidTestUtil testLibs.androidx.test.orchestrator
}
def getLastCommitTimestamp() {
@@ -561,7 +584,7 @@ def getLastCommitTimestamp() {
}
new ByteArrayOutputStream().withStream { os ->
def result = exec {
exec {
executable = 'git'
args = ['log', '-1', '--pretty=format:%ct']
standardOutput = os
@@ -573,20 +596,20 @@ def getLastCommitTimestamp() {
def getGitHash() {
if (!(new File('.git').exists())) {
return "abcd1234"
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
}
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
commandLine 'git', 'rev-parse', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim()
return stdout.toString().trim().substring(0, 12)
}
def getCurrentGitTag() {
if (!(new File('.git').exists())) {
return ''
throw new IllegalStateException("Must be a git repository to guarantee reproducible builds! (git hash is part of APK)")
}
def stdout = new ByteArrayOutputStream()
@@ -620,14 +643,22 @@ def loadKeystoreProperties(filename) {
if (keystorePropertiesFile.exists()) {
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
return keystoreProperties;
return keystoreProperties
} else {
return null;
return null
}
}
def getDateSuffix() {
static def getDateSuffix() {
def date = new Date()
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

@@ -25,6 +25,7 @@
<issue id="VectorRaster" severity="error" />
<issue id="ButtonOrder" severity="error" />
<issue id="ExtraTranslation" severity="warning" />
<issue id="UnspecifiedImmutableFlag" severity="error" />
<!-- Custom lints -->
<issue id="LogNotSignal" severity="error" />
@@ -41,4 +42,5 @@
<ignore path="*/org/thoughtcrime/securesms/jobs/StickerPackDownloadJob.java" />
</issue>
<issue id="OptionalUsedAsFieldOrParameterType" severity="ignore" />
</lint>

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,380 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Before
import org.junit.Ignore
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
@FlakyTest
@Ignore("Test sometimes requires manual intervention to continue.")
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

@@ -0,0 +1,154 @@
package org.thoughtcrime.securesms.conversation
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import java.util.Optional
/**
* Helper test for rendering conversation items for preview.
*/
@RunWith(AndroidJUnit4::class)
@Ignore("For testing/previewing manually, no assertions")
class ConversationItemPreviewer {
@get:Rule
val harness = SignalActivityRule(othersCount = 10)
@Test
fun testShowLongName() {
val other: Recipient = Recipient.resolved(harness.others.first())
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("Seef", "$$$"))
insertFailedMediaMessage(other = other, attachmentCount = 1)
insertFailedMediaMessage(other = other, attachmentCount = 2)
insertFailedMediaMessage(other = other, body = "Test", attachmentCount = 1)
// insertFailedOutgoingMediaMessage(other = other, body = "Test", attachmentCount = 1)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
// insertMediaMessage(other = other)
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
scenario.onActivity {
}
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
// ThreadUtil.sleep(45000)
}
private fun insertMediaMessage(other: Recipient, body: String? = null, attachmentCount: Int = 1) {
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
attachment()
}
val message = IncomingMediaMessage(
from = other.id,
body = body,
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = System.currentTimeMillis(),
receivedTimeMillis = System.currentTimeMillis(),
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
)
SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
ThreadUtil.sleep(1)
}
private fun insertFailedMediaMessage(other: Recipient, body: String? = null, attachmentCount: Int = 1) {
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
attachment()
}
val message = IncomingMediaMessage(
from = other.id,
body = body,
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = System.currentTimeMillis(),
receivedTimeMillis = System.currentTimeMillis(),
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
)
val insert = SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(other)).get()
SignalDatabase.attachments.getAttachmentsForMessage(insert.messageId).forEachIndexed { index, attachment ->
// if (index != 1) {
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachment.attachmentId, insert.messageId)
// } else {
// SignalDatabase.attachments.setTransferState(insert.messageId, attachment, TRANSFER_PROGRESS_STARTED)
// }
}
ThreadUtil.sleep(1)
}
private fun insertFailedOutgoingMediaMessage(other: Recipient, body: String? = null, attachmentCount: Int = 1) {
val attachments: List<SignalServiceAttachmentPointer> = (0 until attachmentCount).map {
attachment()
}
val message = OutgoingMessage(
recipient = other,
body = body,
attachments = PointerAttachment.forPointers(Optional.of(attachments)),
timestamp = System.currentTimeMillis(),
isSecure = true
)
val insert = SignalDatabase.messages.insertMessageOutbox(
message,
SignalDatabase.threads.getOrCreateThreadIdFor(other),
false,
null
)
SignalDatabase.attachments.getAttachmentsForMessage(insert).forEachIndexed { index, attachment ->
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachment.attachmentId, insert)
}
ThreadUtil.sleep(1)
}
private fun attachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
Optional.empty(),
Optional.empty(),
1024,
1024,
Optional.empty(),
Optional.of("/not-there.jpg"),
false,
false,
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
)
}
}

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.conversation
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
import org.thoughtcrime.securesms.testing.SignalActivityRule
/**
* Android test to help show SNC dialog quickly with custom data to make sure it displays properly.
*/
@Ignore("For testing/previewing manually, no assertions")
@RunWith(AndroidJUnit4::class)
class SafetyNumberChangeDialogPreviewer {
@get:Rule val harness = SignalActivityRule(othersCount = 10)
@Test
fun testShowLongName() {
val other: Recipient = Recipient.resolved(harness.others.first())
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("Super really long name like omg", "But seriously it's long like really really long"))
harness.setVerified(other, IdentityTable.VerifiedStatus.VERIFIED)
harness.changeIdentityKey(other)
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
scenario.onActivity {
SafetyNumberBottomSheet.forRecipientId(other.id).show(it.supportFragmentManager)
}
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
// ThreadUtil.sleep(15000)
}
@Test
fun testShowLargeSheet() {
SignalDatabase.distributionLists.setPrivacyMode(DistributionListId.MY_STORY, DistributionListPrivacyMode.ONLY_WITH)
val othersRecipients = harness.others.map { Recipient.resolved(it) }
othersRecipients.forEach { other ->
SignalDatabase.recipients.setProfileName(other.id, ProfileName.fromParts("My", "Name"))
harness.setVerified(other, IdentityTable.VerifiedStatus.DEFAULT)
harness.changeIdentityKey(other)
SignalDatabase.distributionLists.addMemberToList(DistributionListId.MY_STORY, DistributionListPrivacyMode.ONLY_WITH, other.id)
}
val myStoryRecipientId = SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!!
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", harness.others.first().serialize()) }
scenario.onActivity { conversationActivity ->
SafetyNumberBottomSheet
.forIdentityRecordsAndDestinations(
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
destinations = listOf(ContactSearchKey.RecipientSearchKey.Story(myStoryRecipientId))
)
.show(conversationActivity.supportFragmentManager)
}
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
// ThreadUtil.sleep( 30000)
}
}

View File

@@ -0,0 +1,106 @@
package org.thoughtcrime.securesms.database
import android.net.Uri
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.mms.MediaStream
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.MediaUtil
import java.util.Optional
@RunWith(AndroidJUnit4::class)
class AttachmentTableTest {
@Before
fun setUp() {
SignalDatabase.attachments.deleteAllAttachments()
}
@Test
fun givenABlob_whenIInsert2AttachmentsForPreUpload_thenIExpectDistinctIdsButSameFileName() {
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
val highQualityProperties = createHighQualityTransformProperties()
val highQualityImage = createAttachment(1, blob, highQualityProperties)
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
assertNotEquals(attachment2.attachmentId, attachment.attachmentId)
assertEquals(attachment2.fileName, attachment.fileName)
}
@Test
fun givenABlobAndDifferentTransformQuality_whenIInsert2AttachmentsForPreUpload_thenIExpectDifferentFileInfos() {
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
val highQualityProperties = createHighQualityTransformProperties()
val highQualityImage = createAttachment(1, blob, highQualityProperties)
val lowQualityImage = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(lowQualityImage)
SignalDatabase.attachments.updateAttachmentData(
attachment,
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
false
)
SignalDatabase.attachments.updateAttachmentData(
attachment2,
createMediaStream(byteArrayOf(1, 2, 3)),
false
)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
assertNotEquals(attachment1Info, attachment2Info)
}
@Test
fun givenIdenticalAttachmentsInsertedForPreUpload_whenIUpdateAttachmentDataAndSpecifyOnlyModifyThisAttachment_thenIExpectDifferentFileInfos() {
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
val highQualityProperties = createHighQualityTransformProperties()
val highQualityImage = createAttachment(1, blob, highQualityProperties)
val attachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
val attachment2 = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityImage)
SignalDatabase.attachments.updateAttachmentData(
attachment,
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
true
)
SignalDatabase.attachments.updateAttachmentData(
attachment2,
createMediaStream(byteArrayOf(1, 2, 3, 4)),
true
)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA)
assertNotEquals(attachment1Info, attachment2Info)
}
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties): UriAttachment {
return UriAttachmentBuilder.build(
id,
uri = uri,
contentType = MediaUtil.IMAGE_JPEG,
transformProperties = transformProperties
)
}
private fun createHighQualityTransformProperties(): AttachmentTable.TransformProperties {
return AttachmentTable.TransformProperties.forSentMediaQuality(Optional.empty(), SentMediaQuality.HIGH)
}
private fun createMediaStream(byteArray: ByteArray): MediaStream {
return MediaStream(byteArray.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
}
}

View File

@@ -10,9 +10,9 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import java.util.UUID
class DistributionListDatabaseTest {
class DistributionListTablesTest {
private lateinit var distributionDatabase: DistributionListDatabase
private lateinit var distributionDatabase: DistributionListTables
@Before
fun setup() {
@@ -80,25 +80,6 @@ class DistributionListDatabaseTest {
Assert.assertEquals(StoryType.STORY_WITHOUT_REPLIES, storyType)
}
@Test
fun givenStoryExistsAndMarkedNoReplies_getAllListsForContactSelectionUi_returnsStoryWithoutReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
distributionDatabase.setAllowsReplies(id!!, false)
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
Assert.assertFalse(records.first().allowsReplies)
}
@Test
fun givenStoryExists_getAllListsForContactSelectionUi_returnsStoryWithReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val records = distributionDatabase.getAllListsForContactSelectionUi(null, false)
Assert.assertTrue(records.first().allowsReplies)
}
@Test(expected = IllegalStateException::class)
fun givenStoryDoesNotExist_getStoryType_throwsIllegalStateException() {
distributionDatabase.getStoryType(DistributionListId.from(12))

View File

@@ -1,37 +0,0 @@
package org.thoughtcrime.securesms.database
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.database.model.DonationReceiptRecord
import java.math.BigDecimal
import java.util.Currency
class DonationReceiptDatabaseTest {
private val records = listOf(
DonationReceiptRecord.createForBoost(FiatMoney(BigDecimal.valueOf(100), Currency.getInstance("USD"))),
DonationReceiptRecord.createForBoost(FiatMoney(BigDecimal.valueOf(200), Currency.getInstance("USD")))
)
@Test
fun givenNoReceipts_whenICheckHasReceipts_thenIExpectFalse() {
assertFalse(SignalDatabase.donationReceipts.hasReceipts())
}
@Test
fun givenOneReceipt_whenICheckHasReceipts_thenIExpectTrue() {
SignalDatabase.donationReceipts.addReceipt(records.first())
assertTrue(SignalDatabase.donationReceipts.hasReceipts())
}
@Test
fun givenMultipleReceipts_whenICheckHasReceipts_thenIExpectTrue() {
records.forEach {
SignalDatabase.donationReceipts.addReceipt(it)
}
assertTrue(SignalDatabase.donationReceipts.hasReceipts())
}
}

View File

@@ -0,0 +1,188 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
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.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class MessageTableTest_gifts {
private lateinit var mms: MessageTable
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
private lateinit var recipients: List<RecipientId>
@Before
fun setUp() {
mms = SignalDatabase.messages
mms.deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
}
@Test
fun givenNoSentGifts_whenISetOutgoingGiftsRevealed_thenIExpectEmptyList() {
val result = mms.setOutgoingGiftsRevealed(listOf(1))
assertTrue(result.isEmpty())
}
@Test
fun givenSentGift_whenISetOutgoingGiftsRevealed_thenIExpectNonEmptyListContainingThatGift() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
assertTrue(result.isNotEmpty())
assertEquals(messageId, result.first().messageId.id)
}
@Test
fun givenViewedSentGift_whenISetOutgoingGiftsRevealed_thenIExpectEmptyList() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
mms.setOutgoingGiftsRevealed(listOf(messageId))
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
assertTrue(result.isEmpty())
}
@Test
fun givenMultipleSentGift_whenISetOutgoingGiftsRevealedForOne_thenIExpectNonEmptyListContainingThatGift() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId))
assertEquals(1, result.size)
assertEquals(messageId, result.first().messageId.id)
}
@Test
fun givenMultipleSentGift_whenISetOutgoingGiftsRevealedForBoth_thenIExpectNonEmptyListContainingThoseGifts() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId2 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
assertEquals(listOf(messageId, messageId2), result.map { it.messageId.id })
}
@Test
fun givenMultipleSentGiftAndNonGift_whenISetOutgoingGiftsRevealedForBothGifts_thenIExpectNonEmptyListContainingJustThoseGifts() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId2 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = null
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2))
assertEquals(listOf(messageId, messageId2), result.map { it.messageId.id })
}
@Test
fun givenMultipleSentGiftAndNonGift_whenISetOutgoingGiftsRevealedForAllThree_thenIExpectNonEmptyListContainingJustThoseGifts() {
val messageId = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId2 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId3 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = null
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId, messageId2, messageId3))
assertEquals(listOf(messageId, messageId2), result.map { it.messageId.id })
}
@Test
fun givenMultipleSentGiftAndNonGift_whenISetOutgoingGiftsRevealedForNonGift_thenIExpectEmptyList() {
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = GiftBadge.getDefaultInstance()
)
val messageId3 = MmsHelper.insert(
recipient = Recipient.resolved(recipients[0]),
sentTimeMillis = 1,
giftBadge = null
)
val result = mms.setOutgoingGiftsRevealed(listOf(messageId3))
assertTrue(result.isEmpty())
}
}

View File

@@ -1,194 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
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.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class MmsDatabaseTest_stories {
private lateinit var mms: MmsDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
private lateinit var myStory: Recipient
private lateinit var recipients: List<RecipientId>
@Before
fun setUp() {
mms = SignalDatabase.mms
mms.deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
}
@Test
fun givenNoStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectAnEmptyList() {
// WHEN
val result = mms.orderedStoryRecipientsAndIds
// THEN
assertEquals(0, result.size)
}
@Test
fun givenOneOutgoingAndOneIncomingStory_whenIGetOrderedStoryRecipientsAndIds_thenIExpectIncomingThenOutgoing() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
val sender = recipients[0]
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 1,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = threadId
)
MmsHelper.insert(
IncomingMediaMessage(
from = sender,
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
)
// WHEN
val result = mms.orderedStoryRecipientsAndIds
// THEN
assertEquals(listOf(sender.toLong(), myStory.id.toLong()), result.map { it.recipientId.toLong() })
}
@Test
fun givenAStory_whenISetIncomingStoryMessageViewed_thenIExpectASetReceiptTimestamp() {
// GIVEN
val sender = recipients[0]
val messageId = MmsHelper.insert(
IncomingMediaMessage(
from = sender,
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
).get().messageId
val messageBeforeMark = SignalDatabase.mms.getMessageRecord(messageId)
assertFalse(messageBeforeMark.incomingStoryViewedAtTimestamp > 0)
// WHEN
SignalDatabase.mms.setIncomingMessageViewed(messageId)
// THEN
val messageAfterMark = SignalDatabase.mms.getMessageRecord(messageId)
assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0)
}
@Test
fun given5ViewedStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectLatestViewedFirst() {
// GIVEN
val messageIds = recipients.take(5).map {
MmsHelper.insert(
IncomingMediaMessage(
from = it,
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
).get().messageId
}
val randomizedOrderedIds = messageIds.shuffled()
randomizedOrderedIds.forEach {
SignalDatabase.mms.setIncomingMessageViewed(it)
Thread.sleep(5)
}
// WHEN
val result = SignalDatabase.mms.orderedStoryRecipientsAndIds
val resultOrderedIds = result.map { it.messageId }
// THEN
assertEquals(randomizedOrderedIds.reversed(), resultOrderedIds)
}
@Test
fun given15Stories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectUnviewedThenInterspersedViewedAndSelfSendsAllDescending() {
val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
val unviewedIds: List<Long> = (0 until 5).map {
Thread.sleep(5)
MmsHelper.insert(
IncomingMediaMessage(
from = recipients[it],
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
).get().messageId
}
val viewedIds: List<Long> = (0 until 5).map {
Thread.sleep(5)
MmsHelper.insert(
IncomingMediaMessage(
from = recipients[it],
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
).get().messageId
}
val interspersedIds: List<Long> = (0 until 10).map {
Thread.sleep(5)
if (it % 2 == 0) {
SignalDatabase.mms.setIncomingMessageViewed(viewedIds[it / 2])
viewedIds[it / 2]
} else {
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = System.currentTimeMillis(),
storyType = StoryType.STORY_WITH_REPLIES,
threadId = myStoryThread
)
}
}
val result = SignalDatabase.mms.orderedStoryRecipientsAndIds
val resultOrderedIds = result.map { it.messageId }
assertEquals(unviewedIds.reversed() + interspersedIds.reversed(), resultOrderedIds)
}
}

View File

@@ -1,8 +1,10 @@
package org.thoughtcrime.securesms.database
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import java.util.Optional
@@ -18,29 +20,27 @@ object MmsHelper {
subscriptionId: Int = -1,
expiresIn: Long = 0,
viewOnce: Boolean = false,
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
threadId: Long = 1,
storyType: StoryType = StoryType.NONE
distributionType: Int = ThreadTable.DistributionTypes.DEFAULT,
threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipient, distributionType),
storyType: StoryType = StoryType.NONE,
parentStoryId: ParentStoryId? = null,
isStoryReaction: Boolean = false,
giftBadge: GiftBadge? = null,
secure: Boolean = true
): Long {
val message = OutgoingMediaMessage(
recipient,
body,
emptyList(),
sentTimeMillis,
subscriptionId,
expiresIn,
viewOnce,
distributionType,
storyType,
null,
false,
null,
emptyList(),
emptyList(),
emptyList(),
emptySet(),
emptySet(),
null
val message = OutgoingMessage(
recipient = recipient,
body = body,
timestamp = sentTimeMillis,
subscriptionId = subscriptionId,
expiresIn = expiresIn,
viewOnce = viewOnce,
distributionType = distributionType,
storyType = storyType,
parentStoryId = parentStoryId,
isStoryReaction = isStoryReaction,
giftBadge = giftBadge,
isSecure = secure
)
return insert(
@@ -50,16 +50,16 @@ object MmsHelper {
}
fun insert(
message: OutgoingMediaMessage,
message: OutgoingMessage,
threadId: Long
): Long {
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
return SignalDatabase.messages.insertMessageOutbox(message, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null)
}
fun insert(
message: IncomingMediaMessage,
threadId: Long
): Optional<MessageDatabase.InsertResult> {
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId)
): Optional<MessageTable.InsertResult> {
return SignalDatabase.messages.insertSecureDecryptedMessageInbox(message, threadId)
}
}

View File

@@ -0,0 +1,380 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
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
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
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.util.UUID
import java.util.concurrent.TimeUnit
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class MmsTableTest_stories {
private lateinit var mms: MessageTable
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
private lateinit var myStory: Recipient
private lateinit var recipients: List<RecipientId>
private lateinit var releaseChannelRecipient: Recipient
@Before
fun setUp() {
mms = SignalDatabase.messages
mms.deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())) }
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelRecipient.id)
}
@Test
fun givenNoStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectAnEmptyList() {
// WHEN
val result = mms.getOrderedStoryRecipientsAndIds(false)
// THEN
assertEquals(0, result.size)
}
@Test
fun givenOneOutgoingAndOneIncomingStory_whenIGetOrderedStoryRecipientsAndIds_thenIExpectIncomingThenOutgoing() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
val sender = recipients[0]
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 1,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = threadId
)
MmsHelper.insert(
IncomingMediaMessage(
from = sender,
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
)
// WHEN
val result = mms.getOrderedStoryRecipientsAndIds(false)
// THEN
assertEquals(listOf(sender.toLong(), myStory.id.toLong()), result.map { it.recipientId.toLong() })
}
@Test
fun givenAStory_whenISetIncomingStoryMessageViewed_thenIExpectASetReceiptTimestamp() {
// GIVEN
val sender = recipients[0]
val messageId = MmsHelper.insert(
IncomingMediaMessage(
from = sender,
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES
),
-1L
).get().messageId
val messageBeforeMark = SignalDatabase.messages.getMessageRecord(messageId)
assertFalse(messageBeforeMark.incomingStoryViewedAtTimestamp > 0)
// WHEN
SignalDatabase.messages.setIncomingMessageViewed(messageId)
// THEN
val messageAfterMark = SignalDatabase.messages.getMessageRecord(messageId)
assertTrue(messageAfterMark.incomingStoryViewedAtTimestamp > 0)
}
@Ignore
@Test
fun given5ViewedStories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectLatestViewedFirst() {
// GIVEN
val messageIds = recipients.take(5).map {
MmsHelper.insert(
IncomingMediaMessage(
from = it,
sentTimeMillis = 2,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
).get().messageId
}
val randomizedOrderedIds = messageIds.shuffled()
randomizedOrderedIds.forEach {
SignalDatabase.messages.setIncomingMessageViewed(it)
Thread.sleep(5)
}
// WHEN
val result = SignalDatabase.messages.getOrderedStoryRecipientsAndIds(false)
val resultOrderedIds = result.map { it.messageId }
// THEN
assertEquals(randomizedOrderedIds.reversed(), resultOrderedIds)
}
@Test
fun given15Stories_whenIGetOrderedStoryRecipientsAndIds_thenIExpectUnviewedThenInterspersedViewedAndSelfSendsAllDescending() {
val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
val unviewedIds: List<Long> = (0 until 5).map {
Thread.sleep(5)
MmsHelper.insert(
IncomingMediaMessage(
from = recipients[it],
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
).get().messageId
}
val viewedIds: List<Long> = (0 until 5).map {
Thread.sleep(5)
MmsHelper.insert(
IncomingMediaMessage(
from = recipients[it],
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
).get().messageId
}
val interspersedIds: List<Long> = (0 until 10).map {
Thread.sleep(5)
if (it % 2 == 0) {
SignalDatabase.messages.setIncomingMessageViewed(viewedIds[it / 2])
viewedIds[it / 2]
} else {
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = System.currentTimeMillis(),
storyType = StoryType.STORY_WITH_REPLIES,
threadId = myStoryThread
)
}
}
val result = SignalDatabase.messages.getOrderedStoryRecipientsAndIds(false)
val resultOrderedIds = result.map { it.messageId }
assertEquals(unviewedIds.reversed() + interspersedIds.reversed(), resultOrderedIds)
}
@Test
fun givenNoStories_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectFalse() {
// WHEN
val result = mms.isOutgoingStoryAlreadyInDatabase(recipients[0], 200)
// THEN
assertFalse(result)
}
@Test
fun givenNoOutgoingStories_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectFalse() {
// GIVEN
MmsHelper.insert(
IncomingMediaMessage(
from = recipients[0],
sentTimeMillis = 200,
serverTimeMillis = 2,
receivedTimeMillis = 2,
storyType = StoryType.STORY_WITH_REPLIES,
),
-1L
)
// WHEN
val result = mms.isOutgoingStoryAlreadyInDatabase(recipients[0], 200)
// THEN
assertFalse(result)
}
@Test
fun givenOutgoingStoryExistsForRecipientAndTime_whenICheckIsOutgoingStoryAlreadyInDatabase_thenIExpectTrue() {
// GIVEN
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES
)
// WHEN
val result = mms.isOutgoingStoryAlreadyInDatabase(myStory.id, 200)
// THEN
assertTrue(result)
}
@Test
fun givenAGroupStoryWithNoReplies_whenICheckHasSelfReplyInGroupStory_thenIExpectFalse() {
// GIVEN
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
)
// WHEN
val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
// THEN
assertFalse(result)
}
@Ignore
@Test
fun givenAGroupStoryWithAReplyFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectTrue() {
// GIVEN
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
)
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 201,
storyType = StoryType.NONE,
parentStoryId = ParentStoryId.GroupReply(groupStoryId)
)
// WHEN
val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
// THEN
assertTrue(result)
}
@Test
fun givenAGroupStoryWithAReactionFromSelf_whenICheckHasSelfReplyInGroupStory_thenIExpectTrue() {
// GIVEN
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES
)
MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 201,
storyType = StoryType.NONE,
parentStoryId = ParentStoryId.GroupReply(groupStoryId),
isStoryReaction = true
)
// WHEN
val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
// THEN
assertTrue(result)
}
@Test
fun givenAGroupStoryWithAReplyFromSomeoneElse_whenICheckHasSelfReplyInGroupStory_thenIExpectFalse() {
// GIVEN
val groupStoryId = MmsHelper.insert(
recipient = myStory,
sentTimeMillis = 200,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = -1L
)
MmsHelper.insert(
IncomingMediaMessage(
from = myStory.id,
sentTimeMillis = 201,
serverTimeMillis = 201,
receivedTimeMillis = 202,
parentStoryId = ParentStoryId.GroupReply(groupStoryId)
),
-1
)
// WHEN
val result = mms.hasGroupReplyOrReactionInStory(groupStoryId)
// THEN
assertFalse(result)
}
@Test
fun givenNotViewedOnboardingAndOnlyStoryIsOnboardingAndAdded2DaysAgo_whenIGetOldestStoryTimestamp_thenIExpectNull() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(releaseChannelRecipient)
MmsHelper.insert(
recipient = releaseChannelRecipient,
sentTimeMillis = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2),
storyType = StoryType.STORY_WITH_REPLIES,
threadId = threadId
)
// WHEN
val oldestTimestamp = SignalDatabase.messages.getOldestStorySendTimestamp(false)
// THEN
assertNull(oldestTimestamp)
}
@Test
fun givenViewedOnboardingAndOnlyStoryIsOnboardingAndAdded2DaysAgo_whenIGetOldestStoryTimestamp_thenIExpectNotNull() {
// GIVEN
val expected = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(2)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(releaseChannelRecipient)
MmsHelper.insert(
recipient = releaseChannelRecipient,
sentTimeMillis = expected,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = threadId
)
// WHEN
val oldestTimestamp = SignalDatabase.messages.getOldestStorySendTimestamp(true)
// THEN
assertEquals(expected, oldestTimestamp)
}
}

View File

@@ -1,786 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
import org.signal.core.util.ThreadUtil
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.state.SessionRecord
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_getAndPossiblyMerge {
private lateinit var recipientDatabase: RecipientDatabase
private lateinit var identityDatabase: IdentityDatabase
private lateinit var groupReceiptDatabase: GroupReceiptDatabase
private lateinit var groupDatabase: GroupDatabase
private lateinit var threadDatabase: ThreadDatabase
private lateinit var smsDatabase: MessageDatabase
private lateinit var mmsDatabase: MessageDatabase
private lateinit var sessionDatabase: SessionDatabase
private lateinit var mentionDatabase: MentionDatabase
private lateinit var reactionDatabase: ReactionDatabase
private lateinit var notificationProfileDatabase: NotificationProfileDatabase
private lateinit var distributionListDatabase: DistributionListDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
recipientDatabase = SignalDatabase.recipients
identityDatabase = SignalDatabase.identities
groupReceiptDatabase = SignalDatabase.groupReceipts
groupDatabase = SignalDatabase.groups
threadDatabase = SignalDatabase.threads
smsDatabase = SignalDatabase.sms
mmsDatabase = SignalDatabase.mms
sessionDatabase = SignalDatabase.sessions
mentionDatabase = SignalDatabase.mentions
reactionDatabase = SignalDatabase.reactions
notificationProfileDatabase = SignalDatabase.notificationProfiles
distributionListDatabase = SignalDatabase.distributionLists
ensureDbEmpty()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
// ==============================================================
// If both the ACI and E164 map to no one
// ==============================================================
/** If all you have is an ACI, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertFalse(recipient.hasE164())
}
/** If all you have is an ACI, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertFalse(recipient.hasE164())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasServiceId())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasServiceId())
}
/** With high trust, you can associate an ACI-e164 pair. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertEquals(E164_A, recipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore can only store the ACI. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireServiceId())
assertFalse(recipient.hasE164())
}
// ==============================================================
// If the ACI maps to an existing user, but the E164 doesn't
// ==============================================================
/** With high trust, you can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore cannot store the e164. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_lowTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
}
/** Basically the change number case. High trust lets you update the existing user. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
/** Low trust means you cant update the underlying data, but you also dont need to create any new rows. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_lowTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
// ==============================================================
// If the E164 maps to an existing user, but the ACI doesn't
// ==============================================================
/** With high trust, you can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore need to create a new person with just the ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_lowTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(E164_A, existingRecipient.requireE164())
assertFalse(existingRecipient.hasServiceId())
}
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. But high trust lets us take the e164 from the current holder. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
recipientDatabase.setPni(existingId, PNI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
recipientDatabase.setPni(retrievedId, PNI_A)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertFalse(existingRecipient.hasE164())
}
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. And low trust means we cant take the e164. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_lowTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, false)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertEquals(E164_A, existingRecipient.requireE164())
}
/** We never want to remove the e164 of our own contact entry. So basically treat this as a low-trust case, and leave the e164 alone. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser_highTrust() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireServiceId())
assertEquals(E164_A, existingRecipient.requireE164())
}
// ==============================================================
// If both the ACI and E164 map to an existing user
// ==============================================================
/** Regardless of trust, if your ACI and e164 match, youre good. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
changeNumberListener.waitForJobManager()
assertFalse(changeNumberListener.numberChangeWasEnqueued)
}
/** Same as [getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust], but with a number change. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingAciId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingE164Id: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
changeNumberListener.waitForJobManager()
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** Low trust means you cant merge. If youre retrieving a user from the table with this data, prefer the ACI one. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_lowTrust() {
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(E164_A, existingE164Recipient.requireE164())
assertFalse(existingE164Recipient.hasServiceId())
}
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_highTrust() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertFalse(existingRecipient2.hasE164())
assert(changeNumberListener.numberChangeWasEnqueued)
}
/** Another low trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_lowTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireServiceId())
assertEquals(E164_A, existingRecipient2.requireE164())
}
/**
* Another high trust case that results in a merge. Nothing strictly new here, but this case is called out because its a merge but *also* an E164 change,
* which clients may need to know for UX purposes.
*/
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange_highTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
val recipientWithId2 = Recipient.resolved(existingId2)
assertEquals(retrievedId, recipientWithId2.id)
}
/** We never want to remove the e164 of our own contact entry. So basically treat this as a low-trust case, and leave the e164 alone. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_B.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId2, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertFalse(retrievedRecipient.hasE164())
val recipientWithId1 = Recipient.resolved(existingId1)
assertEquals(ACI_B, recipientWithId1.requireServiceId())
assertEquals(E164_A, recipientWithId1.requireE164())
}
/** This is a case where normally we'd update the E164 of a user, but here the changeSelf flag is disabled, so we shouldn't. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfFalse() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** This is a case where we're changing our own number, and it's allowed because changeSelf = true. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciBelongsToLocalUser_highTrust_changeSelfTrue() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, highTrust = true, changeSelf = true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
/** Verifying a case where a change number job is expected to be enqueued. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_highTrust_changedNumber() {
val changeNumberListener = ChangeNumberListener()
changeNumberListener.enqueue()
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_B, retrievedRecipient.requireE164())
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. */
@Test
fun getAndPossiblyMerge_merge_general() {
// Setup
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_B)
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
val smsId3: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
val mmsId1: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
val mmsId2: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
val mmsId3: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
val threadIdAci: Long = threadDatabase.getThreadIdFor(recipientIdAci)!!
val threadIdE164: Long = threadDatabase.getThreadIdFor(recipientIdE164)!!
assertNotEquals(threadIdAci, threadIdE164)
mentionDatabase.insert(threadIdAci, mmsId1, listOf(Mention(recipientIdE164, 0, 1)))
mentionDatabase.insert(threadIdE164, mmsId2, listOf(Mention(recipientIdAci, 0, 1)))
groupReceiptDatabase.insert(listOf(recipientIdAci, recipientIdE164), mmsId1, 0, 3)
val identityKeyAci: IdentityKey = identityKey(1)
val identityKeyE164: IdentityKey = identityKey(2)
identityDatabase.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
identityDatabase.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
sessionDatabase.store(localAci, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
val profile1: NotificationProfile = notificationProfile(name = "Test")
val profile2: NotificationProfile = notificationProfile(name = "Test2")
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdAci)
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB)
val distributionListId: DistributionListId = distributionListDatabase.createList("testlist", listOf(recipientIdE164, recipientIdAciB))!!
// Merge
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
assertEquals(recipientIdAci, retrievedId)
// Recipient validation
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(recipientIdE164)
assertEquals(retrievedId, existingE164Recipient.id)
// Thread validation
assertEquals(threadIdAci, retrievedThreadId)
Assert.assertNull(threadDatabase.getThreadIdFor(recipientIdE164))
Assert.assertNull(threadDatabase.getThreadRecord(threadIdE164))
// SMS validation
val sms1: MessageRecord = smsDatabase.getMessageRecord(smsId1)!!
val sms2: MessageRecord = smsDatabase.getMessageRecord(smsId2)!!
val sms3: MessageRecord = smsDatabase.getMessageRecord(smsId3)!!
assertEquals(retrievedId, sms1.recipient.id)
assertEquals(retrievedId, sms2.recipient.id)
assertEquals(retrievedId, sms3.recipient.id)
assertEquals(retrievedThreadId, sms1.threadId)
assertEquals(retrievedThreadId, sms2.threadId)
assertEquals(retrievedThreadId, sms3.threadId)
// MMS validation
val mms1: MessageRecord = mmsDatabase.getMessageRecord(mmsId1)!!
val mms2: MessageRecord = mmsDatabase.getMessageRecord(mmsId2)!!
val mms3: MessageRecord = mmsDatabase.getMessageRecord(mmsId3)!!
assertEquals(retrievedId, mms1.recipient.id)
assertEquals(retrievedId, mms2.recipient.id)
assertEquals(retrievedId, mms3.recipient.id)
assertEquals(retrievedThreadId, mms1.threadId)
assertEquals(retrievedThreadId, mms2.threadId)
assertEquals(retrievedThreadId, mms3.threadId)
// Mention validation
val mention1: MentionModel = getMention(mmsId1)
assertEquals(retrievedId, mention1.recipientId)
assertEquals(retrievedThreadId, mention1.threadId)
val mention2: MentionModel = getMention(mmsId2)
assertEquals(retrievedId, mention2.recipientId)
assertEquals(retrievedThreadId, mention2.threadId)
// Group receipt validation
val groupReceipts: List<GroupReceiptDatabase.GroupReceiptInfo> = groupReceiptDatabase.getGroupReceiptInfo(mmsId1)
assertEquals(retrievedId, groupReceipts[0].recipientId)
assertEquals(retrievedId, groupReceipts[1].recipientId)
// Identity validation
assertEquals(identityKeyAci, identityDatabase.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
Assert.assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
// Session validation
Assert.assertNotNull(sessionDatabase.load(localAci, SignalProtocolAddress(ACI_A.toString(), 1)))
// Reaction validation
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
val reactionsMms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(mmsId1, true))
assertEquals(1, reactionsSms.size)
assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0])
assertEquals(1, reactionsMms.size)
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
// Notification Profile validation
val updatedProfile1: NotificationProfile = notificationProfileDatabase.getProfile(profile1.id)!!
val updatedProfile2: NotificationProfile = notificationProfileDatabase.getProfile(profile2.id)!!
MatcherAssert.assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
MatcherAssert.assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
// Distribution List validation
val updatedList: DistributionListRecord = distributionListDatabase.getList(distributionListId)!!
MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
}
// ==============================================================
// Misc
// ==============================================================
@Test
fun createByE164SanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
// WHEN I retrieve one by E164
val possible: Optional<RecipientId> = recipientDatabase.getByE164(E164_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.e164.isPresent)
assertEquals(E164_A, recipient.e164.get())
}
@Test
fun createByUuidSanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromServiceId(ACI_A)
// WHEN I retrieve one by UUID
val possible: Optional<RecipientId> = recipientDatabase.getByServiceId(ACI_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.serviceId.isPresent)
assertEquals(ACI_A, recipient.serviceId.get())
}
@Test(expected = IllegalArgumentException::class)
fun getAndPossiblyMerge_noArgs_invalid() {
recipientDatabase.getAndPossiblyMerge(null, null, true)
}
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME} WHERE ${RecipientDatabase.DISTRIBUTION_LIST_ID} IS NULL ", null).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0))
}
}
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
}
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty())
}
private fun identityKey(value: Byte): IdentityKey {
val bytes = ByteArray(33)
bytes[0] = 0x05
bytes[1] = value
return IdentityKey(bytes)
}
private fun notificationProfile(name: String): NotificationProfile {
return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
}
private fun groupMasterKey(value: Byte): GroupMasterKey {
val bytes = ByteArray(32)
bytes[0] = value
return GroupMasterKey(bytes)
}
private fun decryptedGroup(members: Collection<UUID>): DecryptedGroup {
return DecryptedGroup.newBuilder()
.addAllMembers(members.map { DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(it)).build() })
.build()
}
private fun getMention(messageId: Long): MentionModel {
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor ->
cursor.moveToFirst()
return MentionModel(
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)),
threadId = CursorUtil.requireLong(cursor, MentionDatabase.THREAD_ID)
)
}
}
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
data class MentionModel(
val recipientId: RecipientId,
val threadId: Long
)
private class ChangeNumberListener {
var numberChangeWasEnqueued = false
private set
fun waitForJobManager() {
ApplicationDependencies.getJobManager().flush()
ThreadUtil.sleep(500)
}
fun enqueue() {
ApplicationDependencies.getJobManager().addListener(
{ job -> job.factoryKey == RecipientChangedNumberJob.KEY },
{ _, _ -> numberChangeWasEnqueued = true }
)
}
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}

View File

@@ -0,0 +1,162 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
@RunWith(AndroidJUnit4::class)
class RecipientTableTest {
@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, RecipientTable.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, RecipientTable.ID)))
}
ids
}!!
assertNotEquals(0, results.size)
assertFalse(hiddenRecipient in results)
}
@Test
fun givenABlockedRecipient_whenIQueryAllContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results = SignalDatabase.recipients.queryAllContacts("Blocked")!!
assertEquals(0, results.count)
}
@Test
fun givenABlockedRecipient_whenIGetSignalContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results: MutableList<RecipientId> = SignalDatabase.recipients.getSignalContacts(false)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
}!!
assertNotEquals(0, results.size)
assertFalse(blockedRecipient in results)
}
@Test
fun givenABlockedRecipient_whenIQuerySignalContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results = SignalDatabase.recipients.querySignalContacts("Blocked", false)!!
assertEquals(0, results.count)
}
@Test
fun givenABlockedRecipient_whenIQueryNonGroupContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results = SignalDatabase.recipients.queryNonGroupContacts("Blocked", false)!!
assertEquals(0, results.count)
}
@Test
fun givenABlockedRecipient_whenIGetNonGroupContacts_thenIDoNotExpectBlockedToBeReturned() {
val blockedRecipient = harness.others[0]
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results: MutableList<RecipientId> = SignalDatabase.recipients.getNonGroupContacts(false)?.use {
val ids = mutableListOf<RecipientId>()
while (it.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(it, RecipientTable.ID)))
}
ids
}!!
assertNotEquals(0, results.size)
assertFalse(blockedRecipient in results)
}
}

View File

@@ -0,0 +1,762 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil
import org.signal.core.util.select
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.state.SessionRecord
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.Optional
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientTableTest_getAndPossiblyMerge {
@Before
fun setup() {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
}
@Test
fun allSimpleTests() {
test("no match, e164-only") {
process(E164_A, null, null)
expect(E164_A, null, null)
}
test("no match, e164 and pni") {
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
}
test("no match, aci-only") {
process(null, null, ACI_A)
expect(null, null, ACI_A)
}
test("no match, e164 and aci") {
process(E164_A, null, ACI_A)
expect(E164_A, null, ACI_A)
}
test("no match, no data", exception = java.lang.IllegalArgumentException::class.java) {
process(null, null, null)
}
test("no match, all fields") {
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("full match") {
given(E164_A, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("e164 matches, all fields provided") {
given(E164_A, null, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("e164 matches, e164 and aci provided") {
given(E164_A, null, null)
process(E164_A, null, ACI_A)
expect(E164_A, null, ACI_A)
}
test("e164 matches, all provided, different aci") {
given(E164_A, null, ACI_B)
process(E164_A, PNI_A, ACI_A)
expect(null, null, ACI_B)
expect(E164_A, PNI_A, ACI_A)
}
test("e164 matches, e164 and aci provided, different aci") {
given(E164_A, null, ACI_A)
process(E164_A, null, ACI_B)
expect(null, null, ACI_A)
expect(E164_A, null, ACI_B)
}
test("e164 and pni matches, all provided, new aci") {
given(E164_A, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("e164 and aci matches, all provided, new pni") {
given(E164_A, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("pni matches, all provided, new e164 and aci") {
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("pni and aci matches, all provided, new e164") {
given(null, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("e164 and aci matches, e164 and aci provided, nothing new") {
given(E164_A, null, ACI_A)
process(E164_A, null, ACI_A)
expect(E164_A, null, ACI_A)
}
test("aci matches, all provided, new e164 and pni") {
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("aci matches, e164 and aci provided") {
given(null, null, ACI_A)
process(E164_A, null, ACI_A)
expect(E164_A, null, ACI_A)
}
test("aci matches, local user, changeSelf=false") {
given(E164_SELF, PNI_SELF, ACI_SELF)
process(E164_SELF, null, ACI_B)
expect(E164_SELF, PNI_SELF, ACI_SELF)
expect(null, null, ACI_B)
}
test("e164 matches, e164 and pni provided, pni changes, no pni session") {
given(E164_A, PNI_B, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
}
test("e164 and pni matches, all provided, no existing session") {
given(E164_A, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("pni matches, all provided, no existing session") {
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
// This test, I could go either way. We decide to change the E164 on the existing row rather than create a new one.
// But it's an "unstable E164->PNI mapping" case, which we don't expect, so as long as there's a user-visible impact that should be fine.
test("pni matches, no existing pni session, changes number") {
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
// This test, I could go either way. We decide to change the E164 on the existing row rather than create a new one.
// But it's an "unstable E164->PNI mapping" case, which we don't expect, so as long as there's a user-visible impact that should be fine.
test("pni and aci matches, change number") {
given(E164_B, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
test("aci matches, all provided, change number") {
given(E164_B, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
test("aci matches, e164 and aci provided, change number") {
given(E164_B, null, ACI_A)
process(E164_A, null, ACI_A)
expect(E164_A, null, ACI_A)
expectChangeNumberEvent()
}
test("steal, e164+pni & e164+pni, no aci provided, no sessions") {
given(E164_A, PNI_B, null)
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expect(E164_B, null, null)
}
test("steal, e164+pni & aci, e164 record has separate e164") {
given(E164_B, PNI_A, null)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_B, null, null)
expect(E164_A, PNI_A, ACI_A)
}
test("steal, e164+aci & e164+aci, change number") {
given(E164_B, null, ACI_A)
given(E164_A, null, ACI_B)
process(E164_A, null, ACI_A)
expect(E164_A, null, ACI_A)
expect(null, null, ACI_B)
expectChangeNumberEvent()
}
test("merge, e164 & pni & aci, all provided") {
given(E164_A, null, null)
given(null, PNI_A, null)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
test("merge, e164 & pni, no aci provided") {
given(E164_A, null, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expectDeleted()
}
test("merge, e164 & pni, aci provided but no aci record") {
given(E164_A, null, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectDeleted()
}
test("merge, e164 & pni+e164, no aci provided") {
given(E164_A, null, null)
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expect(E164_B, null, null)
}
test("merge, e164+pni & pni, no aci provided") {
given(E164_A, PNI_B, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expectDeleted()
}
test("merge, e164+pni & aci") {
given(E164_A, PNI_A, null)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
test("merge, e164+pni & e164+pni+aci, change number") {
given(E164_A, PNI_A, null)
given(E164_B, PNI_B, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
test("merge, e164+pni & e164+aci, change number") {
given(E164_A, PNI_A, null)
given(E164_B, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
}
test("merge, e164 & aci") {
given(E164_A, null, null)
given(null, null, ACI_A)
process(E164_A, null, ACI_A)
expectDeleted()
expect(E164_A, null, ACI_A)
}
test("merge, e164 & e164+aci, change number") {
given(E164_A, null, null)
given(E164_B, null, ACI_A)
process(E164_A, null, ACI_A)
expectDeleted()
expect(E164_A, null, ACI_A)
expectChangeNumberEvent()
}
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
given(E164_SELF, null, ACI_SELF)
given(null, null, ACI_A)
process(E164_SELF, null, ACI_A)
expect(E164_SELF, null, ACI_SELF)
expect(null, null, ACI_A)
}
test("local user, e164 and aci provided, changeSelf=false, leave e164 alone") {
given(E164_SELF, null, ACI_SELF)
process(E164_A, null, ACI_SELF)
expect(E164_SELF, null, ACI_SELF)
}
test("local user, e164 and aci provided, changeSelf=true, change e164") {
given(E164_SELF, null, ACI_SELF)
process(E164_A, null, ACI_SELF, changeSelf = true)
expect(E164_A, null, ACI_SELF)
}
}
/**
* Somewhat exhaustive test of verifying all the data that gets merged.
*/
@Test
fun getAndPossiblyMerge_merge_general() {
// Setup
val recipientIdAci: RecipientId = SignalDatabase.recipients.getOrInsertFromServiceId(ACI_A)
val recipientIdE164: RecipientId = SignalDatabase.recipients.getOrInsertFromE164(E164_A)
val recipientIdAciB: RecipientId = SignalDatabase.recipients.getOrInsertFromServiceId(ACI_B)
val smsId1: Long = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
val smsId2: Long = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
val smsId3: Long = SignalDatabase.messages.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
val mmsId1: Long = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
val mmsId2: Long = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
val mmsId3: Long = SignalDatabase.messages.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
val threadIdAci: Long = SignalDatabase.threads.getThreadIdFor(recipientIdAci)!!
val threadIdE164: Long = SignalDatabase.threads.getThreadIdFor(recipientIdE164)!!
Assert.assertNotEquals(threadIdAci, threadIdE164)
SignalDatabase.mentions.insert(threadIdAci, mmsId1, listOf(Mention(recipientIdE164, 0, 1)))
SignalDatabase.mentions.insert(threadIdE164, mmsId2, listOf(Mention(recipientIdAci, 0, 1)))
SignalDatabase.groupReceipts.insert(listOf(recipientIdAci, recipientIdE164), mmsId1, 0, 3)
val identityKeyAci: IdentityKey = identityKey(1)
val identityKeyE164: IdentityKey = identityKey(2)
SignalDatabase.identities.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityTable.VerifiedStatus.VERIFIED, false, 0, false)
SignalDatabase.identities.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityTable.VerifiedStatus.VERIFIED, false, 0, false)
SignalDatabase.sessions.store(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
SignalDatabase.reactions.addReaction(MessageId(smsId1), ReactionRecord("a", recipientIdAci, 1, 1))
SignalDatabase.reactions.addReaction(MessageId(mmsId1), ReactionRecord("b", recipientIdE164, 1, 1))
val profile1: NotificationProfile = notificationProfile(name = "Test")
val profile2: NotificationProfile = notificationProfile(name = "Test2")
SignalDatabase.notificationProfiles.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdAci)
SignalDatabase.notificationProfiles.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdE164)
SignalDatabase.notificationProfiles.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164)
SignalDatabase.notificationProfiles.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB)
val distributionListId: DistributionListId = SignalDatabase.distributionLists.createList("testlist", listOf(recipientIdE164, recipientIdAciB))!!
// Merge
val retrievedId: RecipientId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedThreadId: Long = SignalDatabase.threads.getThreadIdFor(retrievedId)!!
assertEquals(recipientIdAci, retrievedId)
// Recipient validation
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireServiceId())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(recipientIdE164)
assertEquals(retrievedId, existingE164Recipient.id)
// Thread validation
assertEquals(threadIdAci, retrievedThreadId)
Assert.assertNull(SignalDatabase.threads.getThreadIdFor(recipientIdE164))
Assert.assertNull(SignalDatabase.threads.getThreadRecord(threadIdE164))
// SMS validation
val sms1: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId1)!!
val sms2: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId2)!!
val sms3: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId3)!!
assertEquals(retrievedId, sms1.recipient.id)
assertEquals(retrievedId, sms2.recipient.id)
assertEquals(retrievedId, sms3.recipient.id)
assertEquals(retrievedThreadId, sms1.threadId)
assertEquals(retrievedThreadId, sms2.threadId)
assertEquals(retrievedThreadId, sms3.threadId)
// MMS validation
val mms1: MessageRecord = SignalDatabase.messages.getMessageRecord(mmsId1)!!
val mms2: MessageRecord = SignalDatabase.messages.getMessageRecord(mmsId2)!!
val mms3: MessageRecord = SignalDatabase.messages.getMessageRecord(mmsId3)!!
assertEquals(retrievedId, mms1.recipient.id)
assertEquals(retrievedId, mms2.recipient.id)
assertEquals(retrievedId, mms3.recipient.id)
assertEquals(retrievedThreadId, mms1.threadId)
assertEquals(retrievedThreadId, mms2.threadId)
assertEquals(retrievedThreadId, mms3.threadId)
// Mention validation
val mention1: MentionModel = getMention(mmsId1)
assertEquals(retrievedId, mention1.recipientId)
assertEquals(retrievedThreadId, mention1.threadId)
val mention2: MentionModel = getMention(mmsId2)
assertEquals(retrievedId, mention2.recipientId)
assertEquals(retrievedThreadId, mention2.threadId)
// Group receipt validation
val groupReceipts: List<GroupReceiptTable.GroupReceiptInfo> = SignalDatabase.groupReceipts.getGroupReceiptInfo(mmsId1)
assertEquals(retrievedId, groupReceipts[0].recipientId)
assertEquals(retrievedId, groupReceipts[1].recipientId)
// Identity validation
assertEquals(identityKeyAci, SignalDatabase.identities.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
Assert.assertNull(SignalDatabase.identities.getIdentityStoreRecord(E164_A))
// Session validation
Assert.assertNotNull(SignalDatabase.sessions.load(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1)))
// Reaction validation
val reactionsSms: List<ReactionRecord> = SignalDatabase.reactions.getReactions(MessageId(smsId1))
val reactionsMms: List<ReactionRecord> = SignalDatabase.reactions.getReactions(MessageId(mmsId1))
assertEquals(1, reactionsSms.size)
assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0])
assertEquals(1, reactionsMms.size)
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
// Notification Profile validation
val updatedProfile1: NotificationProfile = SignalDatabase.notificationProfiles.getProfile(profile1.id)!!
val updatedProfile2: NotificationProfile = SignalDatabase.notificationProfiles.getProfile(profile2.id)!!
MatcherAssert.assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
MatcherAssert.assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
// Distribution List validation
val updatedList: DistributionListRecord = SignalDatabase.distributionLists.getList(distributionListId)!!
MatcherAssert.assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
}
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingTextMessage {
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
}
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.empty()): IncomingMediaMessage {
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.empty(), false, false)
}
private fun identityKey(value: Byte): IdentityKey {
val bytes = ByteArray(33)
bytes[0] = 0x05
bytes[1] = value
return IdentityKey(bytes)
}
private fun notificationProfile(name: String): NotificationProfile {
return (SignalDatabase.notificationProfiles.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
}
private fun getMention(messageId: Long): MentionModel {
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionTable.TABLE_NAME} WHERE ${MentionTable.MESSAGE_ID} = $messageId").use { cursor ->
cursor.moveToFirst()
return MentionModel(
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionTable.RECIPIENT_ID)),
threadId = CursorUtil.requireLong(cursor, MentionTable.THREAD_ID)
)
}
}
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
data class MentionModel(
val recipientId: RecipientId,
val threadId: Long
)
/**
* Baby DSL for making tests readable.
*/
private fun test(name: String, init: TestCase.() -> Unit): TestCase {
// Weird issue with generics wouldn't let me make the exception an arg with default value -- had to do an actual overload
return test(name, null as Class<Throwable>?, init)
}
/**
* Baby DSL for making tests readable.
*/
private fun <E> test(name: String, exception: Class<E>?, init: TestCase.() -> Unit): TestCase where E : Throwable {
val test = TestCase()
try {
test.init()
if (exception != null) {
throw java.lang.AssertionError("Expected $exception, but none was thrown.")
}
if (!test.changeNumberExpected) {
test.expectNoChangeNumberEvent()
}
} catch (e: Throwable) {
if (e.javaClass != exception) {
val error = java.lang.AssertionError("[$name] ${e.message}")
error.stackTrace = e.stackTrace
throw error
}
}
return test
}
private inner class TestCase {
private val generatedIds: LinkedHashSet<RecipientId> = LinkedHashSet()
private var expectCount = 0
private lateinit var outputRecipientId: RecipientId
var changeNumberExpected = false
init {
// Need to delete these first to prevent foreign key crash
SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list")
SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list_member")
SqlUtil.getAllTables(SignalDatabase.rawDatabase)
.filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB
.sorted()
.forEach { table ->
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
}
ApplicationDependencies.getRecipientCache().clear()
RecipientId.clearCache()
}
fun given(
e164: String?,
pni: PNI?,
aci: ACI?,
createThread: Boolean = true,
sms: List<String> = emptyList(),
mms: List<String> = emptyList()
) {
val id = insert(e164, pni, aci)
generatedIds += id
if (createThread) {
// Create a thread and throw a dummy message in it so it doesn't get automatically deleted
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(id))
SignalDatabase.messages.insertMessageInbox(IncomingEncryptedMessage(IncomingTextMessage(id, 1, 0, 0, 0, "", Optional.empty(), 0, false, ""), ""))
}
}
fun process(e164: String?, pni: PNI?, aci: ACI?, changeSelf: Boolean = false) {
outputRecipientId = SignalDatabase.recipients.getAndPossiblyMerge(serviceId = aci ?: pni, pni = pni, e164 = e164, pniVerified = false, changeSelf = changeSelf)
generatedIds += outputRecipientId
}
fun expect(e164: String?, pni: PNI?, aci: ACI?) {
expect(generatedIds.elementAt(expectCount++), e164, pni, aci)
}
fun expect(id: RecipientId, e164: String?, pni: PNI?, aci: ACI?) {
val recipient = Recipient.resolved(id)
val expected = RecipientTuple(
e164 = e164,
pni = pni,
serviceId = aci ?: pni
)
val actual = RecipientTuple(
e164 = recipient.e164.orElse(null),
pni = recipient.pni.orElse(null),
serviceId = recipient.serviceId.orElse(null)
)
assertEquals(expected, actual)
}
fun expectDeleted() {
expectDeleted(generatedIds.elementAt(expectCount++))
}
fun expectDeleted(id: RecipientId) {
SignalDatabase.rawDatabase
.select("1")
.from(RecipientTable.TABLE_NAME)
.where("${RecipientTable.ID} = ?", id)
.run()
.use { !it.moveToFirst() }
}
fun expectChangeNumberEvent() {
assertEquals(1, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
changeNumberExpected = true
}
fun expectNoChangeNumberEvent() {
assertEquals(0, SignalDatabase.messages.getChangeNumberMessageCount(outputRecipientId))
changeNumberExpected = false
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val serviceIdString: String? = (aci ?: pni)?.toString()
val pniString: String? = pni?.toString()
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientTable.TABLE_NAME,
null,
contentValuesOf(
RecipientTable.PHONE to e164,
RecipientTable.SERVICE_ID to serviceIdString,
RecipientTable.PNI_COLUMN to pniString,
RecipientTable.REGISTERED to RecipientTable.RegisteredState.REGISTERED.id
)
)
assertTrue("Failed to insert! E164: $e164, ServiceId: $serviceIdString, PNI: $pniString", id > 0)
return RecipientId.from(id)
}
}
data class RecipientTuple(
val e164: String?,
val pni: PNI?,
val serviceId: ServiceId?
) {
/**
* The intent here is to give nice diffs with the name of the constants rather than the values.
*/
override fun toString(): String {
return "(${e164.e164String()}, ${pni.pniString()}, ${serviceId.serviceIdString()})"
}
private fun String?.e164String(): String {
return this?.let {
when (it) {
E164_A -> "E164_A"
E164_B -> "E164_B"
else -> it
}
} ?: "null"
}
private fun PNI?.pniString(): String {
return this?.let {
when (it) {
PNI_A -> "PNI_A"
PNI_B -> "PNI_B"
PNI_SELF -> "PNI_SELF"
else -> it.toString()
}
} ?: "null"
}
private fun ServiceId?.serviceIdString(): String {
return this?.let {
when (it) {
PNI_A -> "PNI_A"
PNI_B -> "PNI_B"
PNI_SELF -> "PNI_SELF"
ACI_A -> "ACI_A"
ACI_B -> "ACI_B"
ACI_SELF -> "ACI_SELF"
else -> it.toString()
}
} ?: "null"
}
}
companion object {
val ACI_A = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("bbbb0000-0b60-4a68-9cd9-ed2f8453f9ed"))
val ACI_SELF = ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
val PNI_A = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("bbbb1111-cd55-40bf-adda-c35a85375533"))
val PNI_SELF = PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
const val E164_A = "+12222222222"
const val E164_B = "+13333333333"
const val E164_SELF = "+10000000000"
}
}

View File

@@ -0,0 +1,842 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.Util
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.IllegalStateException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientTableTest_processPnpTupleToChangeSet {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
private lateinit var db: RecipientTable
@Before
fun setup() {
db = SignalDatabase.recipients
}
@Test
fun noMatch_e164Only() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, null, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, null, null)
),
changeSet
)
}
@Test
fun noMatch_e164AndPni() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, null)
),
changeSet
)
}
@Test
fun noMatch_aciOnly() {
val changeSet = db.processPnpTupleToChangeSet(null, null, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(null, null, ACI_A)
),
changeSet
)
}
@Test(expected = IllegalStateException::class)
fun noMatch_noData() {
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
}
@Test
fun noMatch_allFields() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, ACI_A)
),
changeSet
)
}
@Test
fun fullMatch() {
val result = applyAndAssert(
Input(E164_A, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id)
),
result.changeSet
)
}
@Test
fun onlyE164Matches() {
val result = applyAndAssert(
Input(E164_A, null, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null, pniSession = true),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun e164AndAciMatches() {
val result = applyAndAssert(
Input(E164_A, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(null, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(null, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches() {
val result = applyAndAssert(
Input(null, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches() {
val result = applyAndAssert(
Input(null, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
Input(null, null, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.thirdId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.Merge(
primaryId = result.thirdId,
secondaryId = result.firstId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.SetAci(
recipientId = result.firstId,
aci = ACI_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniAndE164_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(E164_B, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(
recipientId = result.firstId,
pni = PNI_A
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(E164_B, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_sessionsExist() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null, pniSession = true),
Input(E164_B, PNI_A, null, pniSession = true),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.secondId),
PnpOperation.SessionSwitchoverInsert(result.firstId)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(null, 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.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
val result = applyAndAssert(
listOf(
Input(E164_B, PNI_A, null),
Input(null, 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,
),
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,
),
PnpOperation.SetE164(
recipientId = result.secondId,
e164 = E164_A,
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_C,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, PNI_B, 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.secondId),
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164Aci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, 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.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientTable.TABLE_NAME,
null,
contentValuesOf(
RecipientTable.PHONE to e164,
RecipientTable.SERVICE_ID to (aci ?: pni)?.toString(),
RecipientTable.PNI_COLUMN to pni?.toString(),
RecipientTable.REGISTERED to RecipientTable.RegisteredState.REGISTERED.id
)
)
return RecipientId.from(id)
}
private fun insertMockSessionFor(account: ServiceId, address: ServiceId) {
SignalDatabase.rawDatabase.insert(
SessionTable.TABLE_NAME, null,
contentValuesOf(
SessionTable.ACCOUNT_ID to account.toString(),
SessionTable.ADDRESS to address.toString(),
SessionTable.DEVICE to 1,
SessionTable.RECORD to Util.getSecretBytes(32)
)
)
}
data class Input(val e164: String?, val pni: PNI?, val aci: ACI?, val pniSession: Boolean = false, val aciSession: Boolean = false)
data class Update(val e164: String?, val pni: PNI?, val aci: ACI?, val pniVerified: Boolean = false)
data class Output(val e164: String?, val pni: PNI?, val aci: ACI?)
data class PnpMatchResult(val ids: List<RecipientId>, val changeSet: PnpChangeSet) {
val id
get() = if (ids.size == 1) {
ids[0]
} else {
throw IllegalStateException("There are multiple IDs, but you assumed 1!")
}
val firstId
get() = ids[0]
val secondId
get() = ids[1]
val thirdId
get() = ids[2]
}
private fun applyAndAssert(input: Input, update: Update, output: Output): PnpMatchResult {
return applyAndAssert(listOf(input), update, output)
}
/**
* Helper method that will call insert your recipients, call [RecipientTable.processPnpTupleToChangeSet] with your params,
* and then verify your output matches what you expect.
*
* It results the inserted ID's and changeset for additional verification.
*
* But basically this is here to make the tests more readable. It gives you a clear list of:
* - input
* - update
* - output
*
* that you can spot check easily.
*
* Important: The output will only include records that contain fields from the input. That means
* for:
*
* Input: E164_B, PNI_A, null
* Update: E164_A, PNI_A, null
*
* You will get:
* Output: E164_A, PNI_A, null
*
* Even though there was an update that will also result in the row (E164_B, null, null)
*/
private fun applyAndAssert(input: List<Input>, update: Update, output: Output): PnpMatchResult {
val ids = input.map { insert(it.e164, it.pni, it.aci) }
input
.filter { it.pniSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.pni!!) }
input
.filter { it.aciSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.aci!!) }
val byE164 = update.e164?.let { db.getByE164(it).orElse(null) }
val byPniSid = update.pni?.let { db.getByServiceId(it).orElse(null) }
val byAciSid = update.aci?.let { db.getByServiceId(it).orElse(null) }
val data = PnpDataSet(
e164 = update.e164,
pni = update.pni,
aci = update.aci,
byE164 = byE164,
byPniSid = byPniSid,
byPniOnly = update.pni?.let { db.getByPni(it).orElse(null) },
byAciSid = byAciSid,
e164Record = byE164?.let { db.getRecord(it) },
pniSidRecord = byPniSid?.let { db.getRecord(it) },
aciSidRecord = byAciSid?.let { db.getRecord(it) }
)
val changeSet = db.processPnpTupleToChangeSet(update.e164, update.pni, update.aci, pniVerified = update.pniVerified)
val finalData = data.perform(changeSet.operations)
val finalRecords = setOfNotNull(finalData.e164Record, finalData.pniSidRecord, finalData.aciSidRecord)
assertEquals("There's still multiple records in the resulting record set! $finalRecords", 1, finalRecords.size)
finalRecords.firstOrNull { record -> record.e164 == output.e164 && record.pni == output.pni && record.serviceId == (output.aci ?: output.pni) }
?: throw AssertionError("Expected output was not found in the result set! Expected: $output")
return PnpMatchResult(
ids = ids,
changeSet = changeSet
)
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
const val E164_C = "+14441234567"
}
}

View File

@@ -122,4 +122,62 @@ class SQLiteDatabaseTest {
assertTrue(hasRun1.get())
assertFalse(hasRun2.get())
}
@Test
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransaction() {
val hasRun = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction {
try {
db.beginTransaction()
hasRun.set(true)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
assertFalse(hasRun.get())
db.setTransactionSuccessful()
db.endTransaction()
assertTrue(hasRun.get())
}
@Test
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransactionAndRunPostNested() {
val hasRun1 = AtomicBoolean(false)
val hasRun2 = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction {
db.beginTransaction()
db.runPostSuccessfulTransaction {
assertTrue(hasRun1.get())
assertFalse(hasRun2.get())
hasRun2.set(true)
}
assertFalse(hasRun1.get())
hasRun1.set(true)
assertFalse(hasRun2.get())
db.setTransactionSuccessful()
db.endTransaction()
}
assertFalse(hasRun1.get())
assertFalse(hasRun2.get())
db.setTransactionSuccessful()
db.endTransaction()
assertTrue(hasRun1.get())
assertTrue(hasRun2.get())
}
}

View File

@@ -31,8 +31,8 @@ import java.util.UUID
@RunWith(AndroidJUnit4::class)
class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
private lateinit var recipients: RecipientDatabase
private lateinit var sms: SmsDatabase
private lateinit var recipients: RecipientTable
private lateinit var sms: MessageTable
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@@ -45,7 +45,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
@Before
fun setUp() {
recipients = SignalDatabase.recipients
sms = SignalDatabase.sms
sms = SignalDatabase.messages
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
@@ -163,7 +163,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
*/
@Test
fun previousJoinRequestCollapse() {
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
@@ -197,7 +197,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
fun previousJoinThenTextCollapse() {
val secondLatestMessage = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get()
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
@@ -231,7 +231,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
*/
@Test
fun previousCollapseAndJoinRequestDoubleCollapse() {
val secondLatestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
val secondLatestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
@@ -243,7 +243,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
)
).get()
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
val latestMessage: MessageTable.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {

View File

@@ -10,6 +10,7 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -22,7 +23,7 @@ import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class StorySendsDatabaseTest {
class StorySendTableTest {
private val distributionId1 = DistributionId.from(UUID.randomUUID())
private val distributionId2 = DistributionId.from(UUID.randomUUID())
@@ -45,7 +46,7 @@ class StorySendsDatabaseTest {
private var messageId2: Long = 0
private var messageId3: Long = 0
private lateinit var storySends: StorySendsDatabase
private lateinit var storySends: StorySendTable
@Before
fun setup() {
@@ -183,7 +184,7 @@ class StorySendsDatabaseTest {
@Test
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
SignalDatabase.messages.markAsRemoteDelete(messageId1)
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
@@ -280,19 +281,20 @@ class StorySendsDatabaseTest {
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNonNullResult() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
SignalDatabase.messages.markAsRemoteDelete(messageId1)
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)!!
assertNotNull(manifest)
}
/*
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetRecipientIdsForManifestUpdate_thenIExpectOnlyRecipientsWithStory2() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId1, recipients11to20, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
SignalDatabase.messages.markAsRemoteDelete(messageId1)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
@@ -304,7 +306,7 @@ class StorySendsDatabaseTest {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
SignalDatabase.messages.markAsRemoteDelete(messageId1)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
@@ -317,14 +319,14 @@ class StorySendsDatabaseTest {
fun givenTwoStoriesAndTheOneThatAllowedRepliesIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectAllowRepliesToBeTrue() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.markAsRemoteDelete(messageId2)
SignalDatabase.messages.markAsRemoteDelete(messageId2)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
assertTrue(results.entries.all { it.allowedToReply })
}
*/
@Test
fun givenEmptyManifest_whenIApplyRemoteManifest_thenNothingChanges() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
@@ -354,8 +356,8 @@ class StorySendsDatabaseTest {
assertEquals(expected, result)
}
@Test
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeMarkedRemoteDeleted() {
@Test(expected = NoSuchMessageException::class)
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeDeleted() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
@@ -375,7 +377,8 @@ class StorySendsDatabaseTest {
storySends.applySentStoryManifest(remote, 200)
assertTrue(SignalDatabase.mms.getMessageRecord(messageId5).isRemoteDelete)
SignalDatabase.messages.getMessageRecord(messageId5)
fail("Expected messageId5 to no longer exist.")
}
@Test
@@ -399,7 +402,7 @@ class StorySendsDatabaseTest {
storySends.applySentStoryManifest(remote, 200)
assertFalse(SignalDatabase.mms.getMessageRecord(messageId4).isRemoteDelete)
assertFalse(SignalDatabase.messages.getMessageRecord(messageId4).isRemoteDelete)
}
@Test

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.database
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@Suppress("ClassName")
class ThreadTableTest_pinned {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule()
private lateinit var recipient: Recipient
@Before
fun setUp() {
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIDoNotDeleteOrUnpinTheThread() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.messages.deleteMessage(messageId)
// THEN
val pinned = SignalDatabase.threads.getPinnedThreadIds()
assertTrue(threadId in pinned)
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectTheThreadInUnarchivedCount() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.messages.deleteMessage(messageId)
// THEN
val unarchivedCount = SignalDatabase.threads.getUnarchivedConversationListCount(ConversationFilter.OFF)
assertEquals(1, unarchivedCount)
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectPinnedThreadInUnarchivedList() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.messages.deleteMessage(messageId)
// THEN
SignalDatabase.threads.getUnarchivedConversationList(ConversationFilter.OFF, true, 0, 1).use {
it.moveToFirst()
assertEquals(threadId, CursorUtil.requireLong(it, ThreadTable.ID))
}
}
}

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertFalse
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class ThreadTableTest_recents {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule()
private lateinit var recipient: Recipient
@Before
fun setUp() {
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
}
@Test
fun givenARecentRecipient_whenIBlockAndGetRecents_thenIDoNotExpectToSeeThatRecipient() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.update(threadId, true)
// WHEN
SignalDatabase.recipients.setBlocked(recipient.id, true)
val results: MutableList<RecipientId> = SignalDatabase.threads.getRecentConversationList(10, false, false, false, false, false, false).use { cursor ->
val ids = mutableListOf<RecipientId>()
while (cursor.moveToNext()) {
ids.add(RecipientId.from(CursorUtil.requireLong(cursor, ThreadTable.RECIPIENT_ID)))
}
ids
}
// THEN
assertFalse(recipient.id in results)
}
}

View File

@@ -0,0 +1,44 @@
package org.thoughtcrime.securesms.database
import android.net.Uri
import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.stickers.StickerLocator
object UriAttachmentBuilder {
fun build(
id: Long,
uri: Uri = Uri.parse("content://$id"),
contentType: String,
transferState: Int = AttachmentTable.TRANSFER_PROGRESS_PENDING,
size: Long = 0L,
fileName: String = "file$id",
voiceNote: Boolean = false,
borderless: Boolean = false,
videoGif: Boolean = false,
quote: Boolean = false,
caption: String? = null,
stickerLocator: StickerLocator? = null,
blurHash: BlurHash? = null,
audioHash: AudioHash? = null,
transformProperties: AttachmentTable.TransformProperties? = null
): UriAttachment {
return UriAttachment(
uri,
contentType,
transferState,
size,
fileName,
voiceNote,
borderless,
videoGif,
quote,
caption,
stickerLocator,
blurHash,
audioHash,
transformProperties
)
}
}

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.DistributionListTables
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(
DistributionListTables.LIST_TABLE_NAME,
contentValuesOf(
DistributionListTables.DISTRIBUTION_ID to serializedId
),
"_id = ?",
SqlUtil.buildArgs(DistributionListId.MY_STORY)
)
}
private fun deleteMyStory() {
SignalDatabase.rawDatabase.delete(
DistributionListTables.LIST_TABLE_NAME,
"_id = ?",
SqlUtil.buildArgs(DistributionListId.MY_STORY)
)
}
private fun assertValidMyStoryExists() {
SignalDatabase.rawDatabase.query(
DistributionListTables.LIST_TABLE_NAME,
SqlUtil.COUNT,
"_id = ? AND ${DistributionListTables.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,118 @@
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.recipients.LiveRecipientCache
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
private val recipientCache: LiveRecipientCache
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()
recipientCache = LiveRecipientCache(application) { r -> r.run() }
}
override fun provideSignalServiceNetworkAccess(): SignalServiceNetworkAccess {
return serviceNetworkAccessMock
}
override fun provideKeyBackupService(signalServiceAccountManager: SignalServiceAccountManager, keyStore: KeyStore, enclave: KbsEnclave): KeyBackupService {
return keyBackupService
}
override fun provideRecipientCache(): LiveRecipientCache {
return recipientCache
}
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,62 @@
package org.thoughtcrime.securesms.messages
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import org.junit.Rule
import org.thoughtcrime.securesms.messages.MessageContentProcessor.ExceptionMetadata
import org.thoughtcrime.securesms.messages.MessageContentProcessor.MessageState
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.TestProtos
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
abstract class MessageContentProcessorTest {
@get:Rule
val harness = SignalActivityRule()
protected fun MessageContentProcessor.doProcess(
messageState: MessageState = MessageState.DECRYPTED_OK,
content: SignalServiceContent,
exceptionMetadata: ExceptionMetadata = ExceptionMetadata("sender", 1),
timestamp: Long = 100L,
smsMessageId: Long = -1L
) {
process(messageState, content, exceptionMetadata, timestamp, smsMessageId)
}
protected fun createNormalContentTestSubject(): MessageContentProcessor {
val context = ApplicationProvider.getApplicationContext<Application>()
return MessageContentProcessor.forNormalContent(context)
}
/**
* Creates a valid ServiceContentProto with a data message which can be built via
* `injectDataMessage`. This function is intended to be built on-top of for more
* specific scenario in subclasses.
*
* Example can be seen in __handleStoryMessageTest
*/
protected fun createServiceContentWithDataMessage(
messageSender: Recipient = Recipient.resolved(harness.others.first()),
injectDataMessage: SignalServiceProtos.DataMessage.Builder.() -> Unit
): SignalServiceContentProto {
return TestProtos.build {
serviceContent(
localAddress = address(uuid = harness.self.requireServiceId().uuid()).build(),
metadata = metadata(
address = address(uuid = messageSender.requireServiceId().uuid()).build()
).build()
).apply {
content = content().apply {
dataMessage = dataMessage().apply {
injectDataMessage()
}.build()
}.build()
}.build()
}
}
}

View File

@@ -0,0 +1,181 @@
package org.thoughtcrime.securesms.messages
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.signal.core.util.requireLong
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MmsHelper
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.ParentStoryId
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.TestProtos
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
import kotlin.random.Random
@Suppress("ClassName")
class MessageContentProcessor__handleStoryMessageTest : MessageContentProcessorTest() {
@Before
fun setUp() {
SignalDatabase.messages.deleteAllThreads()
}
@After
fun tearDown() {
SignalDatabase.messages.deleteAllThreads()
}
@Test
fun givenContentWithADirectStoryReplyWhenIProcessThenIInsertAReplyInTheCorrectThread() {
val sender = Recipient.resolved(harness.others.first())
val senderThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(sender)
val myStory = Recipient.resolved(SignalDatabase.distributionLists.getRecipientId(DistributionListId.MY_STORY)!!)
val myStoryThread = SignalDatabase.threads.getOrCreateThreadIdFor(myStory)
val expectedSentTime = 200L
val storyMessageId = MmsHelper.insert(
sentTimeMillis = expectedSentTime,
recipient = myStory,
storyType = StoryType.STORY_WITH_REPLIES,
threadId = myStoryThread
)
SignalDatabase.storySends.insert(
messageId = storyMessageId,
recipientIds = listOf(sender.id),
sentTimestamp = expectedSentTime,
allowsReplies = true,
distributionId = DistributionId.MY_STORY
)
val expectedBody = "Hello!"
val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext(
messageSender = sender,
storyAuthor = harness.self,
storySentTimestamp = expectedSentTime
) {
body = expectedBody
}
runTestWithContent(contentProto = storyContent)
val replyId = SignalDatabase.messages.getConversation(senderThreadId, 0, 1).use {
it.moveToFirst()
it.requireLong(MessageTable.ID)
}
val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord
assertEquals(ParentStoryId.DirectReply(storyMessageId).serialize(), replyRecord.parentStoryId!!.serialize())
assertEquals(expectedBody, replyRecord.body)
SignalDatabase.messages.deleteAllThreads()
}
@Test
fun givenContentWithAGroupStoryReplyWhenIProcessThenIInsertAReplyToTheCorrectStory() {
val sender = Recipient.resolved(harness.others[0])
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.newBuilder()
.addAllMembers(
listOf(
DecryptedMember.newBuilder()
.setUuid(harness.self.requireServiceId().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build(),
DecryptedMember.newBuilder()
.setUuid(sender.requireServiceId().toByteString())
.setJoinedAtRevision(0)
.setRole(Member.Role.DEFAULT)
.build()
)
)
.setRevision(0)
.build()
val group = SignalDatabase.groups.create(
groupMasterKey,
decryptedGroupState
)
val groupRecipient = Recipient.externalGroupExact(group)
val threadForGroup = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
val insertResult = MmsHelper.insert(
message = IncomingMediaMessage(
from = sender.id,
sentTimeMillis = 100L,
serverTimeMillis = 101L,
receivedTimeMillis = 102L,
storyType = StoryType.STORY_WITH_REPLIES
),
threadId = threadForGroup
)
val expectedBody = "Hello, World!"
val storyContent: SignalServiceContentProto = createServiceContentWithStoryContext(
messageSender = sender,
storyAuthor = sender,
storySentTimestamp = 100L
) {
groupV2 = TestProtos.build { groupContextV2(masterKeyBytes = groupMasterKey.serialize()).build() }
body = expectedBody
}
runTestWithContent(storyContent)
val replyId = SignalDatabase.messages.getStoryReplies(insertResult.get().messageId).use { cursor ->
assertEquals(1, cursor.count)
cursor.moveToFirst()
cursor.requireLong(MessageTable.ID)
}
val replyRecord = SignalDatabase.messages.getMessageRecord(replyId) as MediaMmsMessageRecord
assertEquals(ParentStoryId.GroupReply(insertResult.get().messageId).serialize(), replyRecord.parentStoryId?.serialize())
assertEquals(threadForGroup, replyRecord.threadId)
assertEquals(expectedBody, replyRecord.body)
SignalDatabase.messages.deleteGroupStoryReplies(insertResult.get().messageId)
SignalDatabase.messages.deleteAllThreads()
}
/**
* Creates a ServiceContent proto with a StoryContext, and then
* uses `injectDataMessage` to fill in the data message object.
*/
private fun createServiceContentWithStoryContext(
messageSender: Recipient,
storyAuthor: Recipient,
storySentTimestamp: Long,
injectDataMessage: DataMessage.Builder.() -> Unit
): SignalServiceContentProto {
return createServiceContentWithDataMessage(messageSender) {
storyContext = TestProtos.build {
storyContext(
sentTimestamp = storySentTimestamp,
authorUuid = storyAuthor.requireServiceId().toString()
).build()
}
injectDataMessage()
}
}
private fun runTestWithContent(contentProto: SignalServiceContentProto) {
val content = SignalServiceContent.createFromProto(contentProto)
val testSubject = createNormalContentTestSubject()
testSubject.doProcess(content = content!!)
}
}

View File

@@ -0,0 +1,33 @@
package org.thoughtcrime.securesms.messages
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.thoughtcrime.securesms.database.SignalDatabase
import org.whispersystems.signalservice.api.messages.SignalServiceContent
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
@Suppress("ClassName")
class MessageContentProcessor__handleTextMessageTest : MessageContentProcessorTest() {
@Test
fun givenContentWithATextMessageWhenIProcessThenIInsertTheTextMessage() {
val testSubject: MessageContentProcessor = createNormalContentTestSubject()
val expectedBody = "Hello, World!"
val contentProto: SignalServiceContentProto = createServiceContentWithDataMessage {
body = expectedBody
}
val content = SignalServiceContent.createFromProto(contentProto)
// WHEN
testSubject.doProcess(content = content!!)
// THEN
val record = SignalDatabase.messages.getMessageRecord(1)
val threadSize = SignalDatabase.messages.getMessageCountForThread(record.threadId)
assertEquals(1, threadSize)
assertTrue(record.isSecure)
assertEquals(expectedBody, record.body)
}
}

View File

@@ -0,0 +1,141 @@
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.Ignore
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)))
}
@Ignore("Flakey espresso test.")
@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)))
}
@Ignore("Flakey espresso test.")
@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,139 @@
package org.thoughtcrime.securesms.safety
import io.reactivex.rxjava3.plugins.RxJavaPlugins
import io.reactivex.rxjava3.schedulers.TestScheduler
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
class SafetyNumberBottomSheetRepositoryTest {
@get:Rule val harness = SignalActivityRule(othersCount = 10)
private val testScheduler = TestScheduler()
private val subjectUnderTest = SafetyNumberBottomSheetRepository()
@Before
fun setUp() {
RxJavaPlugins.setInitIoSchedulerHandler { testScheduler }
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
}
@Test
fun givenIOnlyHave1to1Destinations_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
val recipients = harness.others
val destinations = harness.others.map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
val result = subjectUnderTest.getBuckets(recipients, destinations).test()
testScheduler.triggerActions()
result.assertValueAt(0) { map ->
assertMatch(map, mapOf(SafetyNumberBucket.ContactsBucket to harness.others))
}
}
@Test
fun givenIOnlyHaveASingle1to1Destination_whenIGetBuckets_thenIOnlyHaveContactsBucketContainingAllRecipients() {
// GIVEN
val recipients = harness.others
val destination = harness.others.take(1).map { ContactSearchKey.RecipientSearchKey.KnownRecipient(it) }
// WHEN
val result = subjectUnderTest.getBuckets(recipients, destination).test(1)
testScheduler.triggerActions()
// THEN
result.assertValue { map ->
assertMatch(map, mapOf(SafetyNumberBucket.ContactsBucket to harness.others.take(1)))
}
}
@Test
fun givenIHaveADistributionListDestination_whenIGetBuckets_thenIOnlyHaveDistributionListDestinationWithCorrespondingMembers() {
// GIVEN
val distributionListMembers = harness.others.take(5)
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
// WHEN
val result = subjectUnderTest.getBuckets(harness.others, listOf(destinationKey)).test(1)
testScheduler.triggerActions()
// THEN
result.assertValue { map ->
assertMatch(
map,
mapOf(
SafetyNumberBucket.DistributionListBucket(distributionList, "ListA") to harness.others.take(5)
)
)
}
}
@Test
fun givenIHaveADistributionListDestinationAndIGetBuckets_whenIRemoveFromStories_thenIOnlyHaveDistributionListDestinationWithCorrespondingMembers() {
// GIVEN
val distributionListMembers = harness.others.take(5)
val toRemove = distributionListMembers.last()
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
testScheduler.triggerActions()
// WHEN
subjectUnderTest.removeFromStories(toRemove, listOf(destinationKey)).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(3)
// THEN
testSubscriber.assertValueAt(2) { map ->
assertMatch(
map,
mapOf(
SafetyNumberBucket.DistributionListBucket(distributionList, "ListA") to distributionListMembers.dropLast(1)
)
)
}
}
@Test
fun givenIHaveADistributionListDestinationAndIGetBuckets_whenIRemoveAllFromStory_thenINoLongerHaveEntryForThatBucket() {
// GIVEN
val distributionListMembers = harness.others.take(5)
val distributionList = SignalDatabase.distributionLists.createList("ListA", distributionListMembers)!!
val destinationKey = ContactSearchKey.RecipientSearchKey.Story(SignalDatabase.distributionLists.getRecipientId(distributionList)!!)
val testSubscriber = subjectUnderTest.getBuckets(distributionListMembers, listOf(destinationKey)).test(2)
testScheduler.triggerActions()
// WHEN
subjectUnderTest.removeAllFromStory(distributionListMembers, distributionList).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(3)
// THEN
testSubscriber.assertValueAt(2) { map ->
assertMatch(map, mapOf())
}
}
private fun assertMatch(
resultMap: Map<SafetyNumberBucket, List<SafetyNumberRecipient>>,
idMap: Map<SafetyNumberBucket, List<RecipientId>>
): Boolean {
assertEquals("Result and ID Maps had different key sets", idMap.keys, resultMap.keys)
resultMap.forEach { (bucket, members) ->
assertEquals("Mismatch in Bucket $bucket", idMap[bucket], members.map { it.recipient.id })
}
return true
}
}

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

@@ -0,0 +1,136 @@
package org.thoughtcrime.securesms.testing
import android.app.Activity
import android.app.Application
import android.content.Context
import android.content.Intent
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
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.IdentityTable
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
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.RegistrationRepository
import org.thoughtcrime.securesms.registration.RegistrationUtil
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
/**
* Test rule to use that sets up the application in a mostly registered state. Enough so that most
* activities should be launchable directly.
*
* To use: `@get:Rule val harness = SignalActivityRule()`
*/
class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource() {
val application: Application = ApplicationDependencies.getApplication()
lateinit var context: Context
private set
lateinit var self: Recipient
private set
lateinit var others: List<RecipientId>
private set
override fun before() {
context = InstrumentationRegistry.getInstrumentation().targetContext
self = setupSelf()
others = setupOthers()
InstrumentationApplicationDependencyProvider.clearHandlers()
}
private fun setupSelf(): Recipient {
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
val masterSecret = MasterSecretUtil.generateMasterSecret(application, MasterSecretUtil.UNENCRYPTED_PASSPHRASE)
MasterSecretUtil.generateAsymmetricMasterSecret(application, masterSecret)
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
preferences.edit().putBoolean("passphrase_initialized", true).commit()
val registrationRepository = RegistrationRepository(application)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(Put("/v2/keys") { MockResponse().success() })
val response: ServiceResponse<VerifyAccountResponse> = registrationRepository.registerAccountWithoutRegistrationLock(
RegistrationData(
code = "123123",
e164 = "+15555550101",
password = Util.getSecret(18),
registrationId = registrationRepository.registrationId,
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"))
return Recipient.self()
}
private fun setupOthers(): List<RecipientId> {
val others = mutableListOf<RecipientId>()
if (othersCount !in 0 until 1000) {
throw IllegalArgumentException("$othersCount must be between 0 and 1000")
}
for (i in 0 until othersCount) {
val aci = ACI.from(UUID.randomUUID())
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, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), IdentityKeyUtil.generateIdentityKeyPair().publicKey)
others += recipientId
}
return others
}
inline fun <reified T : Activity> launchActivity(initIntent: Intent.() -> Unit = {}): ActivityScenario<T> {
return androidx.test.core.app.launchActivity(Intent(context, T::class.java).apply(initIntent))
}
fun changeIdentityKey(recipient: Recipient, identityKey: IdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey) {
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
}
fun getIdentity(recipient: Recipient): IdentityKey {
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
}
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
}
}

View File

@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.testing
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import java.util.UUID
/**
* Sets up bare-minimum to allow writing unit tests against the database,
* including setting up the local ACI and PNI pair.
*
* @param deleteAllThreadsOnEachRun Run deleteAllThreads between each unit test
*/
class SignalDatabaseRule(
private val deleteAllThreadsOnEachRun: Boolean = true
) : TestWatcher() {
val localAci: ACI = ACI.from(UUID.randomUUID())
val localPni: PNI = PNI.from(UUID.randomUUID())
override fun starting(description: Description?) {
deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
override fun finished(description: Description?) {
deleteAllThreads()
}
private fun deleteAllThreads() {
if (deleteAllThreadsOnEachRun) {
SignalDatabase.messages.deleteAllThreads()
}
}
}

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,70 @@
package org.thoughtcrime.securesms.testing
import com.google.protobuf.ByteString
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2
import org.whispersystems.signalservice.internal.serialize.protos.AddressProto
import org.whispersystems.signalservice.internal.serialize.protos.MetadataProto
import org.whispersystems.signalservice.internal.serialize.protos.SignalServiceContentProto
import java.util.UUID
import kotlin.random.Random
class TestProtos private constructor() {
fun address(
uuid: UUID = UUID.randomUUID()
): AddressProto.Builder {
return AddressProto.newBuilder()
.setUuid(ServiceId.from(uuid).toByteString())
}
fun metadata(
address: AddressProto = address().build(),
): MetadataProto.Builder {
return MetadataProto.newBuilder()
.setAddress(address)
}
fun groupContextV2(
revision: Int = 0,
masterKeyBytes: ByteArray = Random.Default.nextBytes(GroupMasterKey.SIZE)
): GroupContextV2.Builder {
return GroupContextV2.newBuilder()
.setRevision(revision)
.setMasterKey(ByteString.copyFrom(masterKeyBytes))
}
fun storyContext(
sentTimestamp: Long = Random.nextLong(),
authorUuid: String = UUID.randomUUID().toString()
): DataMessage.StoryContext.Builder {
return DataMessage.StoryContext.newBuilder()
.setAuthorUuid(authorUuid)
.setSentTimestamp(sentTimestamp)
}
fun dataMessage(): DataMessage.Builder {
return DataMessage.newBuilder()
}
fun content(): SignalServiceProtos.Content.Builder {
return SignalServiceProtos.Content.newBuilder()
}
fun serviceContent(
localAddress: AddressProto = address().build(),
metadata: MetadataProto = metadata().build()
): SignalServiceContentProto.Builder {
return SignalServiceContentProto.newBuilder()
.setLocalAddress(localAddress)
.setMetadata(metadata)
}
companion object {
fun <T> build(buildFn: TestProtos.() -> T): T {
return TestProtos().buildFn()
}
}
}

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

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/signal_accent_green"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -0,0 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="app_name">Signal (Instrumentation)</string>
</resources>

View File

@@ -22,7 +22,6 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
<uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.WRITE_PROFILE"/>
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
@@ -93,6 +92,8 @@
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<application android:name=".ApplicationContext"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -109,7 +110,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 +156,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 +183,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,19 +214,20 @@
</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"
android:host="addstickers" />
</intent-filter>
<intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -255,10 +258,11 @@
</activity-alias>
<activity android:name=".deeplinks.DeepLinkEntryActivity"
android:exported="true"
android:noHistory="true"
android:theme="@style/Signal.Transparent">
<intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -275,7 +279,7 @@
android:host="signal.group"/>
</intent-filter>
<intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -285,7 +289,7 @@
android:host="signal.tube" />
</intent-filter>
<intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -328,11 +332,6 @@
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".migrations.ApplicationMigrationActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
@@ -366,20 +365,32 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.v2.MediaSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:theme="@style/TextSecure.DarkNoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".conversation.mutiselect.forward.MultiselectForwardActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".mediasend.v2.stories.StoriesMultiselectForwardActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".PassphraseChangeActivity"
android:label="@string/AndroidManifest__change_passphrase"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".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">
@@ -406,7 +417,14 @@
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar.StoryViewer"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
android:launchMode="singleTask"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
@@ -464,6 +482,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceActivity"
android:screenOrientation="portrait"
android:label="@string/AndroidManifest__linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -471,10 +490,12 @@
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaPreviewActivity"
<activity android:name=".mediapreview.MediaPreviewV2Activity"
android:label="@string/AndroidManifest__media_preview"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="false"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".AvatarPreviewActivity"
@@ -499,10 +520,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" />
@@ -521,6 +543,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"
@@ -536,7 +559,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"
@@ -552,6 +575,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" />
@@ -635,13 +662,19 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" />
<activity android:name=".megaphone.SmsExportMegaphoneActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" />
<activity android:name=".ratelimit.RecaptchaProofActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" />
<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"
@@ -652,8 +685,18 @@
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<activity android:name=".exporter.flow.SmsExportActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
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:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
<service android:name=".service.webrtc.AndroidCallConnectionService"
@@ -664,13 +707,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>
@@ -710,7 +753,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>
@@ -801,51 +844,53 @@
</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="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
<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>
@@ -853,7 +898,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>
@@ -881,16 +926,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

@@ -1,848 +0,0 @@
/*
* Copyright (C) 2019 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 androidx.camera.view;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.hardware.display.DisplayManager;
import android.hardware.display.DisplayManager.DisplayListener;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.Display;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.Surface;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.annotation.RestrictTo;
import androidx.annotation.RestrictTo.Scope;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.FocusMeteringAction;
import androidx.camera.core.FocusMeteringResult;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
import androidx.camera.core.ImageCapture.OnImageSavedCallback;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Logger;
import androidx.camera.core.MeteringPoint;
import androidx.camera.core.MeteringPointFactory;
import androidx.camera.core.VideoCapture;
import androidx.camera.core.VideoCapture.OnVideoSavedCallback;
import androidx.camera.core.impl.LensFacingConverter;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.core.util.Consumer;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import com.google.common.util.concurrent.ListenableFuture;
import org.signal.core.util.logging.Log;
import java.io.File;
import java.util.concurrent.Executor;
/**
* A {@link View} that displays a preview of the camera with methods {@link
* #takePicture(Executor, OnImageCapturedCallback)},
* {@link #takePicture(ImageCapture.OutputFileOptions, Executor, OnImageSavedCallback)},
* {@link #startRecording(File , Executor , OnVideoSavedCallback callback)}
* and {@link #stopRecording()}.
*
* <p>Because the Camera is a limited resource and consumes a high amount of power, CameraView must
* be opened/closed. CameraView will handle opening/closing automatically through use of a {@link
* LifecycleOwner}. Use {@link #bindToLifecycle(LifecycleOwner)} to start the camera.
*/
@RequiresApi(21)
@SuppressLint("RestrictedApi")
public final class SignalCameraView extends FrameLayout {
static final String TAG = Log.tag(SignalCameraView.class);
static final int INDEFINITE_VIDEO_DURATION = -1;
static final int INDEFINITE_VIDEO_SIZE = -1;
private static final String EXTRA_SUPER = "super";
private static final String EXTRA_ZOOM_RATIO = "zoom_ratio";
private static final String EXTRA_PINCH_TO_ZOOM_ENABLED = "pinch_to_zoom_enabled";
private static final String EXTRA_FLASH = "flash";
private static final String EXTRA_MAX_VIDEO_DURATION = "max_video_duration";
private static final String EXTRA_MAX_VIDEO_SIZE = "max_video_size";
private static final String EXTRA_SCALE_TYPE = "scale_type";
private static final String EXTRA_CAMERA_DIRECTION = "camera_direction";
private static final String EXTRA_CAPTURE_MODE = "captureMode";
private static final int LENS_FACING_NONE = 0;
private static final int LENS_FACING_FRONT = 1;
private static final int LENS_FACING_BACK = 2;
private static final int FLASH_MODE_AUTO = 1;
private static final int FLASH_MODE_ON = 2;
private static final int FLASH_MODE_OFF = 4;
// For tap-to-focus
private long mDownEventTimestamp;
// For pinch-to-zoom
private PinchToZoomGestureDetector mPinchToZoomGestureDetector;
private boolean mIsPinchToZoomEnabled = true;
SignalCameraXModule mCameraModule;
private final DisplayManager.DisplayListener mDisplayListener =
new DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
mCameraModule.invalidateView();
}
};
private PreviewView mPreviewView;
// For accessibility event
private MotionEvent mUpEvent;
// BEGIN Custom Signal Code Block
private Consumer<Throwable> errorConsumer;
private Throwable pendingError;
// END Custom Signal Code Block
public SignalCameraView(@NonNull Context context) {
this(context, null);
}
public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
@RequiresApi(21)
public SignalCameraView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
/**
* Binds control of the camera used by this view to the given lifecycle.
*
* <p>This links opening/closing the camera to the given lifecycle. The camera will not operate
* unless this method is called with a valid {@link LifecycleOwner} that is not in the {@link
* androidx.lifecycle.Lifecycle.State#DESTROYED} state. Call this method only once camera
* permissions have been obtained.
*
* <p>Once the provided lifecycle has transitioned to a {@link
* androidx.lifecycle.Lifecycle.State#DESTROYED} state, CameraView must be bound to a new
* lifecycle through this method in order to operate the camera.
*
* @param lifecycleOwner The lifecycle that will control this view's camera
* @throws IllegalArgumentException if provided lifecycle is in a {@link
* androidx.lifecycle.Lifecycle.State#DESTROYED} state.
* @throws IllegalStateException if camera permissions are not granted.
*/
// BEGIN Custom Signal Code Block
@RequiresPermission(permission.CAMERA)
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner, Consumer<Throwable> errorConsumer) {
mCameraModule.bindToLifecycle(lifecycleOwner);
this.errorConsumer = errorConsumer;
if (pendingError != null) {
errorConsumer.accept(pendingError);
}
}
// END Custom Signal Code Block
private void init(Context context, @Nullable AttributeSet attrs) {
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
// Begin custom signal code block
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
mCameraModule = new SignalCameraXModule(this, error -> {
if (errorConsumer != null) {
errorConsumer.accept(error);
} else {
pendingError = error;
}
});
// End custom signal code block
if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
setScaleType(
PreviewView.ScaleType.fromId(
a.getInteger(R.styleable.CameraView_scaleType,
getScaleType().getId())));
setPinchToZoomEnabled(
a.getBoolean(
R.styleable.CameraView_pinchToZoomEnabled, isPinchToZoomEnabled()));
setCaptureMode(
CaptureMode.fromId(
a.getInteger(R.styleable.CameraView_captureMode,
getCaptureMode().getId())));
int lensFacing = a.getInt(R.styleable.CameraView_lensFacing, LENS_FACING_BACK);
switch (lensFacing) {
case LENS_FACING_NONE:
setCameraLensFacing(null);
break;
case LENS_FACING_FRONT:
setCameraLensFacing(CameraSelector.LENS_FACING_FRONT);
break;
case LENS_FACING_BACK:
setCameraLensFacing(CameraSelector.LENS_FACING_BACK);
break;
default:
// Unhandled event.
}
int flashMode = a.getInt(R.styleable.CameraView_flash, 0);
switch (flashMode) {
case FLASH_MODE_AUTO:
setFlash(ImageCapture.FLASH_MODE_AUTO);
break;
case FLASH_MODE_ON:
setFlash(ImageCapture.FLASH_MODE_ON);
break;
case FLASH_MODE_OFF:
setFlash(ImageCapture.FLASH_MODE_OFF);
break;
default:
// Unhandled event.
}
a.recycle();
}
if (getBackground() == null) {
setBackgroundColor(0xFF111111);
}
mPinchToZoomGestureDetector = new PinchToZoomGestureDetector(context);
}
@Override
@NonNull
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
@Override
@NonNull
protected Parcelable onSaveInstanceState() {
// TODO(b/113884082): Decide what belongs here or what should be invalidated on
// configuration
// change
Bundle state = new Bundle();
state.putParcelable(EXTRA_SUPER, super.onSaveInstanceState());
state.putInt(EXTRA_SCALE_TYPE, getScaleType().getId());
state.putFloat(EXTRA_ZOOM_RATIO, getZoomRatio());
state.putBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED, isPinchToZoomEnabled());
state.putString(EXTRA_FLASH, FlashModeConverter.nameOf(getFlash()));
state.putLong(EXTRA_MAX_VIDEO_DURATION, getMaxVideoDuration());
state.putLong(EXTRA_MAX_VIDEO_SIZE, getMaxVideoSize());
if (getCameraLensFacing() != null) {
state.putString(EXTRA_CAMERA_DIRECTION,
LensFacingConverter.nameOf(getCameraLensFacing()));
}
state.putInt(EXTRA_CAPTURE_MODE, getCaptureMode().getId());
return state;
}
@Override
protected void onRestoreInstanceState(@Nullable Parcelable savedState) {
// TODO(b/113884082): Decide what belongs here or what should be invalidated on
// configuration
// change
if (savedState instanceof Bundle) {
Bundle state = (Bundle) savedState;
super.onRestoreInstanceState(state.getParcelable(EXTRA_SUPER));
setScaleType(PreviewView.ScaleType.fromId(state.getInt(EXTRA_SCALE_TYPE)));
setZoomRatio(state.getFloat(EXTRA_ZOOM_RATIO));
setPinchToZoomEnabled(state.getBoolean(EXTRA_PINCH_TO_ZOOM_ENABLED));
setFlash(FlashModeConverter.valueOf(state.getString(EXTRA_FLASH)));
setMaxVideoDuration(state.getLong(EXTRA_MAX_VIDEO_DURATION));
setMaxVideoSize(state.getLong(EXTRA_MAX_VIDEO_SIZE));
String lensFacingString = state.getString(EXTRA_CAMERA_DIRECTION);
setCameraLensFacing(
TextUtils.isEmpty(lensFacingString)
? null
: LensFacingConverter.valueOf(lensFacingString));
setCaptureMode(CaptureMode.fromId(state.getInt(EXTRA_CAPTURE_MODE)));
} else {
super.onRestoreInstanceState(savedState);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
DisplayManager dpyMgr =
(DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
dpyMgr.registerDisplayListener(mDisplayListener, new Handler(Looper.getMainLooper()));
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
DisplayManager dpyMgr =
(DisplayManager) getContext().getSystemService(Context.DISPLAY_SERVICE);
dpyMgr.unregisterDisplayListener(mDisplayListener);
}
/**
* Gets the {@link LiveData} of the underlying {@link PreviewView}'s
* {@link PreviewView.StreamState}.
*
* @return A {@link LiveData} containing the {@link PreviewView.StreamState}. Apps can either
* get current value by {@link LiveData#getValue()} or register a observer by
* {@link LiveData#observe}.
* @see PreviewView#getPreviewStreamState()
*/
@NonNull
public LiveData<PreviewView.StreamState> getPreviewStreamState() {
return mPreviewView.getPreviewStreamState();
}
@NonNull
PreviewView getPreviewView() {
return mPreviewView;
}
// TODO(b/124269166): Rethink how we can handle permissions here.
@SuppressLint("MissingPermission")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Since bindToLifecycle will depend on the measured dimension, only call it when measured
// dimension is not 0x0
if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
mCameraModule.bindToLifecycleAfterViewMeasured();
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
// TODO(b/124269166): Rethink how we can handle permissions here.
@SuppressLint("MissingPermission")
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
// In case that the CameraView size is always set as 0x0, we still need to trigger to force
// binding to lifecycle
mCameraModule.bindToLifecycleAfterViewMeasured();
mCameraModule.invalidateView();
super.onLayout(changed, left, top, right, bottom);
}
/**
* @return One of {@link Surface#ROTATION_0}, {@link Surface#ROTATION_90}, {@link
* Surface#ROTATION_180}, {@link Surface#ROTATION_270}.
*/
int getDisplaySurfaceRotation() {
Display display = getDisplay();
// Null when the View is detached. If we were in the middle of a background operation,
// better to not NPE. When the background operation finishes, it'll realize that the camera
// was closed.
if (display == null) {
return 0;
}
return display.getRotation();
}
/**
* Returns the scale type used to scale the preview.
*
* @return The current {@link PreviewView.ScaleType}.
*/
@NonNull
public PreviewView.ScaleType getScaleType() {
return mPreviewView.getScaleType();
}
/**
* Sets the view finder scale type.
*
* <p>This controls how the view finder should be scaled and positioned within the view.
*
* @param scaleType The desired {@link PreviewView.ScaleType}.
*/
public void setScaleType(@NonNull PreviewView.ScaleType scaleType) {
mPreviewView.setScaleType(scaleType);
}
/**
* Returns the scale type used to scale the preview.
*
* @return The current {@link CaptureMode}.
*/
@NonNull
public CaptureMode getCaptureMode() {
return mCameraModule.getCaptureMode();
}
/**
* Sets the CameraView capture mode
*
* <p>This controls only image or video capture function is enabled or both are enabled.
*
* @param captureMode The desired {@link CaptureMode}.
*/
public void setCaptureMode(@NonNull CaptureMode captureMode) {
mCameraModule.setCaptureMode(captureMode);
}
/**
* Returns the maximum duration of videos, or {@link #INDEFINITE_VIDEO_DURATION} if there is no
* timeout.
*
* @hide Not currently implemented.
*/
@RestrictTo(Scope.LIBRARY_GROUP)
public long getMaxVideoDuration() {
return mCameraModule.getMaxVideoDuration();
}
/**
* Sets the maximum video duration before
* {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)} is called
* automatically.
* Use {@link #INDEFINITE_VIDEO_DURATION} to disable the timeout.
*/
private void setMaxVideoDuration(long duration) {
mCameraModule.setMaxVideoDuration(duration);
}
/**
* Returns the maximum size of videos in bytes, or {@link #INDEFINITE_VIDEO_SIZE} if there is no
* timeout.
*/
private long getMaxVideoSize() {
return mCameraModule.getMaxVideoSize();
}
/**
* Sets the maximum video size in bytes before
* {@link OnVideoSavedCallback#onVideoSaved(VideoCapture.OutputFileResults)}
* is called automatically. Use {@link #INDEFINITE_VIDEO_SIZE} to disable the size restriction.
*/
private void setMaxVideoSize(long size) {
mCameraModule.setMaxVideoSize(size);
}
/**
* Takes a picture, and calls {@link OnImageCapturedCallback#onCaptureSuccess(ImageProxy)}
* once when done.
*
* @param executor The executor in which the callback methods will be run.
* @param callback Callback which will receive success or failure callbacks.
*/
public void takePicture(@NonNull Executor executor, @NonNull OnImageCapturedCallback callback) {
mCameraModule.takePicture(executor, callback);
}
/**
* Takes a picture and calls
* {@link OnImageSavedCallback#onImageSaved(ImageCapture.OutputFileResults)} when done.
*
* <p> The value of {@link ImageCapture.Metadata#isReversedHorizontal()} in the
* {@link ImageCapture.OutputFileOptions} will be overwritten based on camera direction. For
* front camera, it will be set to true; for back camera, it will be set to false.
*
* @param outputFileOptions Options to store the newly captured image.
* @param executor The executor in which the callback methods will be run.
* @param callback Callback which will receive success or failure.
*/
public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
@NonNull Executor executor,
@NonNull OnImageSavedCallback callback) {
mCameraModule.takePicture(outputFileOptions, executor, callback);
}
/**
* Takes a video and calls the OnVideoSavedCallback when done.
*
* @param outputFileOptions Options to store the newly captured video.
* @param executor The executor in which the callback methods will be run.
* @param callback Callback which will receive success or failure.
*/
public void startRecording(@NonNull VideoCapture.OutputFileOptions outputFileOptions,
@NonNull Executor executor,
@NonNull OnVideoSavedCallback callback) {
mCameraModule.startRecording(outputFileOptions, executor, callback);
}
/** Stops an in progress video. */
public void stopRecording() {
mCameraModule.stopRecording();
}
/** @return True if currently recording. */
public boolean isRecording() {
return mCameraModule.isRecording();
}
/**
* Queries whether the current device has a camera with the specified direction.
*
* @return True if the device supports the direction.
* @throws IllegalStateException if the CAMERA permission is not currently granted.
*/
@RequiresPermission(permission.CAMERA)
public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) {
return mCameraModule.hasCameraWithLensFacing(lensFacing);
}
/**
* Toggles between the primary front facing camera and the primary back facing camera.
*
* <p>This will have no effect if not already bound to a lifecycle via {@link
* #bindToLifecycle(LifecycleOwner)}.
*/
public void toggleCamera() {
mCameraModule.toggleCamera();
}
/**
* Sets the desired camera by specifying desired lensFacing.
*
* <p>This will choose the primary camera with the specified camera lensFacing.
*
* <p>If called before {@link #bindToLifecycle(LifecycleOwner)}, this will set the camera to be
* used when first bound to the lifecycle. If the specified lensFacing is not supported by the
* device, as determined by {@link #hasCameraWithLensFacing(int)}, the first supported
* lensFacing will be chosen when {@link #bindToLifecycle(LifecycleOwner)} is called.
*
* <p>If called with {@code null} AFTER binding to the lifecycle, the behavior would be
* equivalent to unbind the use cases without the lifecycle having to be destroyed.
*
* @param lensFacing The desired camera lensFacing.
*/
public void setCameraLensFacing(@Nullable Integer lensFacing) {
mCameraModule.setCameraLensFacing(lensFacing);
}
/** Returns the currently selected lensFacing. */
@Nullable
public Integer getCameraLensFacing() {
return mCameraModule.getLensFacing();
}
/** Gets the active flash strategy. */
@ImageCapture.FlashMode
public int getFlash() {
return mCameraModule.getFlash();
}
// Begin Signal Custom Code Block
public boolean hasFlash() {
return mCameraModule.hasFlash();
}
// End Signal Custom Code Block
/** Sets the active flash strategy. */
public void setFlash(@ImageCapture.FlashMode int flashMode) {
mCameraModule.setFlash(flashMode);
}
private long delta() {
return System.currentTimeMillis() - mDownEventTimestamp;
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
// Disable pinch-to-zoom and tap-to-focus while the camera module is paused.
if (mCameraModule.isPaused()) {
return false;
}
// Only forward the event to the pinch-to-zoom gesture detector when pinch-to-zoom is
// enabled.
if (isPinchToZoomEnabled()) {
mPinchToZoomGestureDetector.onTouchEvent(event);
}
if (event.getPointerCount() == 2 && isPinchToZoomEnabled() && isZoomSupported()) {
return true;
}
// Camera focus
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownEventTimestamp = System.currentTimeMillis();
break;
case MotionEvent.ACTION_UP:
if (delta() < ViewConfiguration.getLongPressTimeout()
&& mCameraModule.isBoundToLifecycle()) {
mUpEvent = event;
performClick();
}
break;
default:
// Unhandled event.
return false;
}
return true;
}
/**
* Focus the position of the touch event, or focus the center of the preview for
* accessibility events
*/
@Override
public boolean performClick() {
super.performClick();
final float x = (mUpEvent != null) ? mUpEvent.getX() : getX() + getWidth() / 2f;
final float y = (mUpEvent != null) ? mUpEvent.getY() : getY() + getHeight() / 2f;
mUpEvent = null;
Camera camera = mCameraModule.getCamera();
if (camera != null) {
MeteringPointFactory pointFactory = mPreviewView.getMeteringPointFactory();
float afPointWidth = 1.0f / 6.0f; // 1/6 total area
float aePointWidth = afPointWidth * 1.5f;
MeteringPoint afPoint = pointFactory.createPoint(x, y, afPointWidth);
MeteringPoint aePoint = pointFactory.createPoint(x, y, aePointWidth);
ListenableFuture<FocusMeteringResult> future =
camera.getCameraControl().startFocusAndMetering(
new FocusMeteringAction.Builder(afPoint,
FocusMeteringAction.FLAG_AF).addPoint(aePoint,
FocusMeteringAction.FLAG_AE).build());
Futures.addCallback(future, new FutureCallback<FocusMeteringResult>() {
@Override
public void onSuccess(@Nullable FocusMeteringResult result) {
}
@Override
public void onFailure(Throwable t) {
// Throw the unexpected error.
throw new RuntimeException(t);
}
}, CameraXExecutors.directExecutor());
} else {
Logger.d(TAG, "cannot access camera");
}
return true;
}
float rangeLimit(float val, float max, float min) {
return Math.min(Math.max(val, min), max);
}
/**
* Returns whether the view allows pinch-to-zoom.
*
* @return True if pinch to zoom is enabled.
*/
public boolean isPinchToZoomEnabled() {
return mIsPinchToZoomEnabled;
}
/**
* Sets whether the view should allow pinch-to-zoom.
*
* <p>When enabled, the user can pinch the camera to zoom in/out. This only has an effect if the
* bound camera supports zoom.
*
* @param enabled True to enable pinch-to-zoom.
*/
public void setPinchToZoomEnabled(boolean enabled) {
mIsPinchToZoomEnabled = enabled;
}
/**
* Returns the current zoom ratio.
*
* @return The current zoom ratio.
*/
public float getZoomRatio() {
return mCameraModule.getZoomRatio();
}
/**
* Sets the current zoom ratio.
*
* <p>Valid zoom values range from {@link #getMinZoomRatio()} to {@link #getMaxZoomRatio()}.
*
* @param zoomRatio The requested zoom ratio.
*/
public void setZoomRatio(float zoomRatio) {
mCameraModule.setZoomRatio(zoomRatio);
}
/**
* Returns the minimum zoom ratio.
*
* <p>For most cameras this should return a zoom ratio of 1. A zoom ratio of 1 corresponds to a
* non-zoomed image.
*
* @return The minimum zoom ratio.
*/
public float getMinZoomRatio() {
return mCameraModule.getMinZoomRatio();
}
/**
* Returns the maximum zoom ratio.
*
* <p>The zoom ratio corresponds to the ratio between both the widths and heights of a
* non-zoomed image and a maximally zoomed image for the selected camera.
*
* @return The maximum zoom ratio.
*/
public float getMaxZoomRatio() {
return mCameraModule.getMaxZoomRatio();
}
/**
* Returns whether the bound camera supports zooming.
*
* @return True if the camera supports zooming.
*/
public boolean isZoomSupported() {
return mCameraModule.isZoomSupported();
}
/**
* Turns on/off torch.
*
* @param torch True to turn on torch, false to turn off torch.
*/
public void enableTorch(boolean torch) {
mCameraModule.enableTorch(torch);
}
/**
* Returns current torch status.
*
* @return true if torch is on , otherwise false
*/
public boolean isTorchOn() {
return mCameraModule.isTorchOn();
}
/**
* The capture mode used by CameraView.
*
* <p>This enum can be used to determine which capture mode will be enabled for {@link
* SignalCameraView}.
*/
public enum CaptureMode {
/** A mode where image capture is enabled. */
IMAGE(0),
/** A mode where video capture is enabled. */
VIDEO(1),
/**
* A mode where both image capture and video capture are simultaneously enabled. Note that
* this mode may not be available on every device.
*/
MIXED(2);
private final int mId;
int getId() {
return mId;
}
CaptureMode(int id) {
mId = id;
}
static CaptureMode fromId(int id) {
for (CaptureMode f : values()) {
if (f.mId == id) {
return f;
}
}
throw new IllegalArgumentException();
}
}
static class S extends ScaleGestureDetector.SimpleOnScaleGestureListener {
private ScaleGestureDetector.OnScaleGestureListener mListener;
void setRealGestureDetector(ScaleGestureDetector.OnScaleGestureListener l) {
mListener = l;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
return mListener.onScale(detector);
}
}
private class PinchToZoomGestureDetector extends ScaleGestureDetector
implements ScaleGestureDetector.OnScaleGestureListener {
PinchToZoomGestureDetector(Context context) {
this(context, new S());
}
PinchToZoomGestureDetector(Context context, S s) {
super(context, s);
s.setRealGestureDetector(this);
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = detector.getScaleFactor();
// Speeding up the zoom by 2X.
if (scale > 1f) {
scale = 1.0f + (scale - 1.0f) * 2;
} else {
scale = 1.0f - (1.0f - scale) * 2;
}
float newRatio = getZoomRatio() * scale;
newRatio = rangeLimit(newRatio, getMaxZoomRatio(), getMinZoomRatio());
setZoomRatio(newRatio);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
}
}

View File

@@ -1,699 +0,0 @@
/*
* Copyright (C) 2019 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 androidx.camera.view;
import android.Manifest.permission;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Resources;
import android.util.Rational;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RequiresPermission;
import androidx.camera.core.Camera;
import androidx.camera.core.CameraInfoUnavailableException;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCapture.OnImageCapturedCallback;
import androidx.camera.core.ImageCapture.OnImageSavedCallback;
import androidx.camera.core.Logger;
import androidx.camera.core.Preview;
import androidx.camera.core.TorchState;
import androidx.camera.core.UseCase;
import androidx.camera.core.VideoCapture;
import androidx.camera.core.VideoCapture.OnVideoSavedCallback;
import androidx.camera.core.impl.CameraInternal;
import androidx.camera.core.impl.LensFacingConverter;
import androidx.camera.core.impl.utils.CameraOrientationUtil;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.util.Consumer;
import androidx.core.util.Preconditions;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import com.google.common.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.video.VideoUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import static androidx.camera.core.ImageCapture.FLASH_MODE_OFF;
/** CameraX use case operation built on @{link androidx.camera.core}. */
@RequiresApi(21)
@SuppressLint("RestrictedApi")
final class SignalCameraXModule {
public static final String TAG = "CameraXModule";
private static final float UNITY_ZOOM_SCALE = 1f;
private static final float ZOOM_NOT_SUPPORTED = UNITY_ZOOM_SCALE;
private static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
private static final Rational ASPECT_RATIO_4_3 = new Rational(4, 3);
private static final Rational ASPECT_RATIO_9_16 = new Rational(9, 16);
private static final Rational ASPECT_RATIO_3_4 = new Rational(3, 4);
private final Preview.Builder mPreviewBuilder;
private final VideoCapture.Builder mVideoCaptureBuilder;
private final ImageCapture.Builder mImageCaptureBuilder;
private final SignalCameraView mCameraView;
final AtomicBoolean mVideoIsRecording = new AtomicBoolean(false);
private SignalCameraView.CaptureMode mCaptureMode = SignalCameraView.CaptureMode.IMAGE;
private long mMaxVideoDuration = SignalCameraView.INDEFINITE_VIDEO_DURATION;
private long mMaxVideoSize = SignalCameraView.INDEFINITE_VIDEO_SIZE;
@ImageCapture.FlashMode
private int mFlash = FLASH_MODE_OFF;
@Nullable
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
Camera mCamera;
@Nullable
private ImageCapture mImageCapture;
@Nullable
private VideoCapture mVideoCapture;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@Nullable
Preview mPreview;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@Nullable
LifecycleOwner mCurrentLifecycle;
private final LifecycleObserver mCurrentLifecycleObserver =
new LifecycleObserver() {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
public void onDestroy(LifecycleOwner owner) {
if (owner == mCurrentLifecycle) {
clearCurrentLifecycle();
}
}
};
@Nullable
private LifecycleOwner mNewLifecycle;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@Nullable
Integer mCameraLensFacing = CameraSelector.LENS_FACING_BACK;
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@Nullable
ProcessCameraProvider mCameraProvider;
// BEGIN Custom Signal Code Block
SignalCameraXModule(SignalCameraView view, Consumer<Throwable> errorConsumer) {
// END Custom Signal Code Block
mCameraView = view;
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
new FutureCallback<ProcessCameraProvider>() {
// TODO(b/124269166): Rethink how we can handle permissions here.
@SuppressLint("MissingPermission")
@Override
public void onSuccess(@Nullable ProcessCameraProvider provider) {
Preconditions.checkNotNull(provider);
mCameraProvider = provider;
if (mCurrentLifecycle != null) {
bindToLifecycle(mCurrentLifecycle);
}
}
@Override
public void onFailure(Throwable t) {
// BEGIN Custom Signal Code Block
errorConsumer.accept(t);
// END Custom Signal Code Block
}
}, CameraXExecutors.mainThreadExecutor());
mPreviewBuilder = new Preview.Builder().setTargetName("Preview");
mImageCaptureBuilder = new ImageCapture.Builder().setTargetName("ImageCapture");
mVideoCaptureBuilder = new VideoCapture.Builder().setTargetName("VideoCapture")
.setAudioBitRate(VideoUtil.AUDIO_BIT_RATE)
.setVideoFrameRate(VideoUtil.VIDEO_FRAME_RATE)
.setBitRate(VideoUtil.VIDEO_BIT_RATE);
}
@RequiresPermission(permission.CAMERA)
void bindToLifecycle(LifecycleOwner lifecycleOwner) {
mNewLifecycle = lifecycleOwner;
if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) {
bindToLifecycleAfterViewMeasured();
}
}
@RequiresPermission(permission.CAMERA)
void bindToLifecycleAfterViewMeasured() {
if (mNewLifecycle == null) {
return;
}
clearCurrentLifecycle();
if (mNewLifecycle.getLifecycle().getCurrentState() == Lifecycle.State.DESTROYED) {
// Lifecycle is already in a destroyed state. Since it may have been a valid
// lifecycle when bound, but became destroyed while waiting for layout, treat this as
// a no-op now that we have cleared the previous lifecycle.
mNewLifecycle = null;
return;
}
mCurrentLifecycle = mNewLifecycle;
mNewLifecycle = null;
if (mCameraProvider == null) {
// try again once the camera provider is no longer null
return;
}
Set<Integer> available = getAvailableCameraLensFacing();
if (available.isEmpty()) {
Logger.w(TAG, "Unable to bindToLifeCycle since no cameras available");
mCameraLensFacing = null;
}
// Ensure the current camera exists, or default to another camera
if (mCameraLensFacing != null && !available.contains(mCameraLensFacing)) {
Logger.w(TAG, "Camera does not exist with direction " + mCameraLensFacing);
// Default to the first available camera direction
mCameraLensFacing = available.iterator().next();
Logger.w(TAG, "Defaulting to primary camera with direction " + mCameraLensFacing);
}
// Do not attempt to create use cases for a null cameraLensFacing. This could occur if
// the user explicitly sets the LensFacing to null, or if we determined there
// were no available cameras, which should be logged in the logic above.
if (mCameraLensFacing == null) {
return;
}
// Set the preferred aspect ratio as 4:3 if it is IMAGE only mode. Set the preferred aspect
// ratio as 16:9 if it is VIDEO or MIXED mode. Then, it will be WYSIWYG when the view finder
// is in CENTER_INSIDE mode.
boolean isDisplayPortrait = getDisplayRotationDegrees() == 0
|| getDisplayRotationDegrees() == 180;
// Begin Signal Custom Code Block
int resolution = CameraXUtil.getIdealResolution(Resources.getSystem().getDisplayMetrics().widthPixels, Resources.getSystem().getDisplayMetrics().heightPixels);
// End Signal Custom Code Block
Rational targetAspectRatio;
// Begin Signal Custom Code Block
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
// End Signal Custom Code Block
// Begin Signal Custom Code Block
mImageCaptureBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
// End Signal Custom Code Block
mImageCaptureBuilder.setTargetRotation(getDisplaySurfaceRotation());
mImageCapture = mImageCaptureBuilder.build();
// Begin Signal Custom Code Block
Size size = VideoUtil.getVideoRecordingSize();
mVideoCaptureBuilder.setTargetResolution(size);
mVideoCaptureBuilder.setMaxResolution(size);
// End Signal Custom Code Block
mVideoCaptureBuilder.setTargetRotation(getDisplaySurfaceRotation());
// Begin Signal Custom Code Block
if (MediaConstraints.isVideoTranscodeAvailable()) {
mVideoCapture = mVideoCaptureBuilder.build();
}
// End Signal Custom Code Block
// Adjusts the preview resolution according to the view size and the target aspect ratio.
int height = (int) (getMeasuredWidth() / targetAspectRatio.floatValue());
mPreviewBuilder.setTargetResolution(new Size(getMeasuredWidth(), height));
mPreview = mPreviewBuilder.build();
mPreview.setSurfaceProvider(mCameraView.getPreviewView().getSurfaceProvider());
CameraSelector cameraSelector =
new CameraSelector.Builder().requireLensFacing(mCameraLensFacing).build();
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
mImageCapture,
mPreview);
} else if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
mVideoCapture,
mPreview);
} else {
mCamera = mCameraProvider.bindToLifecycle(mCurrentLifecycle, cameraSelector,
mImageCapture,
mVideoCapture, mPreview);
}
setZoomRatio(UNITY_ZOOM_SCALE);
mCurrentLifecycle.getLifecycle().addObserver(mCurrentLifecycleObserver);
// Enable flash setting in ImageCapture after use cases are created and binded.
setFlash(getFlash());
}
public void open() {
throw new UnsupportedOperationException(
"Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
}
public void close() {
throw new UnsupportedOperationException(
"Explicit open/close of camera not yet supported. Use bindtoLifecycle() instead.");
}
public void takePicture(Executor executor, OnImageCapturedCallback callback) {
if (mImageCapture == null) {
return;
}
if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
}
if (callback == null) {
throw new IllegalArgumentException("OnImageCapturedCallback should not be empty");
}
mImageCapture.takePicture(executor, callback);
}
public void takePicture(@NonNull ImageCapture.OutputFileOptions outputFileOptions,
@NonNull Executor executor, OnImageSavedCallback callback) {
if (mImageCapture == null) {
return;
}
if (getCaptureMode() == SignalCameraView.CaptureMode.VIDEO) {
throw new IllegalStateException("Can not take picture under VIDEO capture mode.");
}
if (callback == null) {
throw new IllegalArgumentException("OnImageSavedCallback should not be empty");
}
outputFileOptions.getMetadata().setReversedHorizontal(mCameraLensFacing != null
&& mCameraLensFacing == CameraSelector.LENS_FACING_FRONT);
mImageCapture.takePicture(outputFileOptions, executor, callback);
}
public void startRecording(VideoCapture.OutputFileOptions outputFileOptions,
Executor executor, final OnVideoSavedCallback callback) {
if (mVideoCapture == null) {
return;
}
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) {
throw new IllegalStateException("Can not record video under IMAGE capture mode.");
}
if (callback == null) {
throw new IllegalArgumentException("OnVideoSavedCallback should not be empty");
}
mVideoIsRecording.set(true);
mVideoCapture.startRecording(
outputFileOptions,
executor,
new VideoCapture.OnVideoSavedCallback() {
@Override
public void onVideoSaved(
@NonNull VideoCapture.OutputFileResults outputFileResults) {
mVideoIsRecording.set(false);
callback.onVideoSaved(outputFileResults);
}
@Override
public void onError(
@VideoCapture.VideoCaptureError int videoCaptureError,
@NonNull String message,
@Nullable Throwable cause) {
mVideoIsRecording.set(false);
Logger.e(TAG, message, cause);
callback.onError(videoCaptureError, message, cause);
}
});
}
public void stopRecording() {
if (mVideoCapture == null) {
return;
}
mVideoCapture.stopRecording();
}
public boolean isRecording() {
return mVideoIsRecording.get();
}
// TODO(b/124269166): Rethink how we can handle permissions here.
@SuppressLint("MissingPermission")
public void setCameraLensFacing(@Nullable Integer lensFacing) {
// Setting same lens facing is a no-op, so check for that first
if (!Objects.equals(mCameraLensFacing, lensFacing)) {
// If we're not bound to a lifecycle, just update the camera that will be opened when we
// attach to a lifecycle.
mCameraLensFacing = lensFacing;
if (mCurrentLifecycle != null) {
// Re-bind to lifecycle with new camera
bindToLifecycle(mCurrentLifecycle);
}
}
}
@RequiresPermission(permission.CAMERA)
public boolean hasCameraWithLensFacing(@CameraSelector.LensFacing int lensFacing) {
if (mCameraProvider == null) {
return false;
}
try {
return mCameraProvider.hasCamera(
new CameraSelector.Builder().requireLensFacing(lensFacing).build());
} catch (CameraInfoUnavailableException e) {
return false;
}
}
@Nullable
public Integer getLensFacing() {
return mCameraLensFacing;
}
public void toggleCamera() {
// TODO(b/124269166): Rethink how we can handle permissions here.
@SuppressLint("MissingPermission")
Set<Integer> availableCameraLensFacing = getAvailableCameraLensFacing();
if (availableCameraLensFacing.isEmpty()) {
return;
}
if (mCameraLensFacing == null) {
setCameraLensFacing(availableCameraLensFacing.iterator().next());
return;
}
if (mCameraLensFacing == CameraSelector.LENS_FACING_BACK
&& availableCameraLensFacing.contains(CameraSelector.LENS_FACING_FRONT)) {
setCameraLensFacing(CameraSelector.LENS_FACING_FRONT);
return;
}
if (mCameraLensFacing == CameraSelector.LENS_FACING_FRONT
&& availableCameraLensFacing.contains(CameraSelector.LENS_FACING_BACK)) {
setCameraLensFacing(CameraSelector.LENS_FACING_BACK);
return;
}
}
public float getZoomRatio() {
if (mCamera != null) {
return mCamera.getCameraInfo().getZoomState().getValue().getZoomRatio();
} else {
return UNITY_ZOOM_SCALE;
}
}
public void setZoomRatio(float zoomRatio) {
if (mCamera != null) {
ListenableFuture<Void> future = mCamera.getCameraControl().setZoomRatio(
zoomRatio);
Futures.addCallback(future, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
}
@Override
public void onFailure(Throwable t) {
// Throw the unexpected error.
throw new RuntimeException(t);
}
}, CameraXExecutors.directExecutor());
} else {
Logger.e(TAG, "Failed to set zoom ratio");
}
}
public float getMinZoomRatio() {
if (mCamera != null) {
return mCamera.getCameraInfo().getZoomState().getValue().getMinZoomRatio();
} else {
return UNITY_ZOOM_SCALE;
}
}
public float getMaxZoomRatio() {
if (mCamera != null) {
return mCamera.getCameraInfo().getZoomState().getValue().getMaxZoomRatio();
} else {
return ZOOM_NOT_SUPPORTED;
}
}
public boolean isZoomSupported() {
return getMaxZoomRatio() != ZOOM_NOT_SUPPORTED;
}
// TODO(b/124269166): Rethink how we can handle permissions here.
@SuppressLint("MissingPermission")
private void rebindToLifecycle() {
if (mCurrentLifecycle != null) {
bindToLifecycle(mCurrentLifecycle);
}
}
boolean isBoundToLifecycle() {
return mCamera != null;
}
int getRelativeCameraOrientation(boolean compensateForMirroring) {
int rotationDegrees = 0;
if (mCamera != null) {
rotationDegrees =
mCamera.getCameraInfo().getSensorRotationDegrees(getDisplaySurfaceRotation());
if (compensateForMirroring) {
rotationDegrees = (360 - rotationDegrees) % 360;
}
}
return rotationDegrees;
}
@SuppressLint("UnsafeExperimentalUsageError")
public void invalidateView() {
if (mPreview != null) {
mPreview.setTargetRotation(getDisplaySurfaceRotation()); // Fixes issue #10940 (rotation not updated on phones using "Legacy API")
}
updateViewInfo();
}
void clearCurrentLifecycle() {
if (mCurrentLifecycle != null && mCameraProvider != null) {
// Remove previous use cases
List<UseCase> toUnbind = new ArrayList<>();
if (mImageCapture != null && mCameraProvider.isBound(mImageCapture)) {
toUnbind.add(mImageCapture);
}
if (mVideoCapture != null && mCameraProvider.isBound(mVideoCapture)) {
toUnbind.add(mVideoCapture);
}
if (mPreview != null && mCameraProvider.isBound(mPreview)) {
toUnbind.add(mPreview);
}
if (!toUnbind.isEmpty()) {
mCameraProvider.unbind(toUnbind.toArray((new UseCase[0])));
}
// Remove surface provider once unbound.
if (mPreview != null) {
mPreview.setSurfaceProvider(null);
}
}
mCamera = null;
mCurrentLifecycle = null;
}
// Update view related information used in use cases
private void updateViewInfo() {
if (mImageCapture != null) {
mImageCapture.setCropAspectRatio(new Rational(getWidth(), getHeight()));
mImageCapture.setTargetRotation(getDisplaySurfaceRotation());
}
if (mVideoCapture != null) {
mVideoCapture.setTargetRotation(getDisplaySurfaceRotation());
}
}
@RequiresPermission(permission.CAMERA)
private Set<Integer> getAvailableCameraLensFacing() {
// Start with all camera directions
Set<Integer> available = new LinkedHashSet<>(Arrays.asList(LensFacingConverter.values()));
// If we're bound to a lifecycle, remove unavailable cameras
if (mCurrentLifecycle != null) {
if (!hasCameraWithLensFacing(CameraSelector.LENS_FACING_BACK)) {
available.remove(CameraSelector.LENS_FACING_BACK);
}
if (!hasCameraWithLensFacing(CameraSelector.LENS_FACING_FRONT)) {
available.remove(CameraSelector.LENS_FACING_FRONT);
}
}
return available;
}
@ImageCapture.FlashMode
public int getFlash() {
return mFlash;
}
// Begin Signal Custom Code Block
public boolean hasFlash() {
if (mImageCapture == null) {
return false;
}
CameraInternal camera = mImageCapture.getCamera();
if (camera == null) {
return false;
}
return camera.getCameraInfoInternal().hasFlashUnit();
}
// End Signal Custom Code Block
public void setFlash(@ImageCapture.FlashMode int flash) {
this.mFlash = flash;
if (mImageCapture == null) {
// Do nothing if there is no imageCapture
return;
}
mImageCapture.setFlashMode(flash);
}
public void enableTorch(boolean torch) {
if (mCamera == null) {
return;
}
ListenableFuture<Void> future = mCamera.getCameraControl().enableTorch(torch);
Futures.addCallback(future, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void result) {
}
@Override
public void onFailure(Throwable t) {
// Throw the unexpected error.
throw new RuntimeException(t);
}
}, CameraXExecutors.directExecutor());
}
public boolean isTorchOn() {
if (mCamera == null) {
return false;
}
return mCamera.getCameraInfo().getTorchState().getValue() == TorchState.ON;
}
public Context getContext() {
return mCameraView.getContext();
}
public int getWidth() {
return mCameraView.getWidth();
}
public int getHeight() {
return mCameraView.getHeight();
}
public int getDisplayRotationDegrees() {
return CameraOrientationUtil.surfaceRotationToDegrees(getDisplaySurfaceRotation());
}
protected int getDisplaySurfaceRotation() {
return mCameraView.getDisplaySurfaceRotation();
}
private int getMeasuredWidth() {
return mCameraView.getMeasuredWidth();
}
private int getMeasuredHeight() {
return mCameraView.getMeasuredHeight();
}
@Nullable
public Camera getCamera() {
return mCamera;
}
@NonNull
public SignalCameraView.CaptureMode getCaptureMode() {
return mCaptureMode;
}
public void setCaptureMode(@NonNull SignalCameraView.CaptureMode captureMode) {
this.mCaptureMode = captureMode;
rebindToLifecycle();
}
public long getMaxVideoDuration() {
return mMaxVideoDuration;
}
public void setMaxVideoDuration(long duration) {
mMaxVideoDuration = duration;
}
public long getMaxVideoSize() {
return mMaxVideoSize;
}
public void setMaxVideoSize(long size) {
mMaxVideoSize = size;
}
public boolean isPaused() {
return false;
}
}

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,25 +0,0 @@
package org.thoughtcrime.securesms;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.account.AccountAttributes;
public final class AppCapabilities {
private AppCapabilities() {
}
private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
private static final boolean GV1_MIGRATION = true;
private static final boolean ANNOUNCEMENT_GROUPS = true;
private static final boolean SENDER_KEY = true;
private static final boolean CHANGE_NUMBER = true;
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
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.giftBadges());
}
}

View File

@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.account.AccountAttributes
object AppCapabilities {
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
@JvmStatic
fun getCapabilities(storageCapable: Boolean): AccountAttributes.Capabilities {
return AccountAttributes.Capabilities(
isUuid = false,
isGv2 = true,
isStorage = storageCapable,
isGv1Migration = true,
isSenderKey = true,
isAnnouncementGroup = true,
isChangeNumber = true,
isStories = true,
isGiftBadges = true,
isPnp = FeatureFlags.phoneNumberPrivacy(),
paymentActivation = true
)
}
}

View File

@@ -16,12 +16,13 @@
*/
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.os.StrictMode;
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;
@@ -35,6 +36,8 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.signal.donations.GooglePayApi;
import org.signal.donations.StripeApi;
import org.signal.glide.SignalGlideCodecs;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.signal.ringrtc.CallManager;
@@ -50,19 +53,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;
@@ -99,9 +104,10 @@ import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWra
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.Security;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.core.CompletableObserver;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
@@ -155,7 +161,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addBlocking("rx-init", this::initializeRx)
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
.addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
.addBlocking("app-migrations", this::initializeApplicationMigrations)
.addBlocking("ring-rtc", this::initializeRingRtc)
@@ -177,17 +182,17 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addBlocking("blob-provider", this::initializeBlobProvider)
.addBlocking("feature-flags", FeatureFlags::init)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addNonBlocking(this::checkIsGooglePayReady)
.addNonBlocking(this::cleanAvatarStorage)
.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)
@@ -197,6 +202,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()))
@@ -206,6 +212,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -221,7 +229,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
ApplicationDependencies.getFrameRateTracker().start();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
ApplicationDependencies.getDeadlockDetector().start();
SubscriptionKeepAliveJob.launchSubscriberIdKeepAliveJobIfNecessary();
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary();
@@ -231,6 +239,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");
@@ -330,7 +348,8 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
ApplicationDependencies.getIncomingMessageObserver();
}
private void initializeAppDependencies() {
@VisibleForTesting
void initializeAppDependencies() {
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this));
}
@@ -374,6 +393,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);
@@ -440,6 +466,18 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
AvatarPickerStorage.cleanOrphans(this);
}
@SuppressLint("CheckResult")
private void checkIsGooglePayReady() {
GooglePayApi.queryIsReadyToPay(
this,
new StripeApi.Gateway(Environment.Donations.getStripeConfiguration()),
Environment.Donations.getGooglePayConfiguration()
).subscribe(
/* onComplete = */ () -> SignalStore.donationsValues().setGooglePayReady(true),
/* onError = */ t -> SignalStore.donationsValues().setGooglePayReady(false)
);
}
@WorkerThread
private void initializeCleanup() {
int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();

View File

@@ -92,7 +92,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
Recipient.live(recipientId).observe(this, recipient -> {
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient)
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isSelf() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();

View File

@@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable;
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
@@ -46,7 +47,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
boolean hasWallpaper,
boolean isMessageRequestAccepted,
boolean canPlayInline,
@NonNull Colorizer colorizer);
@NonNull Colorizer colorizer,
@NonNull ConversationItemDisplayMode displayMode);
@NonNull ConversationMessage getConversationMessage();
@@ -61,12 +63,13 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
}
default void updateSelectedState() {
// Intentionall Blank.
// Intentionally Blank.
}
interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
void onQuotedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms);
void onStickerClicked(@NonNull StickerLocator stickerLocator);
void onViewOnceMessageClicked(@NonNull MmsMessageRecord messageRecord);
@@ -100,10 +103,14 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onDonateClicked();
void onBlockJoinRequest(@NonNull Recipient recipient);
void onRecipientNameClicked(@NonNull RecipientId target);
void onInviteToSignalClicked();
void onActivatePaymentsClicked();
void onSendPaymentClicked(@NonNull RecipientId recipientId);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
@@ -11,7 +12,8 @@ import java.util.Set;
public interface BindableConversationListItem extends Unbindable {
void bind(@NonNull ThreadRecord thread,
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations);

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

@@ -28,6 +28,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -69,8 +70,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
@Override
protected void onCreate(Bundle icicle, boolean ready) {
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
int displayMode = Util.isDefaultSmsProvider(this) ? DisplayMode.FLAG_ALL
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
boolean includeSms = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
int displayMode = includeSms ? DisplayMode.FLAG_ALL : DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
}

View File

@@ -18,7 +18,6 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
@@ -32,7 +31,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.TextView;
import android.widget.Toast;
@@ -43,6 +41,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.DefaultItemAnimator;
@@ -54,15 +53,14 @@ import androidx.transition.TransitionManager;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper;
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactChip;
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
@@ -70,22 +68,25 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.HeaderAction;
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.SelectedContacts;
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareContact;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException;
@@ -95,6 +96,9 @@ import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
import io.reactivex.rxjava3.disposables.Disposable;
import kotlin.Unit;
/**
* Fragment for selecting a one or more contacts from a list.
*
@@ -106,7 +110,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
@SuppressWarnings("unused")
private static final String TAG = Log.tag(ContactSelectionListFragment.class);
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1;
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 0;
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
public static final int NO_LIMIT = Integer.MAX_VALUE;
@@ -134,25 +138,26 @@ public final class ContactSelectionListFragment extends LoggingFragment
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private RecyclerView chipRecycler;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
private View shadowView;
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper;
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) {
@@ -201,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
@@ -250,17 +263,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
showContactsButton = view.findViewById(R.id.show_contacts_button);
showContactsDescription = view.findViewById(R.id.show_contacts_description);
showContactsProgress = view.findViewById(R.id.progress);
chipGroup = view.findViewById(R.id.chipGroup);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
chipRecycler = view.findViewById(R.id.chipRecycler);
constraintLayout = view.findViewById(R.id.container);
shadowView = view.findViewById(R.id.toolbar_shadow);
headerActionView = view.findViewById(R.id.header_action);
toolbarShadowAnimationHelper = new ToolbarShadowAnimationHelper(shadowView);
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
recyclerView.addOnScrollListener(toolbarShadowAnimationHelper);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setItemAnimator(new DefaultItemAnimator() {
@Override
@@ -269,6 +277,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
});
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
contactChipAdapter = new MappingAdapter();
lifecycleDisposable = new LifecycleDisposable();
lifecycleDisposable.bindTo(getViewLifecycleOwner());
SelectedContacts.register(contactChipAdapter, this::onChipCloseIconClicked);
chipRecycler.setAdapter(contactChipAdapter);
Disposable disposable = contactChipViewModel.getState().subscribe(this::handleSelectedContactsChanged);
lifecycleDisposable.add(disposable);
Intent intent = requireActivity().getIntent();
Bundle arguments = safeArguments();
@@ -336,6 +356,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
return view;
}
@Override
public void onDestroyView() {
super.onDestroyView();
constraintLayout = null;
}
private @NonNull Bundle safeArguments() {
return getArguments() != null ? getArguments() : new Bundle();
}
@@ -391,7 +417,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
null,
new ListClickListener(),
isMulti,
currentSelection);
currentSelection,
safeArguments().getInt(ContactSelectionArguments.CHECKBOX_RESOURCE, R.drawable.contact_selection_checkbox));
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
@@ -640,7 +667,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orElse(null), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orElse(null), contact.getNumber());
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
if (!canSelectSelf && !selectedContact.hasUsername() && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return;
}
@@ -707,6 +734,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
}
}
@Override
public boolean onItemLongClick(ContactSelectionListItem item) {
if (onItemLongClickListener != null) {
return onItemLongClickListener.onLongClick(item, recyclerView);
} else {
return false;
}
}
}
private boolean selectionHardLimitReached() {
@@ -734,75 +770,22 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
removeChipForContact(selectedContact);
contactChipViewModel.remove(selectedContact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged();
}
}
private void removeChipForContact(@NonNull SelectedContact contact) {
for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) {
View v = chipGroup.getChildAt(i);
if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) {
chipGroup.removeView(v);
}
}
private void handleSelectedContactsChanged(@NonNull List<SelectedContacts.Model> selectedContacts) {
contactChipAdapter.submitList(new MappingModelList(selectedContacts), this::smoothScrollChipsToEnd);
if (getChipCount() == 0) {
if (selectedContacts.isEmpty()) {
setChipGroupVisibility(ConstraintSet.GONE);
}
}
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
resolved -> addChipForRecipient(resolved, selectedContact));
}
private void addChipForRecipient(@NonNull Recipient recipient, @NonNull SelectedContact selectedContact) {
final ContactChip chip = new ContactChip(requireContext());
if (getChipCount() == 0) {
} else {
setChipGroupVisibility(ConstraintSet.VISIBLE);
}
chip.setText(recipient.getShortDisplayName(requireContext()));
chip.setContact(selectedContact);
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(view -> {
markContactUnselected(selectedContact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orElse(null));
}
});
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
if (getView() == null || !requireView().isAttachedToWindow()) {
Log.w(TAG, "Fragment's view was detached before the animation completed.");
return;
}
if (view == chip && transitionType == LayoutTransition.APPEARING) {
chipGroup.getLayoutTransition().removeTransitionListener(this);
registerChipRecipientObserver(chip, recipient.live());
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
}
}
});
chip.setAvatar(glideRequests, recipient, () -> addChip(chip));
}
private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip);
if (selectionWarningLimitReachedExactly()) {
if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
@@ -812,21 +795,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
}
private int getChipCount() {
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
if (count < 0) throw new AssertionError();
return count;
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
resolved -> contactChipViewModel.add(selectedContact));
}
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) {
if (recipient != null) {
recipient.observe(getViewLifecycleOwner(), resolved -> {
if (chip.isAttachedToWindow()) {
chip.setAvatar(glideRequests, resolved, null);
chip.setText(resolved.getShortDisplayName(chip.getContext()));
}
});
private Unit onChipCloseIconClicked(SelectedContacts.Model model) {
markContactUnselected(model.getSelectedContact());
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(Optional.of(model.getRecipient().getId()), model.getRecipient().getE164().orElse(null));
}
return Unit.INSTANCE;
}
private int getChipCount() {
int count = contactChipViewModel.getCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
if (count < 0) throw new AssertionError();
return count;
}
private void setChipGroupVisibility(int visibility) {
@@ -842,7 +829,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(constraintLayout);
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility);
constraintSet.setVisibility(R.id.chipRecycler, visibility);
constraintSet.applyTo(constraintLayout);
}
@@ -851,8 +838,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
private void smoothScrollChipsToEnd() {
int x = ViewUtil.isLtr(chipGroupScrollContainer) ? chipGroup.getWidth() : 0;
chipGroupScrollContainer.smoothScrollTo(x, 0);
int x = ViewUtil.isLtr(chipRecycler) ? chipRecycler.getWidth() : 0;
chipRecycler.smoothScrollBy(x, 0);
}
public interface OnContactSelectedListener {
@@ -886,6 +873,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
@NonNull HeaderAction getHeaderAction();
}
public interface OnItemLongClickListener {
boolean onLongClick(ContactSelectionListItem contactSelectionListItem, RecyclerView recyclerView);
}
public interface AbstractContactsCursorLoaderFactoryProvider {
@NonNull AbstractContactsCursorLoader.Factory get();
}

View File

@@ -1,201 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Parcelable;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
import org.thoughtcrime.securesms.service.ApplicationMigrationService.ImportState;
public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
private final ImportServiceConnection serviceConnection = new ImportServiceConnection();
private final ImportStateHandler importStateHandler = new ImportStateHandler();
private final BroadcastReceiver completedReceiver = new NullReceiver();
private LinearLayout promptLayout;
private LinearLayout progressLayout;
private Button skipButton;
private Button importButton;
private ProgressBar progress;
private TextView progressLabel;
private ApplicationMigrationService importService;
private boolean isVisible = false;
@Override
protected void onCreate(Bundle bundle, boolean ready) {
setContentView(R.layout.database_migration_activity);
initializeResources();
initializeServiceBinding();
}
@Override
public void onResume() {
super.onResume();
isVisible = true;
registerForCompletedNotification();
}
@Override
public void onPause() {
super.onPause();
isVisible = false;
unregisterForCompletedNotification();
}
@Override
public void onDestroy() {
super.onDestroy();
shutdownServiceBinding();
}
@Override
public void onBackPressed() {
}
private void initializeServiceBinding() {
Intent intent = new Intent(this, ApplicationMigrationService.class);
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
private void initializeResources() {
this.promptLayout = (LinearLayout)findViewById(R.id.prompt_layout);
this.progressLayout = (LinearLayout)findViewById(R.id.progress_layout);
this.skipButton = (Button) findViewById(R.id.skip_button);
this.importButton = (Button) findViewById(R.id.import_button);
this.progress = (ProgressBar) findViewById(R.id.import_progress);
this.progressLabel = (TextView) findViewById(R.id.import_status);
this.progressLayout.setVisibility(View.GONE);
this.promptLayout.setVisibility(View.GONE);
this.importButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(DatabaseMigrationActivity.this, ApplicationMigrationService.class);
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
intent.putExtra("master_secret", (Parcelable)getIntent().getParcelableExtra("master_secret"));
startService(intent);
promptLayout.setVisibility(View.GONE);
progressLayout.setVisibility(View.VISIBLE);
}
});
this.skipButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ApplicationMigrationService.setDatabaseImported(DatabaseMigrationActivity.this);
handleImportComplete();
}
});
}
private void registerForCompletedNotification() {
IntentFilter filter = new IntentFilter();
filter.addAction(ApplicationMigrationService.COMPLETED_ACTION);
filter.setPriority(1000);
registerReceiver(completedReceiver, filter);
}
private void unregisterForCompletedNotification() {
unregisterReceiver(completedReceiver);
}
private void shutdownServiceBinding() {
unbindService(serviceConnection);
}
private void handleStateIdle() {
this.promptLayout.setVisibility(View.VISIBLE);
this.progressLayout.setVisibility(View.GONE);
}
private void handleStateProgress(ProgressDescription update) {
this.promptLayout.setVisibility(View.GONE);
this.progressLayout.setVisibility(View.VISIBLE);
this.progressLabel.setText(update.primaryComplete + "/" + update.primaryTotal);
double max = this.progress.getMax();
double primaryTotal = update.primaryTotal;
double primaryComplete = update.primaryComplete;
double secondaryTotal = update.secondaryTotal;
double secondaryComplete = update.secondaryComplete;
this.progress.setProgress((int)Math.round((primaryComplete / primaryTotal) * max));
this.progress.setSecondaryProgress((int)Math.round((secondaryComplete / secondaryTotal) * max));
}
private void handleImportComplete() {
if (isVisible) {
if (getIntent().hasExtra("next_intent")) {
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
} else {
// TODO [greyson] Navigation
startActivity(MainActivity.clearTop(this));
}
}
finish();
}
private class ImportStateHandler extends Handler {
public ImportStateHandler() {
super(Looper.getMainLooper());
}
@Override
public void handleMessage(Message message) {
switch (message.what) {
case ImportState.STATE_IDLE: handleStateIdle(); break;
case ImportState.STATE_MIGRATING_IN_PROGRESS: handleStateProgress((ProgressDescription)message.obj); break;
case ImportState.STATE_MIGRATING_COMPLETE: handleImportComplete(); break;
}
}
}
private class ImportServiceConnection implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
importService = ((ApplicationMigrationService.ApplicationMigrationBinder)service).getService();
importService.setImportStateHandler(importStateHandler);
ImportState state = importService.getState();
importStateHandler.obtainMessage(state.state, state.progress).sendToTarget();
}
@Override
public void onServiceDisconnected(ComponentName name) {
importService.setImportStateHandler(null);
}
}
private static class NullReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
abortBroadcast();
}
}
}

View File

@@ -10,6 +10,8 @@ import android.os.Bundle;
import android.os.Vibrator;
import android.text.TextUtils;
import android.transition.TransitionInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
@@ -25,11 +27,11 @@ import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
@@ -54,6 +56,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
private DeviceAddFragment deviceAddFragment;
private DeviceListFragment deviceListFragment;
private DeviceLinkFragment deviceLinkFragment;
private MenuItem cameraSwitchItem = null;
@Override
public void onPreCreate() {
@@ -102,6 +105,18 @@ public class DeviceActivity extends PassphraseRequiredActivity
return false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.device_add, menu);
cameraSwitchItem = menu.findItem(R.id.device_add_camera_switch);
cameraSwitchItem.setVisible(false);
return super.onCreateOptionsMenu(menu);
}
public MenuItem getCameraSwitchItem() {
return cameraSwitchItem;
}
@Override
public void onClick(View v) {
Permissions.with(this)

View File

@@ -2,48 +2,47 @@ package org.thoughtcrime.securesms;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.qr.ScanningThread;
import org.signal.qr.QrScannerView;
import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXModelBlocklist;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.ViewUtil;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
public class DeviceAddFragment extends LoggingFragment {
private ViewGroup container;
private LinearLayout overlay;
private ImageView devicesImage;
private CameraView scannerView;
private ScanningThread scanningThread;
private ScanListener scanListener;
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
private ImageView devicesImage;
private ScanListener scanListener;
private QrScannerView scannerView;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
this.overlay = this.container.findViewById(R.id.overlay);
this.scannerView = this.container.findViewById(R.id.scanner);
this.devicesImage = this.container.findViewById(R.id.devices);
ViewGroup container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
this.overlay.setOrientation(LinearLayout.HORIZONTAL);
} else {
this.overlay.setOrientation(LinearLayout.VERTICAL);
}
this.scannerView = container.findViewById(R.id.scanner);
this.devicesImage = container.findViewById(R.id.devices);
ViewCompat.setTransitionName(devicesImage, "devices");
if (Build.VERSION.SDK_INT >= 21) {
this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@TargetApi(21)
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
@@ -59,52 +58,43 @@ public class DeviceAddFragment extends LoggingFragment {
});
}
return this.container;
scannerView.start(getViewLifecycleOwner(), CameraXModelBlocklist.isBlocklisted());
lifecycleDisposable.bindTo(getViewLifecycleOwner());
Disposable qrDisposable = scannerView
.getQrData()
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(qrData -> {
if (scanListener != null) {
scanListener.onQrDataFound(qrData);
}
});
lifecycleDisposable.add(qrDisposable);
return container;
}
@Override
public void onResume() {
super.onResume();
this.scanningThread = new ScanningThread();
this.scanningThread.setScanListener(scanListener);
this.scannerView.onResume();
this.scannerView.setPreviewCallback(scanningThread);
this.scanningThread.start();
}
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
MenuItem switchCamera = ((DeviceActivity) requireActivity()).getCameraSwitchItem();
@Override
public void onPause() {
super.onPause();
this.scannerView.onPause();
this.scanningThread.stopScanning();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
this.scannerView.onPause();
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
overlay.setOrientation(LinearLayout.HORIZONTAL);
} else {
overlay.setOrientation(LinearLayout.VERTICAL);
if (switchCamera != null) {
switchCamera.setVisible(true);
switchCamera.setOnMenuItemClickListener(v -> {
scannerView.toggleCamera();
return true;
});
}
this.scannerView.onResume();
this.scannerView.setPreviewCallback(scanningThread);
}
public ImageView getDevicesImage() {
return devicesImage;
}
public void setScanListener(ScanListener scanListener) {
this.scanListener = scanListener;
if (this.scanningThread != null) {
this.scanningThread.setScanListener(scanListener);
}
}
}

View File

@@ -9,6 +9,7 @@ import android.view.ViewGroup;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
public class DeviceLinkFragment extends Fragment implements View.OnClickListener {
@@ -21,6 +22,7 @@ public class DeviceLinkFragment extends Fragment implements View.OnClickListener
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = (LinearLayout) inflater.inflate(R.layout.device_link_fragment, container, false);
this.container.findViewById(R.id.link_device).setOnClickListener(this);
ViewCompat.setTransitionName(container.findViewById(R.id.devices), "devices");
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
container.setOrientation(LinearLayout.HORIZONTAL);

View File

@@ -21,7 +21,7 @@ import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.melnykov.fab.FloatingActionButton;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;

View File

@@ -27,10 +27,11 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Util;
@@ -118,7 +119,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
smsSendButton.setOnClickListener(new SmsSendClickListener());
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
if (Util.isDefaultSmsProvider(this)) {
if (Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().isSmsSupported()) {
shareButton.setOnClickListener(new ShareClickListener());
smsButton.setOnClickListener(new SmsClickListener());
} else {
@@ -253,7 +254,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
Recipient recipient = Recipient.resolved(recipientId);
int subscriptionId = recipient.getDefaultSubscriptionId().orElse(-1);
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
MessageSender.send(context, OutgoingMessage.sms(recipient, message, subscriptionId), -1L, true, null, null);
if (recipient.getContactUri() != null) {
SignalDatabase.recipients().setHasSentInvite(recipient.getId());

View File

@@ -125,10 +125,10 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
private void updateTabVisibility() {
if (Stories.isFeatureEnabled()) {
findViewById(R.id.conversation_list_tabs).setVisibility(View.VISIBLE);
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_colorSecondaryContainer));
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorSurface2));
} else {
findViewById(R.id.conversation_list_tabs).setVisibility(View.GONE);
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_background_primary));
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorBackground));
conversationListTabsViewModel.onChatsSelected();
}
}

View File

@@ -1,890 +0,0 @@
/*
* Copyright (C) 2014 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.app.ShareCompat;
import androidx.core.util.Pair;
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.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.animation.DepthPageTransformer;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.StorageUtil;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
/**
* Activity for displaying media attachments in-app
*/
public final class MediaPreviewActivity extends PassphraseRequiredActivity
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
MediaRailAdapter.RailItemListener,
MediaPreviewFragment.Events,
VoiceNoteMediaControllerOwner
{
private final static String TAG = Log.tag(MediaPreviewActivity.class);
private static final int NOT_IN_A_THREAD = -2;
public static final String THREAD_ID_EXTRA = "thread_id";
public static final String DATE_EXTRA = "date";
public static final String SIZE_EXTRA = "size";
public static final String CAPTION_EXTRA = "caption";
public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent";
public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media";
public static final String SHOW_THREAD_EXTRA = "show_thread";
public static final String SORTING_EXTRA = "sorting";
public static final String IS_VIDEO_GIF = "is_video_gif";
private ViewPager mediaPager;
private View detailsContainer;
private TextView caption;
private View captionContainer;
private RecyclerView albumRail;
private MediaRailAdapter albumRailAdapter;
private ViewGroup playbackControlsContainer;
private Uri initialMediaUri;
private String initialMediaType;
private long initialMediaSize;
private String initialCaption;
private boolean initialMediaIsVideoGif;
private boolean leftIsRecent;
private MediaPreviewViewModel viewModel;
private ViewPagerListener viewPagerListener;
private int restartItem = -1;
private long threadId = NOT_IN_A_THREAD;
private boolean cameFromAllMedia;
private boolean showThread;
private MediaDatabase.Sorting sorting;
private FullscreenHelper fullscreenHelper;
private VoiceNoteMediaController voiceNoteMediaController;
private @Nullable Cursor cursor = null;
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
@NonNull MediaRecord mediaRecord,
boolean leftIsRecent)
{
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId());
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif());
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
return intent;
}
@Override
protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
super.attachBaseContext(newBase);
}
@SuppressWarnings("ConstantConditions")
@Override
protected void onCreate(Bundle bundle, boolean ready) {
this.setTheme(R.style.TextSecure_MediaPreview);
setContentView(R.layout.media_preview_activity);
setSupportActionBar(findViewById(R.id.toolbar));
voiceNoteMediaController = new VoiceNoteMediaController(this);
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
fullscreenHelper = new FullscreenHelper(this);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
initializeViews();
initializeResources();
initializeObservers();
}
@SuppressLint("MissingSuperCall")
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
public void onRailItemClicked(int distanceFromActive) {
mediaPager.setCurrentItem(mediaPager.getCurrentItem() + distanceFromActive);
}
@Override
public void onRailItemDeleteClicked(int distanceFromActive) {
throw new UnsupportedOperationException("Callback unsupported.");
}
@SuppressWarnings("ConstantConditions")
private void initializeActionBar() {
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem != null) {
getSupportActionBar().setTitle(getTitleText(mediaItem));
getSupportActionBar().setSubtitle(getSubTitleText(mediaItem));
}
}
private @NonNull String getTitleText(@NonNull MediaItem mediaItem) {
String from;
if (mediaItem.outgoing) from = getString(R.string.MediaPreviewActivity_you);
else if (mediaItem.recipient != null) from = mediaItem.recipient.getDisplayName(this);
else from = "";
if (showThread) {
String titleText = null;
Recipient threadRecipient = mediaItem.threadRecipient;
if (threadRecipient != null) {
if (mediaItem.outgoing) {
if (threadRecipient.isSelf()) {
titleText = getString(R.string.note_to_self);
} else {
titleText = getString(R.string.MediaPreviewActivity_you_to_s, threadRecipient.getDisplayName(this));
}
} else {
if (threadRecipient.isGroup()) {
titleText = getString(R.string.MediaPreviewActivity_s_to_s, from, threadRecipient.getDisplayName(this));
} else {
titleText = getString(R.string.MediaPreviewActivity_s_to_you, from);
}
}
}
return titleText != null ? titleText : from;
} else {
return from;
}
}
private @NonNull String getSubTitleText(@NonNull MediaItem mediaItem) {
if (mediaItem.date > 0) {
return DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), mediaItem.date);
} else {
return getString(R.string.MediaPreviewActivity_draft);
}
}
@Override
public void onResume() {
super.onResume();
initializeMedia();
}
@Override
public void onPause() {
super.onPause();
restartItem = cleanupMedia();
}
@Override
protected void onDestroy() {
if (cursor != null) {
cursor.close();
cursor = null;
}
super.onDestroy();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
initializeResources();
}
private void initializeViews() {
mediaPager = findViewById(R.id.media_pager);
mediaPager.setOffscreenPageLimit(1);
mediaPager.setPageTransformer(true, new DepthPageTransformer());
viewPagerListener = new ViewPagerListener();
mediaPager.addOnPageChangeListener(viewPagerListener);
albumRail = findViewById(R.id.media_preview_album_rail);
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
albumRail.setItemAnimator(null); // Or can crash when set to INVISIBLE while animating by FullscreenHelper https://issuetracker.google.com/issues/148720682
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
albumRail.setAdapter(albumRailAdapter);
detailsContainer = findViewById(R.id.media_preview_details_container);
caption = findViewById(R.id.media_preview_caption);
captionContainer = findViewById(R.id.media_preview_caption_container);
playbackControlsContainer = findViewById(R.id.media_preview_playback_controls_container);
View toolbarLayout = findViewById(R.id.toolbar_layout);
anchorMarginsToBottomInsets(detailsContainer);
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), findViewById(R.id.toolbar));
fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
}
private void initializeResources() {
Intent intent = getIntent();
threadId = intent.getLongExtra(THREAD_ID_EXTRA, NOT_IN_A_THREAD);
cameFromAllMedia = intent.getBooleanExtra(HIDE_ALL_MEDIA_EXTRA, false);
showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false);
sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)];
initialMediaUri = intent.getData();
initialMediaType = intent.getType();
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
initialMediaIsVideoGif = intent.getBooleanExtra(IS_VIDEO_GIF, false);
restartItem = -1;
}
private void initializeObservers() {
viewModel.getPreviewData().observe(this, previewData -> {
if (previewData == null || mediaPager == null || mediaPager.getAdapter() == null) {
return;
}
if (!((MediaItemAdapter) mediaPager.getAdapter()).hasFragmentFor(mediaPager.getCurrentItem())) {
Log.d(TAG, "MediaItemAdapter wasn't ready. Posting again...");
viewModel.resubmitPreviewData();
}
View playbackControls = ((MediaItemAdapter) mediaPager.getAdapter()).getPlaybackControls(mediaPager.getCurrentItem());
if (previewData.getAlbumThumbnails().isEmpty() && previewData.getCaption() == null && playbackControls == null) {
detailsContainer.setVisibility(View.GONE);
} else {
detailsContainer.setVisibility(View.VISIBLE);
}
albumRail.setVisibility(previewData.getAlbumThumbnails().isEmpty() ? View.GONE : View.VISIBLE);
albumRailAdapter.setMedia(previewData.getAlbumThumbnails(), previewData.getActivePosition());
albumRail.smoothScrollToPosition(previewData.getActivePosition());
captionContainer.setVisibility(previewData.getCaption() == null ? View.GONE : View.VISIBLE);
caption.setText(previewData.getCaption());
if (playbackControls != null) {
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
playbackControls.setLayoutParams(params);
playbackControlsContainer.removeAllViews();
playbackControlsContainer.addView(playbackControls);
} else {
playbackControlsContainer.removeAllViews();
}
});
}
private void initializeMedia() {
if (!isContentTypeSupported(initialMediaType)) {
Log.w(TAG, "Unsupported media type sent to MediaPreviewActivity, finishing.");
Toast.makeText(getApplicationContext(), R.string.MediaPreviewActivity_unssuported_media_type, Toast.LENGTH_LONG).show();
finish();
}
Log.i(TAG, "Loading Part URI: " + initialMediaUri);
if (isMediaInDb()) {
LoaderManager.getInstance(this).restartLoader(0, null, this);
} else {
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize, initialMediaIsVideoGif));
if (initialCaption != null) {
detailsContainer.setVisibility(View.VISIBLE);
captionContainer.setVisibility(View.VISIBLE);
caption.setText(initialCaption);
}
}
}
private int cleanupMedia() {
int restartItem = mediaPager.getCurrentItem();
mediaPager.removeAllViews();
mediaPager.setAdapter(null);
viewModel.setCursor(this, null, leftIsRecent);
return restartItem;
}
private void showOverview() {
startActivity(MediaOverviewActivity.forThread(this, threadId));
}
private void forward() {
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem != null) {
MultiselectForwardFragmentArgs.create(
this,
mediaItem.uri,
mediaItem.type,
args -> MultiselectForwardFragment.showBottomSheet(getSupportFragmentManager(), args)
);
}
}
private void share() {
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem != null) {
Uri publicUri = PartAuthority.getAttachmentPublicUri(mediaItem.uri);
String mimeType = Intent.normalizeMimeType(mediaItem.type);
Intent shareIntent = ShareCompat.IntentBuilder.from(this)
.setStream(publicUri)
.setType(mimeType)
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
try {
startActivity(shareIntent);
} catch (ActivityNotFoundException e) {
Log.w(TAG, "No activity existed to share the media.", e);
Toast.makeText(this, R.string.MediaPreviewActivity_cant_find_an_app_able_to_share_this_media, Toast.LENGTH_LONG).show();
}
}
}
@SuppressWarnings("CodeBlock2Expr")
@SuppressLint("InlinedApi")
private void saveToDisk() {
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem != null) {
SaveAttachmentTask.showWarningDialog(this, (dialogInterface, i) -> {
if (StorageUtil.canWriteToMediaStore()) {
performSavetoDisk(mediaItem);
return;
}
Permissions.with(this)
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
.onAnyDenied(() -> Toast.makeText(this, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
.onAllGranted(() -> {
performSavetoDisk(mediaItem);
})
.execute();
});
}
}
private void performSavetoDisk(@NonNull MediaItem mediaItem) {
SaveAttachmentTask saveTask = new SaveAttachmentTask(MediaPreviewActivity.this);
long saveDate = (mediaItem.date > 0) ? mediaItem.date : System.currentTimeMillis();
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(mediaItem.uri, mediaItem.type, saveDate, null));
}
@SuppressLint("StaticFieldLeak")
private void deleteMedia() {
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem == null || mediaItem.attachment == null) {
return;
}
AlertDialog.Builder builder = new AlertDialog.Builder(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);
builder.setCancelable(true);
builder.setPositiveButton(R.string.delete, (dialogInterface, which) -> {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... voids) {
AttachmentUtil.deleteAttachment(MediaPreviewActivity.this.getApplicationContext(),
mediaItem.attachment);
return null;
}
}.execute();
finish();
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
menu.clear();
MenuInflater inflater = this.getMenuInflater();
inflater.inflate(R.menu.media_preview, menu);
super.onCreateOptionsMenu(menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (!isMediaInDb()) {
menu.findItem(R.id.media_preview__overview).setVisible(false);
menu.findItem(R.id.delete).setVisible(false);
}
// Restricted to API26 because of MemoryFileUtil not supporting lower API levels well
menu.findItem(R.id.media_preview__share).setVisible(Build.VERSION.SDK_INT >= 26);
if (cameFromAllMedia) {
menu.findItem(R.id.media_preview__overview).setVisible(false);
}
super.onPrepareOptionsMenu(menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
super.onOptionsItemSelected(item);
int itemId = item.getItemId();
if (itemId == R.id.media_preview__overview) { showOverview(); return true; }
if (itemId == R.id.media_preview__forward) { forward(); return true; }
if (itemId == R.id.media_preview__share) { share(); return true; }
if (itemId == R.id.save) { saveToDisk(); return true; }
if (itemId == R.id.delete) { deleteMedia(); return true; }
if (itemId == android.R.id.home) { finish(); return true; }
return false;
}
private boolean isMediaInDb() {
return threadId != NOT_IN_A_THREAD;
}
private @Nullable MediaItem getCurrentMediaItem() {
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
if (adapter != null) {
return adapter.getMediaItemFor(mediaPager.getCurrentItem());
} else {
return null;
}
}
public static boolean isContentTypeSupported(final String contentType) {
return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType);
}
@Override
public @NonNull Loader<Pair<Cursor, Integer>> onCreateLoader(int id, Bundle args) {
return new PagingMediaLoader(this, threadId, initialMediaUri, leftIsRecent, sorting);
}
@Override
public void onLoadFinished(@NonNull Loader<Pair<Cursor, Integer>> loader, @Nullable Pair<Cursor, Integer> data) {
if (data != null) {
if (data.first == cursor) {
return;
}
if (cursor != null) {
cursor.close();
}
cursor = Objects.requireNonNull(data.first);
viewModel.setCursor(this, cursor, leftIsRecent);
int mediaPosition = Objects.requireNonNull(data.second);
CursorPagerAdapter oldAdapter = (CursorPagerAdapter) mediaPager.getAdapter();
if (oldAdapter == null) {
CursorPagerAdapter adapter = new CursorPagerAdapter(getSupportFragmentManager(), this, cursor, mediaPosition, leftIsRecent);
mediaPager.setAdapter(adapter);
adapter.setActive(true);
} else {
oldAdapter.setCursor(cursor, mediaPosition);
oldAdapter.setActive(true);
}
if (oldAdapter == null || restartItem >= 0) {
int item = restartItem >= 0 ? restartItem : mediaPosition;
mediaPager.setCurrentItem(item);
if (item == 0) {
viewPagerListener.onPageSelected(0);
}
}
} else {
mediaNotAvailable();
}
}
@Override
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
}
@Override
public boolean singleTapOnMedia() {
fullscreenHelper.toggleUiVisibility();
return true;
}
@Override
public void mediaNotAvailable() {
Toast.makeText(this, R.string.MediaPreviewActivity_media_no_longer_available, Toast.LENGTH_LONG).show();
finish();
}
@Override
public void onMediaReady() {
}
@Override
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
return voiceNoteMediaController;
}
private class ViewPagerListener extends ExtendedOnPageChangedListener {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
if (adapter != null) {
MediaItem item = adapter.getMediaItemFor(position);
if (item != null && item.recipient != null) {
item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
}
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
initializeActionBar();
}
}
@Override
public void onPageUnselected(int position) {
MediaItemAdapter adapter = (MediaItemAdapter)mediaPager.getAdapter();
if (adapter != null) {
MediaItem item = adapter.getMediaItemFor(position);
if (item != null && item.recipient != null) {
item.recipient.live().removeObservers(MediaPreviewActivity.this);
}
adapter.pause(position);
}
}
}
private static class SingleItemPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
private final Uri uri;
private final String mediaType;
private final long size;
private final boolean isVideoGif;
private MediaPreviewFragment mediaPreviewFragment;
SingleItemPagerAdapter(@NonNull FragmentManager fragmentManager,
@NonNull Uri uri,
@NonNull String mediaType,
long size,
boolean isVideoGif)
{
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.uri = uri;
this.mediaType = mediaType;
this.size = size;
this.isVideoGif = isVideoGif;
}
@Override
public int getCount() {
return 1;
}
@NonNull
@Override
public Fragment getItem(int position) {
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true, isVideoGif);
return mediaPreviewFragment;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
if (mediaPreviewFragment != null) {
mediaPreviewFragment.cleanUp();
mediaPreviewFragment = null;
}
}
@Override
public @Nullable MediaItem getMediaItemFor(int position) {
return new MediaItem(null, null, null, uri, mediaType, -1, true);
}
@Override
public void pause(int position) {
if (mediaPreviewFragment != null) {
mediaPreviewFragment.pause();
}
}
@Override
public @Nullable View getPlaybackControls(int position) {
if (mediaPreviewFragment != null) {
return mediaPreviewFragment.getPlaybackControls();
}
return null;
}
@Override
public boolean hasFragmentFor(int position) {
return mediaPreviewFragment != null;
}
}
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
ViewCompat.setOnApplyWindowInsetsListener(viewToAnchor, (view, insets) -> {
ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
layoutParams.setMargins(insets.getSystemWindowInsetLeft(),
layoutParams.topMargin,
insets.getSystemWindowInsetRight(),
insets.getSystemWindowInsetBottom());
view.setLayoutParams(layoutParams);
return insets;
});
}
private static class CursorPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
@SuppressLint("UseSparseArrays")
private final Map<Integer, MediaPreviewFragment> mediaFragments = new HashMap<>();
private final Context context;
private final boolean leftIsRecent;
private boolean active;
private Cursor cursor;
private int autoPlayPosition;
CursorPagerAdapter(@NonNull FragmentManager fragmentManager,
@NonNull Context context,
@NonNull Cursor cursor,
int autoPlayPosition,
boolean leftIsRecent)
{
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.context = context.getApplicationContext();
this.cursor = cursor;
this.autoPlayPosition = autoPlayPosition;
this.leftIsRecent = leftIsRecent;
}
public void setActive(boolean active) {
this.active = active;
notifyDataSetChanged();
}
public void setCursor(@NonNull Cursor cursor, int autoPlayPosition) {
this.cursor = cursor;
this.autoPlayPosition = autoPlayPosition;
}
@Override
public int getCount() {
if (!active) return 0;
else return cursor.getCount();
}
@NonNull
@Override
public Fragment getItem(int position) {
boolean autoPlay = autoPlayPosition == position;
int cursorPosition = getCursorPosition(position);
autoPlayPosition = -1;
cursor.moveToPosition(cursorPosition);
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(context, cursor);
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
MediaPreviewFragment fragment = MediaPreviewFragment.newInstance(attachment, autoPlay);
mediaFragments.put(position, fragment);
return fragment;
}
@Override
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
MediaPreviewFragment removed = mediaFragments.remove(position);
if (removed != null) {
removed.cleanUp();
}
super.destroyItem(container, position, object);
}
public @Nullable MediaItem getMediaItemFor(int position) {
int cursorPosition = getCursorPosition(position);
if (cursor.isClosed() || cursorPosition < 0) {
Log.w(TAG, "Invalid cursor state! Closed: " + cursor.isClosed() + " Position: " + cursorPosition);
return null;
}
cursor.moveToPosition(cursorPosition);
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
RecipientId recipientId = mediaRecord.getRecipientId();
RecipientId threadRecipientId = mediaRecord.getThreadRecipientId();
return new MediaItem(Recipient.live(recipientId).get(),
Recipient.live(threadRecipientId).get(),
attachment,
Objects.requireNonNull(attachment.getUri()),
mediaRecord.getContentType(),
mediaRecord.getDate(),
mediaRecord.isOutgoing());
}
@Override
public void pause(int position) {
MediaPreviewFragment mediaView = mediaFragments.get(position);
if (mediaView != null) mediaView.pause();
}
@Override
public @Nullable View getPlaybackControls(int position) {
MediaPreviewFragment mediaView = mediaFragments.get(position);
if (mediaView != null) return mediaView.getPlaybackControls();
return null;
}
@Override
public boolean hasFragmentFor(int position) {
return mediaFragments.containsKey(position);
}
private int getCursorPosition(int position) {
if (leftIsRecent) return position;
else return cursor.getCount() - 1 - position;
}
}
private static class MediaItem {
private final @Nullable Recipient recipient;
private final @Nullable Recipient threadRecipient;
private final @Nullable DatabaseAttachment attachment;
private final @NonNull Uri uri;
private final @NonNull String type;
private final long date;
private final boolean outgoing;
private MediaItem(@Nullable Recipient recipient,
@Nullable Recipient threadRecipient,
@Nullable DatabaseAttachment attachment,
@NonNull Uri uri,
@NonNull String type,
long date,
boolean outgoing)
{
this.recipient = recipient;
this.threadRecipient = threadRecipient;
this.attachment = attachment;
this.uri = uri;
this.type = type;
this.date = date;
this.outgoing = outgoing;
}
}
interface MediaItemAdapter {
@Nullable MediaItem getMediaItemFor(int position);
void pause(int position);
@Nullable View getPlaybackControls(int position);
boolean hasFragmentFor(int position);
}
}

View File

@@ -20,11 +20,28 @@ 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 androidx.recyclerview.widget.RecyclerView;
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,42 +50,70 @@ 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.Util;
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
public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
boolean smsSupported = SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
if (recipientId.isPresent()) {
launch(Recipient.resolved(recipientId.get()));
} else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
if (SignalStore.account().isRegistered() && NetworkConstraint.isMet(getApplication())) {
if (SignalStore.account().isRegistered()) {
Log.i(TAG, "[onContactSelected] Doing contact refresh.");
AlertDialog progress = SimpleProgressDialog.show(this);
@@ -83,15 +128,31 @@ public class NewConversationActivity extends ContactSelectionActivity
resolved = Recipient.resolved(resolved.getId());
} catch (IOException e) {
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
return null;
}
}
return resolved;
}, resolved -> {
progress.dismiss();
launch(resolved);
if (resolved != null) {
if (smsSupported || resolved.isRegistered() && resolved.hasServiceId()) {
launch(resolved);
} else {
new MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, resolved.getDisplayName(this)))
.setPositiveButton(android.R.string.ok, null)
.show();
}
} else {
new MaterialAlertDialogBuilder(this)
.setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again)
.setPositiveButton(android.R.string.ok, null)
.show();
}
});
} else {
} else if (smsSupported) {
launch(Recipient.external(this, number));
}
}
@@ -120,10 +181,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 +231,151 @@ public class NewConversationActivity extends ContactSelectionActivity
handleCreateGroup();
finish();
}
@Override
public boolean onLongClick(ContactSelectionListItem contactSelectionListItem, RecyclerView recyclerView) {
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))
.onDismiss(() -> recyclerView.suppressLayout(false))
.show(actions);
recyclerView.suppressLayout(true);
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_chat_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;
}
if (recipient.isRegistered() || (SignalStore.misc().getSmsExportPhase().allowSmsFeatures())) {
return new ActionItem(
R.drawable.ic_phone_right_24,
getString(R.string.NewConversationActivity__audio_call),
R.color.signal_colorOnSurface,
() -> CommunicationActions.startVoiceCall(this, recipient)
);
} else {
return null;
}
}
private @Nullable ActionItem createVideoCallActionItem(@NonNull Recipient recipient) {
if (recipient.isSelf() || recipient.isMmsGroup() || !recipient.isRegistered()) {
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_blocked, recipient.getDisplayName(this));
contactsFragment.reset();
}));
})
);
}
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, recipient.getDisplayName(this));
}));
}
)
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private void displaySnackbar(@StringRes int message, Object ... formatArgs) {
Snackbar.make(findViewById(android.R.id.content), getString(message, formatArgs), 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

@@ -1,149 +0,0 @@
package org.thoughtcrime.securesms;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import java.util.Optional;
public class TransportOption implements Parcelable {
public enum Type {
SMS,
TEXTSECURE
}
private final int drawable;
private final int backgroundColor;
private final @NonNull String text;
private final @NonNull Type type;
private final @NonNull String composeHint;
private final @NonNull CharacterCalculator characterCalculator;
private final @NonNull Optional<CharSequence> simName;
private final @NonNull Optional<Integer> simSubscriptionId;
public TransportOption(@NonNull Type type,
@DrawableRes int drawable,
int backgroundColor,
@NonNull String text,
@NonNull String composeHint,
@NonNull CharacterCalculator characterCalculator)
{
this(type, drawable, backgroundColor, text, composeHint, characterCalculator,
Optional.empty(), Optional.empty());
}
public TransportOption(@NonNull Type type,
@DrawableRes int drawable,
int backgroundColor,
@NonNull String text,
@NonNull String composeHint,
@NonNull CharacterCalculator characterCalculator,
@NonNull Optional<CharSequence> simName,
@NonNull Optional<Integer> simSubscriptionId)
{
this.type = type;
this.drawable = drawable;
this.backgroundColor = backgroundColor;
this.text = text;
this.composeHint = composeHint;
this.characterCalculator = characterCalculator;
this.simName = simName;
this.simSubscriptionId = simSubscriptionId;
}
TransportOption(Parcel in) {
this(Type.valueOf(in.readString()),
in.readInt(),
in.readInt(),
in.readString(),
in.readString(),
CharacterCalculator.readFromParcel(in),
Optional.ofNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.empty());
}
public @NonNull Type getType() {
return type;
}
public boolean isType(Type type) {
return this.type == type;
}
public boolean isSms() {
return type == Type.SMS;
}
public CharacterState calculateCharacters(String messageBody) {
return characterCalculator.calculateCharacters(messageBody);
}
public @DrawableRes int getDrawable() {
return drawable;
}
public int getBackgroundColor() {
return backgroundColor;
}
public @NonNull String getComposeHint() {
return composeHint;
}
public @NonNull String getDescription() {
return text;
}
@NonNull
public Optional<CharSequence> getSimName() {
return simName;
}
@NonNull
public Optional<Integer> getSimSubscriptionId() {
return simSubscriptionId;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(type.name());
dest.writeInt(drawable);
dest.writeInt(backgroundColor);
dest.writeString(text);
dest.writeString(composeHint);
CharacterCalculator.writeToParcel(dest, characterCalculator);
TextUtils.writeToParcel(simName.orElse(null), dest, flags);
if (simSubscriptionId.isPresent()) {
dest.writeInt(1);
dest.writeInt(simSubscriptionId.get());
} else {
dest.writeInt(0);
}
}
public static final Creator<TransportOption> CREATOR = new Creator<TransportOption>() {
@Override
public TransportOption createFromParcel(Parcel in) {
return new TransportOption(in);
}
@Override
public TransportOption[] newArray(int size) {
return new TransportOption[size];
}
};
}

View File

@@ -1,226 +0,0 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.MmsCharacterCalculator;
import org.thoughtcrime.securesms.util.PushCharacterCalculator;
import org.thoughtcrime.securesms.util.SmsCharacterCalculator;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
import org.whispersystems.signalservice.api.util.OptionalUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
import static org.thoughtcrime.securesms.TransportOption.Type;
public class TransportOptions {
private static final String TAG = Log.tag(TransportOptions.class);
private final List<OnTransportChangedListener> listeners = new LinkedList<>();
private final Context context;
private final List<TransportOption> enabledTransports;
private Type defaultTransportType = Type.SMS;
private Optional<Integer> defaultSubscriptionId = Optional.empty();
private Optional<TransportOption> selectedOption = Optional.empty();
private final Optional<Integer> systemSubscriptionId;
public TransportOptions(Context context, boolean media) {
this.context = context;
this.enabledTransports = initializeAvailableTransports(media);
this.systemSubscriptionId = new SubscriptionManagerCompat(context).getPreferredSubscriptionId();
}
public void reset(boolean media) {
List<TransportOption> transportOptions = initializeAvailableTransports(media);
this.enabledTransports.clear();
this.enabledTransports.addAll(transportOptions);
if (selectedOption.isPresent() && !isEnabled(selectedOption.get())) {
setSelectedTransport(null);
} else {
this.defaultTransportType = Type.SMS;
this.defaultSubscriptionId = Optional.empty();
notifyTransportChangeListeners();
}
}
public void setDefaultTransport(Type type) {
this.defaultTransportType = type;
if (!selectedOption.isPresent()) {
notifyTransportChangeListeners();
}
}
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
if (defaultSubscriptionId.equals(subscriptionId)) {
return;
}
this.defaultSubscriptionId = subscriptionId;
if (!selectedOption.isPresent()) {
notifyTransportChangeListeners();
}
}
public void setSelectedTransport(@Nullable TransportOption transportOption) {
this.selectedOption = Optional.ofNullable(transportOption);
notifyTransportChangeListeners();
}
public boolean isManualSelection() {
return this.selectedOption.isPresent();
}
public @NonNull TransportOption getSelectedTransport() {
if (selectedOption.isPresent()) return selectedOption.get();
if (defaultTransportType == Type.SMS) {
TransportOption transportOption = findEnabledSmsTransportOption(OptionalUtil.or(defaultSubscriptionId, systemSubscriptionId));
if (transportOption != null) {
return transportOption;
}
}
for (TransportOption transportOption : enabledTransports) {
if (transportOption.getType() == defaultTransportType) {
return transportOption;
}
}
throw new AssertionError("No options of default type!");
}
public static @NonNull TransportOption getPushTransportOption(@NonNull Context context) {
return new TransportOption(Type.TEXTSECURE,
R.drawable.ic_send_lock_24,
context.getResources().getColor(R.color.core_ultramarine),
context.getString(R.string.ConversationActivity_transport_signal),
context.getString(R.string.conversation_activity__type_message_push),
new PushCharacterCalculator());
}
private @Nullable TransportOption findEnabledSmsTransportOption(Optional<Integer> subscriptionId) {
if (subscriptionId.isPresent()) {
final int subId = subscriptionId.get();
for (TransportOption transportOption : enabledTransports) {
if (transportOption.getType() == Type.SMS &&
subId == transportOption.getSimSubscriptionId().orElse(-1)) {
return transportOption;
}
}
}
return null;
}
public void disableTransport(Type type) {
TransportOption selected = selectedOption.orElse(null);
Iterator<TransportOption> iterator = enabledTransports.iterator();
while (iterator.hasNext()) {
TransportOption option = iterator.next();
if (option.isType(type)) {
if (selected == option) {
setSelectedTransport(null);
}
iterator.remove();
}
}
}
public List<TransportOption> getEnabledTransports() {
return enabledTransports;
}
public void addOnTransportChangedListener(OnTransportChangedListener listener) {
this.listeners.add(listener);
}
private List<TransportOption> initializeAvailableTransports(boolean isMediaMessage) {
List<TransportOption> results = new LinkedList<>();
if (isMediaMessage) {
results.addAll(getTransportOptionsForSimCards(context.getString(R.string.ConversationActivity_transport_insecure_mms),
context.getString(R.string.conversation_activity__type_message_mms_insecure),
new MmsCharacterCalculator()));
} else {
results.addAll(getTransportOptionsForSimCards(context.getString(R.string.ConversationActivity_transport_insecure_sms),
context.getString(R.string.conversation_activity__type_message_sms_insecure),
new SmsCharacterCalculator()));
}
results.add(getPushTransportOption(context));
return results;
}
private @NonNull List<TransportOption> getTransportOptionsForSimCards(@NonNull String text,
@NonNull String composeHint,
@NonNull CharacterCalculator characterCalculator)
{
List<TransportOption> results = new LinkedList<>();
SubscriptionManagerCompat subscriptionManager = new SubscriptionManagerCompat(context);
Collection<SubscriptionInfoCompat> subscriptions;
if (Permissions.hasAll(context, Manifest.permission.READ_PHONE_STATE)) {
subscriptions = subscriptionManager.getActiveAndReadySubscriptionInfos();
} else {
subscriptions = Collections.emptyList();
}
if (subscriptions.size() < 2) {
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_unlock_24,
context.getResources().getColor(R.color.core_grey_50),
text, composeHint, characterCalculator));
} else {
for (SubscriptionInfoCompat subscriptionInfo : subscriptions) {
results.add(new TransportOption(Type.SMS, R.drawable.ic_send_unlock_24,
context.getResources().getColor(R.color.core_grey_50),
text, composeHint, characterCalculator,
Optional.of(subscriptionInfo.getDisplayName()),
Optional.of(subscriptionInfo.getSubscriptionId())));
}
}
return results;
}
private void notifyTransportChangeListeners() {
for (OnTransportChangedListener listener : listeners) {
listener.onChange(getSelectedTransport(), selectedOption.isPresent());
}
}
private boolean isEnabled(TransportOption transportOption) {
for (TransportOption option : enabledTransports) {
if (option.equals(transportOption)) return true;
}
return false;
}
public interface OnTransportChangedListener {
public void onChange(TransportOption newTransport, boolean manuallySelected);
}
}

View File

@@ -1,73 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.graphics.PorterDuff.Mode;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import java.util.List;
public class TransportOptionsAdapter extends BaseAdapter {
private final LayoutInflater inflater;
private List<TransportOption> enabledTransports;
public TransportOptionsAdapter(@NonNull Context context,
@NonNull List<TransportOption> enabledTransports)
{
super();
this.inflater = LayoutInflater.from(context);
this.enabledTransports = enabledTransports;
}
public void setEnabledTransports(List<TransportOption> enabledTransports) {
this.enabledTransports = enabledTransports;
}
@Override
public int getCount() {
return enabledTransports.size();
}
@Override
public Object getItem(int position) {
return enabledTransports.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = inflater.inflate(R.layout.transport_selection_list_item, parent, false);
}
TransportOption transport = (TransportOption) getItem(position);
ImageView imageView = convertView.findViewById(R.id.icon);
TextView textView = convertView.findViewById(R.id.text);
TextView subtextView = convertView.findViewById(R.id.subtext);
imageView.getBackground().setColorFilter(transport.getBackgroundColor(), Mode.MULTIPLY);
imageView.setImageResource(transport.getDrawable());
textView.setText(transport.getDescription());
if (transport.getSimName().isPresent()) {
subtextView.setText(transport.getSimName().get());
subtextView.setVisibility(View.VISIBLE);
} else {
subtextView.setVisibility(View.GONE);
}
return convertView;
}
}

View File

@@ -1,50 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.ListPopupWindow;
import java.util.LinkedList;
import java.util.List;
public class TransportOptionsPopup extends ListPopupWindow implements ListView.OnItemClickListener {
private final TransportOptionsAdapter adapter;
private final SelectedListener listener;
public TransportOptionsPopup(@NonNull Context context, @NonNull View anchor, @NonNull SelectedListener listener) {
super(context);
this.listener = listener;
this.adapter = new TransportOptionsAdapter(context, new LinkedList<TransportOption>());
setVerticalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_yoff));
setHorizontalOffset(context.getResources().getDimensionPixelOffset(R.dimen.transport_selection_popup_xoff));
setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
setModal(true);
setAnchorView(anchor);
setAdapter(adapter);
setContentWidth(context.getResources().getDimensionPixelSize(R.dimen.transport_selection_popup_width));
setOnItemClickListener(this);
}
public void display(List<TransportOption> enabledTransports) {
adapter.setEnabledTransports(enabledTransports);
adapter.notifyDataSetChanged();
show();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
listener.onSelected((TransportOption)adapter.getItem(position));
}
public interface SelectedListener {
void onSelected(TransportOption option);
}
}

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;
@@ -62,6 +64,7 @@ import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls;
import org.thoughtcrime.securesms.components.webrtc.WifiToCellularPopupWindow;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -70,6 +73,7 @@ import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequest
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
@@ -105,19 +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();
@@ -130,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);
@@ -155,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));
}
@@ -182,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);
}
@@ -231,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);
}
@@ -253,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()
@@ -298,7 +312,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
callScreen = findViewById(R.id.callScreen);
callScreen.setControlsListener(new ControlsListener());
participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen);
participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen);
wifiToCellularPopupWindow = new WifiToCellularPopupWindow(callScreen);
}
private void initializeViewModel(boolean isLandscapeEnabled) {
@@ -337,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) {
@@ -344,7 +364,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall());
return;
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords());
SafetyNumberBottomSheet.forGroupCall(((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords())
.show(getSupportFragmentManager());
return;
} else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) {
callScreen.switchToSpeakerView();
@@ -373,6 +394,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
videoTooltip.dismiss();
videoTooltip = null;
}
} else if (event instanceof WebRtcCallViewModel.Event.ShowWifiToCellularPopup) {
wifiToCellularPopupWindow.show();
} else {
throw new IllegalArgumentException("Unknown event: " + event);
}
@@ -557,11 +580,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
final Recipient recipient = event.getRemoteParticipants().get(0).getRecipient();
if (theirKey == null) {
Log.w(TAG, "Untrusted identity without an identity key, terminating call.");
handleTerminate(recipient, HangupMessage.Type.NORMAL);
Log.w(TAG, "Untrusted identity without an identity key.");
}
SafetyNumberChangeDialog.showForCall(getSupportFragmentManager(), recipient.getId());
SafetyNumberBottomSheet.forCall(recipient.getId()).show(getSupportFragmentManager());
}
public void handleSafetyNumberChangeEvent(@NonNull WebRtcCallViewModel.SafetyNumberChangeEvent safetyNumberChangeEvent) {
@@ -570,7 +592,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
GroupCallSafetyNumberChangeNotificationUtil.showNotification(this, viewModel.getRecipient().get());
} else {
GroupCallSafetyNumberChangeNotificationUtil.cancelNotification(this, viewModel.getRecipient().get());
SafetyNumberChangeDialog.showForDuringGroupCall(getSupportFragmentManager(), safetyNumberChangeEvent.getRecipientIds());
SafetyNumberBottomSheet.forDuringGroupCall(safetyNumberChangeEvent.getRecipientIds()).show(getSupportFragmentManager());
}
}
}
@@ -580,7 +602,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
public void handleGroupMemberCountChange(int count) {
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize() && FeatureFlags.groupCallRinging();
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize();
callScreen.enableRingGroup(canRing);
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
}

View File

@@ -1,40 +0,0 @@
package org.thoughtcrime.securesms.animation;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.ViewPager;
/**
* Based on https://developer.android.com/training/animation/screen-slide#depth-page
*/
public final class DepthPageTransformer implements ViewPager.PageTransformer {
private static final float MIN_SCALE = 0.75f;
public void transformPage(@NonNull View view, float position) {
final int pageWidth = view.getWidth();
if (position < -1f) {
view.setAlpha(0f);
} else if (position <= 0f) {
view.setAlpha(1f);
view.setTranslationX(0f);
view.setScaleX(1f);
view.setScaleY(1f);
} else if (position <= 1f) {
view.setAlpha(1f - position);
view.setTranslationX(pageWidth * -position);
final float scaleFactor = MIN_SCALE + (1f - MIN_SCALE) * (1f - Math.abs(position));
view.setScaleX(scaleFactor);
view.setScaleY(scaleFactor);
} else {
view.setAlpha(0f);
}
}
}

View File

@@ -3,15 +3,14 @@ package org.thoughtcrime.securesms.animation.transitions
import android.animation.Animator
import android.animation.ValueAnimator
import android.content.Context
import android.transition.Transition
import android.transition.TransitionValues
import android.util.AttributeSet
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.core.view.ViewCompat
import androidx.transition.Transition
import androidx.transition.TransitionValues
import com.google.android.material.floatingactionbutton.FloatingActionButton
@RequiresApi(21)
class FabElevationFadeTransform(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
class FabElevationFadeTransform(context: Context, attrs: AttributeSet) : Transition(context, attrs) {
companion object {
private const val ELEVATION = "CrossfaderTransition.ELEVATION"
@@ -19,23 +18,23 @@ class FabElevationFadeTransform(context: Context, attrs: AttributeSet?) : Transi
override fun captureStartValues(transitionValues: TransitionValues) {
if (transitionValues.view is FloatingActionButton) {
transitionValues.values[ELEVATION] = transitionValues.view.elevation
transitionValues.values[ELEVATION] = ViewCompat.getElevation(transitionValues.view)
}
}
override fun captureEndValues(transitionValues: TransitionValues) {
if (transitionValues.view is FloatingActionButton) {
transitionValues.values[ELEVATION] = transitionValues.view.elevation
transitionValues.values[ELEVATION] = ViewCompat.getElevation(transitionValues.view)
}
}
override fun createAnimator(sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues?.view !is FloatingActionButton || endValues?.view !is FloatingActionButton) {
return null
}
val startElevation = startValues.view.elevation
val endElevation = endValues.view.elevation
val startElevation = ViewCompat.getElevation(startValues.view)
val endElevation = ViewCompat.getElevation(endValues.view)
if (startElevation == endElevation) {
return null
}
@@ -46,7 +45,7 @@ class FabElevationFadeTransform(context: Context, attrs: AttributeSet?) : Transi
).apply {
addUpdateListener {
val elevation = it.animatedValue as Float
endValues.view.elevation = elevation
ViewCompat.setElevation(endValues.view, elevation)
}
}
}

View File

@@ -7,8 +7,8 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.stickers.StickerLocator;
public abstract class Attachment {
@@ -118,8 +118,13 @@ public abstract class Attachment {
}
public boolean isInProgress() {
return transferState != AttachmentDatabase.TRANSFER_PROGRESS_DONE &&
transferState != AttachmentDatabase.TRANSFER_PROGRESS_FAILED;
return transferState != AttachmentTable.TRANSFER_PROGRESS_DONE &&
transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED &&
transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE;
}
public boolean isPermanentlyFailed() {
return transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE;
}
public long getSize() {

View File

@@ -6,7 +6,7 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.stickers.StickerLocator;

View File

@@ -5,8 +5,8 @@ import android.net.Uri;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.MessageTable;
public class MmsNotificationAttachment extends Attachment {
@@ -26,14 +26,14 @@ public class MmsNotificationAttachment extends Attachment {
}
private static int getTransferStateFromStatus(int status) {
if (status == MmsDatabase.Status.DOWNLOAD_INITIALIZED ||
status == MmsDatabase.Status.DOWNLOAD_NO_CONNECTIVITY)
if (status == MessageTable.MmsStatus.DOWNLOAD_INITIALIZED ||
status == MessageTable.MmsStatus.DOWNLOAD_NO_CONNECTIVITY)
{
return AttachmentDatabase.TRANSFER_PROGRESS_PENDING;
} else if (status == MmsDatabase.Status.DOWNLOAD_CONNECTING) {
return AttachmentDatabase.TRANSFER_PROGRESS_STARTED;
return AttachmentTable.TRANSFER_PROGRESS_PENDING;
} else if (status == MessageTable.MmsStatus.DOWNLOAD_CONNECTING) {
return AttachmentTable.TRANSFER_PROGRESS_STARTED;
} else {
return AttachmentDatabase.TRANSFER_PROGRESS_FAILED;
return AttachmentTable.TRANSFER_PROGRESS_FAILED;
}
}
}

View File

@@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
@@ -68,7 +68,7 @@ public class PointerAttachment extends Attachment {
return results;
}
public static List<Attachment> forPointers(List<SignalServiceDataMessage.Quote.QuotedAttachment> pointers) {
public static List<Attachment> forPointers(@Nullable List<SignalServiceDataMessage.Quote.QuotedAttachment> pointers) {
List<Attachment> results = new LinkedList<>();
if (pointers != null) {
@@ -102,7 +102,7 @@ public class PointerAttachment extends Attachment {
}
return Optional.of(new PointerAttachment(pointer.get().getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
AttachmentTable.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().orElse(0),
pointer.get().asPointer().getFileName().orElse(null),
pointer.get().asPointer().getCdnNumber(),
@@ -126,7 +126,7 @@ public class PointerAttachment extends Attachment {
SignalServiceAttachment thumbnail = pointer.getThumbnail();
return Optional.of(new PointerAttachment(pointer.getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
AttachmentTable.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
pointer.getFileName(),
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,

View File

@@ -5,7 +5,7 @@ import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentTable;
/**
* An attachment that represents where an attachment used to be. Useful when you need to know that
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
public class TombstoneAttachment extends Attachment {
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
}
@Override

View File

@@ -7,9 +7,11 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import java.util.Objects;
public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri;
@@ -51,7 +53,7 @@ public class UriAttachment extends Attachment {
@Nullable TransformProperties transformProperties)
{
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.dataUri = dataUri;
this.dataUri = Objects.requireNonNull(dataUri);
}
@Override

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.audio;
import android.content.Context;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
@@ -25,13 +26,15 @@ public class AudioRecorder {
private static final ExecutorService executor = SignalExecutors.newCachedSingleThreadExecutor("signal-AudioRecorder");
private final Context context;
private final Context context;
private final AudioRecorderFocusManager audioFocusManager;
private Recorder recorder;
private Uri captureUri;
public AudioRecorder(@NonNull Context context) {
this.context = context;
audioFocusManager = AudioRecorderFocusManager.create(context, focusChange -> stopRecording());
}
public void startRecording() {
@@ -52,6 +55,10 @@ public class AudioRecorder {
.createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
recorder = Build.VERSION.SDK_INT >= 26 ? new MediaRecorderWrapper() : new AudioCodec();
int focusResult = audioFocusManager.requestAudioFocus();
if (focusResult != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
Log.w(TAG, "Could not gain audio focus. Received result code " + focusResult);
}
recorder.start(fds[1]);
} catch (IOException e) {
Log.w(TAG, e);
@@ -70,6 +77,7 @@ public class AudioRecorder {
return;
}
audioFocusManager.abandonAudioFocus();
recorder.stop();
try {

View File

@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.audio
import android.content.Context
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.media.AudioManager
import android.media.AudioManager.OnAudioFocusChangeListener
import android.os.Build
import androidx.annotation.RequiresApi
import org.thoughtcrime.securesms.util.ServiceUtil
abstract class AudioRecorderFocusManager(val context: Context) {
protected val audioManager: AudioManager = ServiceUtil.getAudioManager(context)
abstract fun requestAudioFocus(): Int
abstract fun abandonAudioFocus(): Int
companion object {
@JvmStatic
fun create(context: Context, changeListener: OnAudioFocusChangeListener): AudioRecorderFocusManager {
return if (Build.VERSION.SDK_INT >= 26) {
AudioRecorderFocusManager26(context, changeListener)
} else {
AudioRecorderFocusManagerLegacy(context, changeListener)
}
}
}
}
@RequiresApi(26)
private class AudioRecorderFocusManager26(context: Context, changeListener: OnAudioFocusChangeListener) : AudioRecorderFocusManager(context) {
val audioFocusRequest: AudioFocusRequest
init {
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
.setAudioAttributes(audioAttributes)
.setOnAudioFocusChangeListener(changeListener)
.build()
}
override fun requestAudioFocus(): Int {
return audioManager.requestAudioFocus(audioFocusRequest)
}
override fun abandonAudioFocus(): Int {
return audioManager.abandonAudioFocusRequest(audioFocusRequest)
}
}
@Suppress("DEPRECATION")
private class AudioRecorderFocusManagerLegacy(context: Context, val changeListener: OnAudioFocusChangeListener) : AudioRecorderFocusManager(context) {
override fun requestAudioFocus(): Int {
return audioManager.requestAudioFocus(changeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE)
}
override fun abandonAudioFocus(): Int {
return audioManager.abandonAudioFocus(changeListener)
}
}

View File

@@ -21,7 +21,7 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
@@ -100,7 +100,7 @@ public final class AudioWaveForm {
if (attachment instanceof DatabaseAttachment) {
try {
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
AttachmentTable attachmentDatabase = SignalDatabase.attachments();
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
long startTime = System.currentTimeMillis();

View File

@@ -30,6 +30,7 @@ class TextAvatarDrawable(
setBounds(0, 0, size, size)
}
@Suppress("DEPRECATION")
override fun draw(canvas: Canvas) {
val width = bounds.width()
val textSize = Avatars.getTextSizeForLength(context, avatar.text, width * 0.8f, width * 0.45f)

View File

@@ -61,6 +61,9 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
Navigation.findNavController(requireView()).popBackStack()
}
override fun restoreState() {
}
override fun onMainImageLoaded() {
}

View File

@@ -81,14 +81,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
}
clearButton.visible = state.canClear
val wasEnabled = saveButton.isEnabled
saveButton.isEnabled = state.canSave
if (wasEnabled != state.canSave) {
val alpha = if (state.canSave) 1f else 0.5f
saveButton.animate().cancel()
saveButton.animate().alpha(alpha)
}
saveButton.isClickable = state.canSave
val items = state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) }
val selectedPosition = items.indexOfFirst { it.isSelected }
@@ -104,6 +97,11 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
photoButton.setOnIconClickedListener { openGallery() }
textButton.setOnIconClickedListener { openTextEditor(null) }
saveButton.setOnClickListener { v ->
if (!saveButton.isEnabled) {
return@setOnClickListener
}
saveButton.isEnabled = false
viewModel.save(
{
setFragmentResult(
@@ -125,7 +123,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
}
)
}
clearButton.setOnClickListener { viewModel.clear() }
clearButton.setOnClickListener { viewModel.clearAvatar() }
setFragmentResultListener(TextAvatarCreationFragment.REQUEST_KEY_TEXT) { _, bundle ->
val text = AvatarBundler.extractText(bundle)

View File

@@ -50,7 +50,7 @@ class AvatarPickerRepository(context: Context) {
}
fun getAvatarForGroup(groupId: GroupId): Single<Avatar> = Single.fromCallable {
val recipient = Recipient.externalGroupExact(applicationContext, groupId)
val recipient = Recipient.externalGroupExact(groupId)
if (AvatarHelper.hasAvatar(applicationContext, recipient.id)) {
try {
@@ -161,7 +161,7 @@ class AvatarPickerRepository(context: Context) {
}
fun getDefaultAvatarForGroup(groupId: GroupId): Avatar {
val recipient = Recipient.externalGroupExact(applicationContext, groupId)
val recipient = Recipient.externalGroupExact(groupId)
return getDefaultAvatarForGroup(recipient.avatarColor)
}

View File

@@ -32,7 +32,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
}
}
fun clear() {
fun clearAvatar() {
store.update {
val avatar = getDefaultAvatarFromRepository()
it.copy(currentAvatar = avatar, canSave = true, canClear = false, isCleared = true)
@@ -183,7 +183,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
private val isNewGroup: Boolean,
private val groupAvatarMedia: Media?
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val viewModel = if (groupId == null && !isNewGroup) {
SelfAvatarPickerViewModel(repository)
} else if (groupId == null) {

View File

@@ -33,7 +33,7 @@ class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
}
class Factory(private val initialText: Avatar.Text) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(TextAvatarCreationViewModel(initialText)))
}
}

View File

@@ -20,7 +20,7 @@ class VectorAvatarCreationViewModel(initialAvatar: Avatar.Vector) : ViewModel()
fun getCurrentAvatar() = store.state.currentAvatar
class Factory(private val initialAvatar: Avatar.Vector) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(VectorAvatarCreationViewModel(initialAvatar)))
}
}

View File

@@ -1,30 +1,27 @@
package org.thoughtcrime.securesms.backup
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.MessageTable
/**
* Queries used by backup exporter to estimate total counts for various complicated tables.
*/
object BackupCountQueries {
const val mmsCount: String = "SELECT COUNT(*) FROM ${MmsDatabase.TABLE_NAME} WHERE ${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.VIEW_ONCE} <= 0"
const val smsCount: String = "SELECT COUNT(*) FROM ${SmsDatabase.TABLE_NAME} WHERE ${SmsDatabase.EXPIRES_IN} <= 0"
const val mmsCount: String = "SELECT COUNT(*) FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.VIEW_ONCE} <= 0"
@get:JvmStatic
val groupReceiptCount: String = """
SELECT COUNT(*) FROM ${GroupReceiptDatabase.TABLE_NAME}
INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${GroupReceiptDatabase.TABLE_NAME}.${GroupReceiptDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID}
WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0
SELECT COUNT(*) FROM ${GroupReceiptTable.TABLE_NAME}
INNER JOIN ${MessageTable.TABLE_NAME} ON ${GroupReceiptTable.TABLE_NAME}.${GroupReceiptTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0
""".trimIndent()
@get:JvmStatic
val attachmentCount: String = """
SELECT COUNT(*) FROM ${AttachmentDatabase.TABLE_NAME}
INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${AttachmentDatabase.TABLE_NAME}.${AttachmentDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID}
WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0
SELECT COUNT(*) FROM ${AttachmentTable.TABLE_NAME}
INNER JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0
""".trimIndent()
}

View File

@@ -159,12 +159,12 @@ public class BackupDialog {
public static void showVerifyBackupPassphraseDialog(@NonNull Context context) {
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify)
.setView(view)
.setPositiveButton(R.string.BackupDialog_verify, null)
.setNegativeButton(android.R.string.cancel, null)
.show();
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify)
.setView(view)
.setPositiveButton(R.string.BackupDialog_verify, null)
.setNegativeButton(android.R.string.cancel, null)
.show();
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setEnabled(false);

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);
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), PendingIntentFlags.mutable());
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.getInstance().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,83 @@
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
@Throws(IOException::class)
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 id: ${attachment.attachmentId} len: ${attachment.length}", 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 id: ${sticker.rowId} len: ${sticker.length}", 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 avatar id: ${avatar.recipientId} len: ${avatar.length}", 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
}
}

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