Compare commits

..

629 Commits

Author SHA1 Message Date
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
Cody Henthorne
0fe0765e63 Bump version to 5.38.5 2022-05-17 14:04:33 -04:00
Cody Henthorne
6e6752cfed Updated language translations. 2022-05-17 14:04:23 -04:00
Cody Henthorne
0107e8e6eb Reduce minimum translation requirement. 2022-05-17 13:41:30 -04:00
Cody Henthorne
7fe5376772 Bump version to 5.38.4 2022-05-17 11:14:42 -04:00
Cody Henthorne
30d2d12f89 Updated language translations. 2022-05-17 11:05:31 -04:00
Cody Henthorne
98ab48f0eb Revert "Fix system UI freeze with image notifications."
This reverts commit 8f2c5d43df.
2022-05-17 11:00:55 -04:00
Cody Henthorne
a181ed0420 Revert "Always try to close the PartProvider open file pipe."
This reverts commit d97184ef60.
2022-05-17 11:00:54 -04:00
Cody Henthorne
dbddb274db Revert "Fix NPE in PartProvider."
This reverts commit 3b16a1d28c.
2022-05-17 11:00:52 -04:00
Cody Henthorne
8502badb6d Bump version to 5.38.3 2022-05-16 12:16:39 -04:00
Cody Henthorne
cb4ba1ccfe Updated language translations. 2022-05-16 12:06:36 -04:00
Cody Henthorne
3b16a1d28c Fix NPE in PartProvider. 2022-05-16 11:32:23 -04:00
Cody Henthorne
ba1473acb9 Revert "Fix Google Camera social share."
This reverts commit c078d08df7.
2022-05-16 11:02:02 -04:00
Cody Henthorne
709c866786 Revert "Fix direct sharing, again."
This reverts commit ad626fe7ee.
2022-05-16 11:02:01 -04:00
Alex Hart
ab4e5b1d7c Bump version to 5.38.2 2022-05-13 16:30:36 -03:00
Alex Hart
7b2552e8f2 Updated language translations. 2022-05-13 16:29:28 -03:00
Cody Henthorne
a501940909 Fix Payment to Help navigation. 2022-05-13 14:27:12 -04:00
Cody Henthorne
a3bbf944e5 Handle bluetooth permission crash during calls. 2022-05-13 12:39:23 -04:00
Greyson Parrelli
97d41fdd1e Small refactor of RecipientDatabase androidTests. 2022-05-13 11:39:43 -04:00
Greyson Parrelli
a9bdc1abfc Only stop the FCM foreground service if it was used. 2022-05-13 09:12:02 -04:00
Cody Henthorne
ad626fe7ee Fix direct sharing, again. 2022-05-13 08:35:39 -04:00
Cody Henthorne
d97184ef60 Always try to close the PartProvider open file pipe. 2022-05-13 08:31:23 -04:00
Alex Hart
b527b2ffb9 Bump version to 5.38.1 2022-05-12 17:30:58 -03:00
Alex Hart
468cda034a Updated language translations. 2022-05-12 17:30:58 -03:00
Cody Henthorne
8f2c5d43df Fix system UI freeze with image notifications. 2022-05-12 17:30:58 -03:00
Cody Henthorne
9bc4dfc3f6 Fix PNI crash in in group processing. 2022-05-12 17:30:58 -03:00
Greyson Parrelli
dc095c9db4 Give recipient resolves their own thread pool. 2022-05-12 17:30:58 -03:00
Greyson Parrelli
ef85b29ddf Fix keyboard icon when opening emoji keyboard. 2022-05-12 17:30:58 -03:00
Alex Hart
392a66ed59 Fix bad toolbar animations when switching to and from archive fragment. 2022-05-12 17:30:58 -03:00
Cody Henthorne
c078d08df7 Fix Google Camera social share. 2022-05-12 11:56:55 -04:00
Alex Hart
c89b818a31 Bump version to 5.38.0 2022-05-12 10:42:21 -03:00
Alex Hart
e495c25687 Updated language translations. 2022-05-12 10:42:21 -03:00
Alex Hart
3b2a3500a1 Do not send viewed receipt to gift sender after redemption. 2022-05-12 10:42:21 -03:00
clauz9
d3d9b95924 Fix navigation for creating a new pin if forgotten or skipped during registration
Co-authored-by: henry <henry.ph2@gmail.com>

Closes #12183
2022-05-12 10:42:21 -03:00
Sgn-32
12d1254d4e Update libphonenumber to 8.12.48 2022-05-12 10:42:21 -03:00
Cody Henthorne
ecc358ef40 Consolidate S3 requests into one interface. 2022-05-12 10:42:21 -03:00
Cody Henthorne
bb963f9210 Add remote megaphone. 2022-05-12 10:42:21 -03:00
Cody Henthorne
820277800b Ignore identity updates for self. 2022-05-12 10:42:21 -03:00
Cody Henthorne
14b2d12895 Reduce disk reads on main thread. 2022-05-12 10:42:21 -03:00
Greyson Parrelli
92a506e4da Add a donate megaphone for Q2 2022. 2022-05-12 10:42:21 -03:00
Cody Henthorne
12e6ebb4df Improve performance of GV2 profile fetch and mentions initialization. 2022-05-12 10:42:21 -03:00
Greyson Parrelli
c0db88960c Make FcmFetchForegroundService stop itself. 2022-05-12 10:42:21 -03:00
Alex Hart
eeb4cdf064 Add strict-mode logging for disk access on Spinner variant. 2022-05-12 10:42:21 -03:00
Greyson Parrelli
85cecbb7e9 Remove the chat colors megaphone. 2022-05-12 10:42:21 -03:00
Alex Hart
33d60ebe14 Implement proper group story reply deletion for remotely deleted group stories. 2022-05-12 10:42:21 -03:00
Greyson Parrelli
9afeb206fc Refactor FCM processing to improve use of foreground services. 2022-05-12 10:42:21 -03:00
Cody Henthorne
06a49b5d5a Force use of system settings to configure notifications on SDK30+. 2022-05-12 10:42:21 -03:00
Alex Hart
68ba3433a3 Always display donation receipts page. 2022-05-12 10:42:21 -03:00
Alex Hart
eaf36be9f6 NotificationThread migration. 2022-05-12 10:42:21 -03:00
Alex Hart
af9465fefe Add sent story syncing. 2022-05-12 10:42:21 -03:00
Alex Hart
8ca0f4baf4 Add support for replying to gift badges. 2022-05-12 10:42:21 -03:00
Winston Cooke
0c1edd6a56 Update CONTRIBUTING.md link from master to main
Fixes #12242
2022-05-12 10:42:21 -03:00
Alex Hart
df88c2fd14 Update ViewModel file template to use RxStore. 2022-05-12 10:42:21 -03:00
Alex Hart
c698bfca44 Fix currency selection disabled state. 2022-05-12 10:42:21 -03:00
Alex Hart
431f5501c6 Do not display keyboard when entering blocked users page.
Fixes #12241
2022-05-12 10:42:21 -03:00
Alex Hart
9a20447993 Add touch delegate for user avatar in conversation list view. 2022-05-12 10:42:21 -03:00
Sgn-32
049e5a1b99 Fix animation for call buttons.
Closes #12240
2022-05-12 10:42:21 -03:00
Sgn-32
4cbacc9804 Change text when blocking/unblocking unregistered recipient.
Closes #12239
2022-05-12 10:42:20 -03:00
Sgn-32
6462d053ae Add divider in ChatSettingsFragment.
Closes #12238
2022-05-11 09:29:14 -03:00
Alex Hart
0f08acbc04 Verify recipient before launching google pay sheet in badge gifting flow. 2022-05-11 09:29:14 -03:00
Alex Hart
dc5f7d0906 Add gift tab in donation receipts page. 2022-05-11 09:29:14 -03:00
Alex Voloshyn
60b20a9b8a Use shorter fog report URI in wallet 2022-05-11 09:29:14 -03:00
Alex Hart
ec361d6349 Update gift badge open animation to use anticipate interpolator. 2022-05-11 09:29:14 -03:00
Alex Hart
1f8f1d433b Add Gift badging bow. 2022-05-11 09:29:14 -03:00
Alex Hart
bc44704f54 Center currency code in selector. 2022-05-11 09:29:14 -03:00
Alex Hart
756eafe3c8 Add slide animation to conversation list to archive. 2022-05-11 09:29:14 -03:00
Alex Hart
e770241ed4 Remove story text posts feature flag. 2022-05-11 09:29:14 -03:00
Cody Henthorne
4b8729c2ae Fix story unavailable emoji render bug. 2022-05-11 09:29:14 -03:00
Alex Hart
8261e21005 Lock story viewer orientation to portrait. 2022-05-11 09:29:14 -03:00
Alex Hart
1b1bbbab7a Add multi-device sync for viewed status of redeemed gift badge. 2022-05-11 09:29:14 -03:00
Alex Hart
964d214434 Remove inset for check circle and update copy for private and group story pickers. 2022-05-11 09:29:14 -03:00
Alex Hart
e2b0079a5c Utilize sending reply instead of reply sent in story reply toast. 2022-05-11 09:29:14 -03:00
Alex Hart
158f77a634 Add thread display body and proper image for gift badges. 2022-05-11 09:29:14 -03:00
Alex Hart
1345413645 Ensure new storage id is synchronized to recipient. 2022-05-11 09:29:14 -03:00
Alex Hart
ee69895123 Bump version to 5.37.4 2022-05-11 09:23:56 -03:00
Alex Hart
f25f47654e Updated language translations. 2022-05-11 09:23:30 -03:00
Alex Hart
8f52f803cf Ensure networking is not performed on main during Subscription creation. 2022-05-11 09:09:07 -03:00
Alex Hart
82d42c03f7 Bump version to 5.37.3 2022-05-09 15:31:04 -03:00
Alex Hart
c0f8e5adbf Updated language translations. 2022-05-09 15:30:26 -03:00
Cody Henthorne
c54c73cb48 Exclude visible thread from notification shown check. 2022-05-09 12:14:30 -04:00
Cody Henthorne
02c8656b92 Fix remove from group bug. 2022-05-09 12:13:52 -04:00
Cody Henthorne
3553a28683 Bump version to 5.37.2 2022-05-06 14:52:07 -04:00
Cody Henthorne
acf4e97578 Updated language translations. 2022-05-06 14:40:09 -04:00
Cody Henthorne
5142c8c58f Fix double divider bug when payments not available. 2022-05-06 13:08:20 -04:00
Cody Henthorne
55919cba59 Add notification not showing debuglog. 2022-05-06 12:59:03 -04:00
Cody Henthorne
1a6bd3d3f2 Add VPN/metered connection status during FCM receives. 2022-05-06 11:47:57 -04:00
Jim Gustafson
100dc54292 Update to RingRTC v2.20.5 2022-05-06 10:11:21 -04:00
Alex Hart
cffbfcb957 Hide receipts item if user has none. 2022-05-06 10:01:14 -03:00
Sgn-32
f73c5dde6b Replace use of AlertDialog.Builder with MaterialAlertDialogBuilder. 2022-05-04 09:48:41 -04:00
Victor Ding
d5a466851a Use the same SmsManager to divide and send a message. 2022-05-04 09:46:19 -04:00
Greyson Parrelli
38836198a1 Bump version to 5.37.1 2022-05-02 22:38:15 -04:00
Greyson Parrelli
52429dcd33 Updated language translations. 2022-05-02 22:30:52 -04:00
Ehren Kret
fae427c09b Revert "Use shorter URLs for MOB FOG"
This reverts commit ef0c6c79cb.
2022-05-02 20:17:33 -05:00
Greyson Parrelli
e22ddb8f96 Bump version to 5.37.0 2022-05-02 15:33:58 -04:00
Greyson Parrelli
19f0722df3 Updated language translations. 2022-05-02 15:33:58 -04:00
Greyson Parrelli
921f7a70b3 Rotate the FCM foreground service flag. 2022-05-02 15:33:58 -04:00
Greyson Parrelli
bb8faebc7d Improve handling of mismatched expiry timers on messages. 2022-05-02 15:25:55 -04:00
Cody Henthorne
5ed6a05eb9 Adjust how preferred variation is handled for reaction customization. 2022-05-02 15:25:55 -04:00
Alex Hart
a4a4665aaa Implement badge gifting behind feature flag. 2022-05-02 15:25:55 -04:00
Alex Hart
5d16d1cd23 Fix story send issues due to insertion of story sends to database. 2022-05-02 15:25:55 -04:00
Rashad Sookram
38b6362b25 Fix enabling video while ringing for an audio-only call.
* Update to RingRTC v2.20.4

Co-authored-by: Jim Gustafson <jim@signal.org>
2022-05-02 15:25:55 -04:00
Ehren Kret
ef0c6c79cb Use shorter URLs for MOB FOG 2022-05-02 15:25:55 -04:00
Cody Henthorne
f10d5651f0 Fix storage sync bug for distribution lists. 2022-05-02 15:25:55 -04:00
Justin Tracey
8a2f89b4f6 Fix .onion link linkification.
Fixes #11458.
2022-05-02 15:25:55 -04:00
Alex Hart
6563ea970f Revert "Change send method for text stories to cover link previews."
This reverts commit 0f9923e2619ec21eec3f2c4a97a3cc0eb4ab5e29.
2022-05-02 15:25:55 -04:00
Greyson Parrelli
f1cb416bda Update workflow to reference main branch. 2022-05-02 15:25:55 -04:00
Greyson Parrelli
df48e5ce92 Fix pluralization possibilities for group invite string.
Fixes #12197
2022-05-02 15:25:55 -04:00
Greyson Parrelli
e710e231ad Remove notification profile megaphone. 2022-05-02 15:25:55 -04:00
Cody Henthorne
9599d3a0b6 Remove announcement group capability checks. 2022-05-02 15:25:55 -04:00
Greyson Parrelli
1fad4d4f65 Handle early read receipt sync messages. 2022-05-02 15:25:55 -04:00
Alex Hart
f57e06677b Change send method for text stories to cover link previews. 2022-05-02 15:25:55 -04:00
Rashad Sookram
f7b9942f11 Stop showing video in group calls when it isn't being forwarded. 2022-05-02 15:25:55 -04:00
Alex Hart
2f1b05f882 Start slider progress at RED. 2022-05-02 15:25:55 -04:00
Alex Hart
6650f41200 Pause voice note playback when starting an audio recording. 2022-05-02 15:25:55 -04:00
Alex Hart
2d8de03e05 Fix crash when ViewStub and contained view shared an id. 2022-05-02 15:25:55 -04:00
Alex Hart
9ffa866907 Ensure proper message ID is passed to Story viewer. 2022-05-02 15:25:55 -04:00
Alex Hart
3c0c5478b5 Fix forward sheet weirdness in full screen activities. 2022-05-02 15:25:55 -04:00
Alex Hart
aae888f5af Reduce opacity of text story hint text. 2022-05-02 15:25:55 -04:00
Alex Hart
e00a3730b4 Remove autosizing of text in story caption and bold styling. 2022-05-02 15:25:55 -04:00
Alex Hart
5c2394aa4f Add corner radius to text story creator. 2022-05-02 15:25:55 -04:00
Alex Hart
c6be273a38 Add signal connection image. 2022-04-28 15:45:52 -03:00
Alex Hart
33236ea8e6 Add retry when user resubscribes after canceling. 2022-04-28 14:33:30 -03:00
Alex Hart
a6f1e0e972 Log out charge failure for pending payment if present. 2022-04-28 09:02:37 -03:00
Jim Gustafson
fa8f8beb56 Add internal setting to disable telecom 2022-04-27 14:57:43 -07:00
Greyson Parrelli
11db59d8a1 Improve the Android 12 splash screen. 2022-04-27 14:44:00 -04:00
Greyson Parrelli
39a11ce26c Ensure message resends are called on a background thread. 2022-04-27 14:24:50 -04:00
Greyson Parrelli
8bb1b2d596 Ignore empty profile fetches in RefreshOwnProfileJob. 2022-04-27 14:15:27 -04:00
Alex Hart
3f1abe05fc Allow users to copy Subscription ID to clipboard. 2022-04-27 12:47:24 -03:00
Greyson Parrelli
08ac99b4c1 Fix crash around unbinding GenericForegroundService. 2022-04-27 10:34:04 -04:00
Greyson Parrelli
ebf2ef65e2 Add log section for the last thread dump during a possible deadlock. 2022-04-27 10:30:30 -04:00
Greyson Parrelli
8cb74fb776 Improve updates to CdsDatabase. 2022-04-26 13:59:51 -04:00
Greyson Parrelli
eccb796199 Ensure that destinationUuid is always populated. 2022-04-26 12:16:58 -04:00
Greyson Parrelli
4635a77fbc Improve logging in base identity store. 2022-04-26 12:16:58 -04:00
Greyson Parrelli
9505c3d070 Prevent failed Spinner transforms from blocking query. 2022-04-26 12:16:58 -04:00
Cody Henthorne
657a9c7b0a Add ability to reject group invite by PNI. 2022-04-26 12:16:58 -04:00
Alex Hart
e22560a794 Add dialog to nav to story or profile photo. 2022-04-26 12:15:50 -04:00
Alex Hart
c081193373 Add helper text at the bottom of the private stories list. 2022-04-26 12:15:50 -04:00
Alex Hart
d23faf4278 Always allow story error slate this->this transition. 2022-04-26 12:15:50 -04:00
Alex Hart
da7e4cefd5 Add properly tinted conversation tab icons. 2022-04-26 12:15:50 -04:00
Sgn-32
0d9a5ef9a6 Fix SMS delivery reports 2022-04-26 12:15:50 -04:00
Victor Ding
e5aea7c49e Replace non-ASCII characters in comments to their ASCII equivalent
Fixes #12201
2022-04-26 12:15:50 -04:00
Greyson Parrelli
b8c42fa57e Filter out invalid phone numbers from system contacts.
Some phones are putting UUIDs in phone number fields. Who knows why.

Fixes #12191
2022-04-26 12:15:50 -04:00
Cody Henthorne
2a086ad574 Prevent VerifiedMessages from altering self. 2022-04-26 12:15:50 -04:00
Cody Henthorne
33346d8033 Fix bug with receiving GV2 message for previously unknown group. 2022-04-26 12:15:50 -04:00
Greyson Parrelli
1446af97a2 Use newer CellService observer when possible. 2022-04-26 12:15:50 -04:00
Alex Hart
64b5dad783 Fix text story preview on incoming 1to1 replies. 2022-04-26 12:15:50 -04:00
Alex Hart
a6e7f9a4c1 Fix incorrect column in query. 2022-04-26 12:15:50 -04:00
Greyson Parrelli
70b0a120f0 Fix partial contact syncs and ignore your own contact info. 2022-04-26 12:12:17 -04:00
Jim Gustafson
4a6569fa1c Update to RingRTC v2.20.2 2022-04-26 12:12:17 -04:00
Greyson Parrelli
f5173fa6f5 Update libsignal-client to 0.16.0 2022-04-26 12:12:17 -04:00
Greyson Parrelli
5478285362 Improve contact sync for individual contacts. 2022-04-26 12:12:17 -04:00
Alex Hart
e2292dfa34 Add handling for story reply sync messages. 2022-04-26 12:12:17 -04:00
Alex Hart
17111abc72 Add support for smarter story downloads. 2022-04-26 12:12:13 -04:00
Cody Henthorne
c4bc2162f2 Bump version to 5.36.3 2022-04-26 11:55:37 -04:00
Cody Henthorne
bfd966217f Updated language translations. 2022-04-26 11:49:27 -04:00
Greyson Parrelli
797c02e893 Improve reliability of launching FCM foreground service.
We were getting weird errors around the service not calling
startForeground() that I couldn't reproduce. I figure it must be
something with how we only sometimes start the FcmFetchService in the
foreground. So I think the safest thing to do is to just use
GenericForegroundService.
2022-04-26 11:22:01 -04:00
Cody Henthorne
65372e547a Auto-decline invites to a group by a blocked user. 2022-04-25 13:41:50 -04:00
Alex Hart
2454b2e0db Fix crash when trying to access media controller after activity is destroyed and reference is nullified. 2022-04-25 10:29:30 -03:00
Alex Hart
0505a46603 Fix crash when item animation ends after we leave fragment. 2022-04-25 10:09:28 -03:00
Alex Hart
7f77cd6a22 Prevent crash when user quickly leaves the share fragment. 2022-04-25 10:04:21 -03:00
Alex Hart
efe7b3099f Bump version to 5.36.2 2022-04-22 16:50:21 -03:00
Alex Hart
26a831b49f Updated language translations. 2022-04-22 16:50:21 -03:00
Alex Hart
a3a5bb8177 Fix direct shares. 2022-04-22 16:50:21 -03:00
Alex Hart
4282f3eb6d Add explicit export to ShareActivity. 2022-04-22 16:50:21 -03:00
Greyson Parrelli
8a49db650a Do not show SMS contacts as shortcuts if we're not the SMS app. 2022-04-22 16:50:21 -03:00
Greyson Parrelli
fadd4ac61e Fix possible index out of bounds exception in ConversationAdapter.
If we're deferring to super.getItem(), then we should be guarding with
super.getItemCount().
2022-04-22 16:50:21 -03:00
Alex Hart
d0c14895d0 Fix crash when parent does not implement optional bottom sheet callback. 2022-04-22 16:50:21 -03:00
Greyson Parrelli
32ee18240b Fix crash that occurs if we don't have permission to add an account. 2022-04-22 14:32:05 -04:00
Cody Henthorne
cd10aa90cc Adjust tap area on forwarding fab. 2022-04-22 13:40:07 -04:00
Rashad Sookram
33d28c4359 Inset audio level indicator by nav bar height. 2022-04-22 12:36:05 -04:00
Greyson Parrelli
530403ec04 Updated emoji to version 14.0 2022-04-22 11:40:07 -04:00
Greyson Parrelli
f15072bc8d Fix other group update description bugs and add tests. 2022-04-22 08:32:07 -04:00
Ehren Kret
8c2db972cf Fix crash if recipient appears multiple times in group update description.
Without starting from start index, if the same recipient appears
multiple times in the recipient list, this function will crash.
2022-04-22 07:55:42 -04:00
Alex Hart
ff8f9ca81a Bump version to 5.36.1 2022-04-21 12:45:39 -03:00
Alex Hart
40991cc8e9 Updated language translations. 2022-04-21 12:45:12 -03:00
Cody Henthorne
2f551ee3f2 Do not auto-leave groups you have requested to join. 2022-04-21 11:30:13 -04:00
Alex Hart
f1ab0a05f1 Fix issue preventing stories shared element transition from starting. 2022-04-21 12:12:15 -03:00
Greyson Parrelli
fb919466de Enqueue a profile fetch to resolve identity key conflicts. 2022-04-20 19:04:24 -04:00
Greyson Parrelli
4a4cf08cd8 Do not run StorageForcePushJob if you're not registered. 2022-04-20 18:52:41 -04:00
Alex Hart
ed20c24326 Bump version to 5.36.0 2022-04-20 16:32:55 -03:00
Alex Hart
e01cbcec62 Updated language translations. 2022-04-20 16:32:55 -03:00
Rashad Sookram
04d6ccc30e Re-enable audio level indicators in calls. 2022-04-20 16:32:55 -03:00
Alex Hart
6860f96973 Add subscription retry on 402 and print out status when we think a sub is active. 2022-04-20 16:32:55 -03:00
Alex Hart
944c8530d8 Improve remote delete handling in group story threads. 2022-04-20 16:32:55 -03:00
Alex Hart
8b1552952c Stories: Pass recipients through via constructor. 2022-04-20 16:32:55 -03:00
Greyson Parrelli
dfcadde076 Ensure we enqueue a storage sync after a safety number change. 2022-04-20 16:32:55 -03:00
Cody Henthorne
55acd0f048 Auto-leave group if added by blocked user. 2022-04-20 16:32:55 -03:00
Alex Hart
820c016aad Allow quoted story to launch into viewer. 2022-04-20 16:32:55 -03:00
Alex Hart
d1d63d83dc Drop stories from inactive groups. 2022-04-20 16:32:55 -03:00
Alex Hart
7da5b2cdef Fix spinner group change description. 2022-04-20 16:32:55 -03:00
Alex Hart
442dde5c40 Keep caption when forwarding media with a body to a story. 2022-04-20 16:32:55 -03:00
Cody Henthorne
f038e81ff3 Fix long name issues in reaction and recipient bottom sheet.
Fixes #12113
2022-04-20 16:32:55 -03:00
Greyson Parrelli
32c4fcb065 Improve handling of empty profiles. 2022-04-20 16:32:55 -03:00
Alex Hart
e2703b459f Rework color selector and background. 2022-04-20 16:32:55 -03:00
Cody Henthorne
405d99fbe2 Allow keyboard switch when disabling pin reminders.
Fixes #9862
2022-04-20 16:32:55 -03:00
Evan Hahn
7b89687206 Update donation strings: "One-time", not "One Time". 2022-04-20 16:32:55 -03:00
Alex Hart
d74f1a386c Set story text placeholder alpha to 60 percent. 2022-04-20 16:32:55 -03:00
Alex Hart
b041ed1510 Ensure delivery receipts are sent for 1:1 story replies and reactions. 2022-04-20 16:32:55 -03:00
Alex Hart
3426556a51 Disable group private replies. 2022-04-20 16:32:54 -03:00
Greyson Parrelli
e2cb535f3f Make names in group update descriptions tappable. 2022-04-20 16:32:54 -03:00
Alex Hart
3b17a41415 Send actual quote author in story direct reply. 2022-04-20 16:32:54 -03:00
Alex Hart
631720f111 Ensure direct replies respect disappearing message timeout. 2022-04-20 16:32:54 -03:00
Cody Henthorne
ab031d3dad Add internal setting to clear keep longer logs. 2022-04-19 08:17:21 -04:00
Alex Hart
6101048f07 Update donor badge strings. 2022-04-18 16:38:06 -03:00
Alex Hart
115f7063d5 Add support and tracking of ChargeFailure in ActiveSubscription. 2022-04-18 16:37:12 -03:00
Alex Hart
159d67ec59 Fix crash for non-story replies. 2022-04-18 13:26:28 -03:00
Greyson Parrelli
e09ce4c820 Use foreground services to process notification when appropriate.
Right now, the only condition is once every 3 minutes on Android 12.

This is ok because Android 12 will allow us (once every 2 minutes or
so) to start a foreground service, and it won't show it for the first 10
seconds. So we can kind of do it without any visual penalty.
2022-04-18 11:27:32 -04:00
Alex Hart
8cfc013960 Fix issue where story viewer would get stuck. 2022-04-18 10:24:54 -03:00
Alex Hart
a436c46cb2 Remove label from remote deleted story reactions. 2022-04-18 09:43:59 -03:00
Alex Hart
893be51810 Allow 1:1 replies to increment thread unread counter. 2022-04-18 09:40:05 -03:00
Greyson Parrelli
97b5a49e36 Update default state for whether legacy passwords are disabled.
This was a feature that was removed from the app over 4.5 years ago.
The value should have been manually set to false when they set a
password, meaning that it should be safe to set the default to true.

Fixes #10367
2022-04-17 10:12:44 -04:00
Alex Hart
043f06e188 Prevent sending videos over 30s in length to a story. 2022-04-15 16:07:15 -04:00
Alex Hart
fa13b464f8 Fix elongated message bubbles when intentional newlines are present. 2022-04-15 16:07:15 -04:00
Alex Hart
bfaaf20fd9 Fix image editor outlining on Android 12+. 2022-04-15 16:07:15 -04:00
Peter Thatcher
2f97b80b9c Add internal setting for call bandwidth mode. 2022-04-15 16:07:15 -04:00
Alex Hart
eee9c967fa Fix overlapping text issue in review cards. 2022-04-15 16:07:15 -04:00
Alex Hart
515981c044 Add horizontal margins to donation receipt PNG. 2022-04-15 16:07:15 -04:00
Alex Hart
a06528e5e1 Always notifyIfReady for each boolean change. 2022-04-15 16:07:14 -04:00
Alex Hart
98194c854a Add blur and adjust layout for story error slate. 2022-04-15 16:07:14 -04:00
Alex Hart
2d60a88a75 Fix crossfade target aspect ratio. 2022-04-15 16:07:14 -04:00
Alex Hart
c3e7d6c74c Fix bug causing cancellation of dialog fragment. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
8da66bc789 Fix corner case in story distribution list syncing. 2022-04-15 16:07:14 -04:00
Alex Hart
9ceb5b2e85 Fix view-off-main bug in Landing fragment. 2022-04-15 16:07:14 -04:00
Alex Hart
17b8e086c9 Fix crash when trying to view 1:1 conversation with reaction quote. 2022-04-15 16:07:14 -04:00
Alex Hart
9a097d113d Update share interstitial to use proper title. 2022-04-15 16:07:14 -04:00
Alex Hart
46ca1e16bb Fix crash when trying to add a link to a text post. 2022-04-15 16:07:14 -04:00
Alex Hart
d4d3124a90 Prevent flicker of user avatar in MyStories when moving between tabs. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
35a9fddbb2 Add basic support for receiving messages at your PNI.
We haven't implemented merging yet, so this is still very basic, but it
"works".
2022-04-15 16:07:14 -04:00
Alex Hart
41e417ff0b Add proper interpolator and duration to chrome show/hide. 2022-04-15 16:07:14 -04:00
Alex Hart
f6614c1174 Set story viewer background exit fade duration to 100ms. 2022-04-15 16:07:14 -04:00
Alex Hart
9136bcf5e8 Add radius animator to cross fade when launching story viewer. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
7c156d10d6 Keep active table selected in Browse page in Spinner. 2022-04-15 16:07:14 -04:00
Alex Hart
3372d942ec Swap out outlinethumbnailview for shapeableimageviews in mystories. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
7fc9876b1e Start transaction earlier in backup restore.
Fixes #12159
2022-04-15 16:07:14 -04:00
Alex Hart
cff62e9528 Add 12dp margin to top of stories context menu. 2022-04-15 16:07:14 -04:00
Alex Hart
24f59b0a17 Hide bottom nav when viewing archived conversations. 2022-04-15 16:07:14 -04:00
Alex Hart
0a07800eba Fix caption sending for outgoing image and video stories. 2022-04-15 16:07:14 -04:00
Alex Hart
c863e9ed4d Fix crash when trying to create a text story. 2022-04-15 16:07:14 -04:00
Alex Hart
523537cf05 Enable sharing to stories and refactor share activity. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
fd4543ffe0 Improve speed of many SMS/MMS queries by removing unnecessary attachment join. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
83b0309f23 Fix bug in Spinner where some query history items didn't work. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
5cabe5ecfa Default the query history to hidden in Spinner. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
fae3004512 Fix issue where you could have multiple context menus.
Fixes #12149
2022-04-15 16:07:14 -04:00
Alex Hart
e143c47c25 Fix crash and icon change issue with shared element transition. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
27c3fca324 Change ContactRecordProcess to merge identity key/state as a group. 2022-04-15 16:07:14 -04:00
Alex Hart
26d637cafc Add explicit themes to fabs. 2022-04-15 16:07:14 -04:00
Alex Hart
03e8fe9f27 Migrate all internal shares to MultiselectForwardFragment. 2022-04-15 16:07:14 -04:00
Artem Varaksa
23939aeee3 Removed unused string and the variable that was used in it.
Close #12146
2022-04-15 16:07:14 -04:00
clauz9
d7b793ce4c Fade out fab buttons and megaphone when entering action mode or search.
Closes #12112
2022-04-15 16:07:14 -04:00
Greyson Parrelli
d3096c56cb Basic client usage of CDSHv2.
This provides a basic (read: useful-for-development-yet-broken) client
usage of CDSHv2.
2022-04-15 16:07:14 -04:00
clauz9
b0e7b49056 Remove redundant exit animation.
Fixes #12119
2022-04-15 16:07:14 -04:00
Cody Henthorne
2f0f26c328 Add story send multi-send, error, and improved SNC states. 2022-04-15 16:07:14 -04:00
Alex Hart
7f2f5a182f Add shared element transition for camera fab. 2022-04-15 16:07:14 -04:00
Alex Hart
33b88796e8 Simplify layout and do not display until data is loaded to prevent flashing. 2022-04-15 16:07:14 -04:00
Greyson Parrelli
31e4db2186 Bump version to 5.35.3 2022-04-15 16:06:11 -04:00
Greyson Parrelli
76ad7866ec Updated language translations. 2022-04-15 16:05:41 -04:00
Rashad Sookram
e9804eccbb Disable audio level indicator in calls. 2022-04-15 15:45:50 -04:00
Rashad Sookram
d62d0efb1d Fix screen pulsing when your video is disabled. 2022-04-15 11:44:49 -04:00
Greyson Parrelli
3ec9cd1244 Bump version to 5.35.2 2022-04-13 11:56:41 -04:00
Greyson Parrelli
bd5f48f193 Updated language translations. 2022-04-13 11:56:22 -04:00
Alex Hart
98fc3e5b0b Log warning instead of throwing NPE for voice note controller. 2022-04-13 12:37:24 -03:00
Greyson Parrelli
d06c633dc4 Fix full text search filter.
Closes #12158
2022-04-13 10:58:16 -04:00
Greyson Parrelli
d401386e2d Bump version to 5.35.1 2022-04-11 20:44:10 -04:00
Greyson Parrelli
8f0f9e64b9 Updated language translations. 2022-04-11 20:43:47 -04:00
Greyson Parrelli
bd5ac85ac0 Fix old DB migration.
A cautionary tale that serves as a reminder to never call external code
during a migration...

Fixes #12147
2022-04-11 20:38:32 -04:00
Greyson Parrelli
417070e957 Prevent possible deadlock in identity cache. 2022-04-11 12:30:18 -04:00
Greyson Parrelli
a92638e897 Fix possible threading issue in RetrieveProfileJob. 2022-04-11 12:00:01 -04:00
Rashad Sookram
08abe890ff Adjust audio levels animation. 2022-04-11 11:56:46 -04:00
Cody Henthorne
66b6420f21 Revert "Fix overlapping text with voice notes."
This reverts commit dabd131222.
2022-04-11 11:54:29 -04:00
Rashad Sookram
b21bd5a01e Ensure PiP view is animated to its final position. 2022-04-08 17:34:50 -04:00
Cody Henthorne
d11e8ec04b Bump version to 5.35.0 2022-04-08 12:29:29 -04:00
Cody Henthorne
3e5fe0f1cb Updated language translations. 2022-04-08 12:22:37 -04:00
Alex Hart
b65d62e065 Update prioritization of donation error bottom sheets. 2022-04-08 12:19:27 -04:00
Alex Hart
fc55be0916 Stop voice note on video playback. 2022-04-08 12:19:27 -04:00
Alex Hart
a87aa0fbe2 Don't keep around shortcuts for archived chats. 2022-04-08 12:19:27 -04:00
Alex Hart
a44a105cbc Add ability to copy text slides in full. 2022-04-08 12:19:27 -04:00
Alex Hart
c4817ac017 Allow generic links to be sent as stories. 2022-04-08 12:19:27 -04:00
Cody Henthorne
65835606cc Fix reply UX on reply disabled 1:1 stories. 2022-04-08 12:19:27 -04:00
Cody Henthorne
ff26922afb Tweak private story reaction UI. 2022-04-08 12:19:27 -04:00
Alex Hart
a894ba7a51 Implement cross-fade for story thumb shared element animation. 2022-04-08 12:19:26 -04:00
Cody Henthorne
cb63fe600c Fix react with any closing for story direct replies. 2022-04-08 12:19:26 -04:00
Jim Gustafson
2d6146351d Update to RingRTC v2.20.1 2022-04-08 12:19:26 -04:00
Alex Hart
e5953b25e1 Disallow fling gesture when we are translating for on back. 2022-04-08 12:19:26 -04:00
Alex Hart
6354cb194c Update ordering query to display content in expected order. 2022-04-08 12:19:26 -04:00
Cody Henthorne
8d6beb92cb Reply sheet polish. 2022-04-08 12:19:26 -04:00
Cody Henthorne
bb5edccf34 Update view count in My Story view. 2022-04-08 12:19:26 -04:00
Cody Henthorne
6d86b25acd Improve story contact search. 2022-04-08 12:19:26 -04:00
Alex Hart
04677d21bb Push repository calls to background. 2022-04-08 12:19:26 -04:00
Alex Hart
20022b88fc Fix issue where if no stories exist we would never display. 2022-04-08 12:19:26 -04:00
Cody Henthorne
3088d7f182 Adjust quote view colors. 2022-04-08 12:19:26 -04:00
Alex Hart
ce8dafd33d Start align text when displaying in smallest size otherwise center. 2022-04-08 12:19:26 -04:00
Alex Hart
6054285ddb Add description to story link previews. 2022-04-08 12:19:26 -04:00
Alex Hart
dabea5169b Fix opening long messages. 2022-04-08 12:19:26 -04:00
Alex Hart
7fb5ceeda4 Allow hidden story viewing. 2022-04-06 14:37:25 -03:00
Cody Henthorne
dc6fd8be7f Fix story reply crash and tweak UI. 2022-04-06 13:17:33 -04:00
Alex Hart
c271b9c2de Prevent multiple clicks when accessing the viewer. 2022-04-06 12:38:43 -04:00
Alex Hart
6fb6092a6b Implement a cache for faster typeface resolution. 2022-04-06 12:38:43 -04:00
Alex Hart
46bb64ad24 Display reply icon if you responded 1:1 to the displayed story. 2022-04-06 12:38:43 -04:00
Alex Hart
bcd16ce296 Lock orientation when creating a text post. 2022-04-06 12:38:43 -04:00
Alex Hart
07ec14d5c4 Fix issue where reaction animation would only play first time. 2022-04-06 12:38:43 -04:00
Alex Hart
d716416d1d Prevent caption from swallowing taps if no overlay needed. 2022-04-06 12:38:43 -04:00
Alex Hart
343871ed8b Use unread thread count for bottom bar. 2022-04-06 12:38:43 -04:00
Alex Hart
aa60247e42 Add rounded corners back to secondary story. 2022-04-06 12:38:43 -04:00
Alex Hart
283e3e99a5 Adjust badge positioning on stories landing items. 2022-04-06 12:38:43 -04:00
Alex Hart
ca79bdb16b Preserve tab state between configuration changes. 2022-04-06 12:38:43 -04:00
Alex Hart
a75d2cfa34 Adjust viewer ordering to match landing page. 2022-04-06 12:38:43 -04:00
Cody Henthorne
1746f37276 Use smarter scrolling for group story replies. 2022-04-06 12:38:43 -04:00
Rashad Sookram
73f32868a2 Display audio levels in 1:1 calls. 2022-04-06 12:38:43 -04:00
Cody Henthorne
dabd131222 Fix overlapping text with voice notes. 2022-04-06 12:38:43 -04:00
Greyson Parrelli
612c6db6db Rename some CDS-related classes. 2022-04-06 12:38:43 -04:00
Cody Henthorne
c56ef33833 Fix resend after safety number change in groups or distribution lists. 2022-04-06 12:38:43 -04:00
Alex Hart
2253e25ae1 Consolidate toolbar_basic usage to single location. 2022-04-06 12:38:43 -04:00
Alex Hart
adb24d480a Remove access modifier from ChatColors constructor. 2022-04-06 12:38:43 -04:00
Cody Henthorne
9fb1dcf28f Fix overlapping text with remote delete. 2022-04-06 12:38:43 -04:00
Cody Henthorne
bba36a5724 Keep gif search open when viewing a result. 2022-04-06 12:38:43 -04:00
Alex Hart
fa515be258 Hide tab bar during multiselect. 2022-04-06 12:38:43 -04:00
Cody Henthorne
be241524db Fix font networking main thread crash. 2022-04-06 12:38:43 -04:00
Cody Henthorne
bb66c3fa68 Fix story views not using entire bottom sheet space. 2022-04-06 12:38:43 -04:00
Greyson Parrelli
a32d5bef20 Refactor more ContactDiscovery code. 2022-04-06 12:38:43 -04:00
Greyson Parrelli
d409278dd5 Do not allow emoji in image editing if device doesn't support it. 2022-04-06 12:37:43 -04:00
Alex Hart
3328e43a40 Add initial shared element transition between conversation list and stories. 2022-04-06 12:37:43 -04:00
Alex Hart
678e832058 Update stories camera fab coloring. 2022-04-06 12:37:43 -04:00
Alex Hart
2c341f450f Add finalized private story icons. 2022-04-06 12:37:43 -04:00
Alex Hart
5854074d4a Update my stories item and landing page empty notice. 2022-04-06 12:37:43 -04:00
Alex Hart
dbe186248d Rework sizing on landing page. 2022-04-06 12:37:43 -04:00
Alex Hart
0504161b04 Change stories camera fab icon to outline. 2022-04-06 12:37:43 -04:00
Alex Hart
9748f1cff8 Add padding to context menus in story landing and my story. 2022-04-06 12:37:43 -04:00
Alex Hart
6a061ed52c Remove stroke from story thumbnail. 2022-04-06 12:37:43 -04:00
Alex Hart
102d58502a Fix bad layout of group story text replies. 2022-04-06 12:37:43 -04:00
Alex Hart
477698f917 Use media forward sheet when forwarding in Media preview. 2022-04-06 12:37:43 -04:00
Alex Hart
d865b5d7b5 Color the send button properly for insecure chats. 2022-04-06 12:37:43 -04:00
Alex Hart
eeaf6df925 Change wording of receipts button. 2022-04-06 12:37:43 -04:00
Alex Hart
95abca4e03 Dismiss after pressing don't show this again. 2022-04-06 12:37:43 -04:00
Alex Hart
c5906b6f3a Fix background color on forward sheet bottom bar. 2022-04-06 12:37:43 -04:00
Alex Hart
91c581b475 Do not process story records if capability doesn't support it. 2022-04-06 12:37:43 -04:00
Fumiaki Yoshimatsu
47760867d5 Adds additional padding to the bottom of the line so the following line wouldn't overlap the previous line.
Fixes a bug [reported by Salt505 in the beta forum](https://community.signalusers.org/t/beta-feedback-for-the-upcoming-android-5-26-release/38629/163).
2022-04-06 12:37:43 -04:00
Alex Hart
5612a5d9e4 Fix issue where all forwarded MMS media would try to send as a secure message. 2022-04-06 12:37:43 -04:00
Alex Hart
4c462bd75a Enforce L1 media restrictions on link preview thumbnails.
Co-Authored-By: Alexandre Erwin Ittner <110642+ittner@users.noreply.github.com>
2022-04-06 12:37:43 -04:00
Cesar Valiente
092b30f64f Utilize isSeparating for better foldable device support. 2022-04-06 12:37:43 -04:00
Greyson Parrelli
b34ca8ca2f Improve handling of unknown IDs in storage service. 2022-04-06 12:37:43 -04:00
clauz9
e2c54eef77 Filter out some Base64 encoded status messages from search. 2022-04-06 12:37:43 -04:00
Fumiaki Yoshimatsu
2a2c27edef Remove unnecessary marginEnd attribute. 2022-04-06 12:37:43 -04:00
Rashad Sookram
ec92d5ddb7 Display audio levels for each participant in group calls. 2022-04-06 12:37:43 -04:00
Alex Hart
a9f208153c Fix artifacting corners on landing page. 2022-04-06 12:37:43 -04:00
Alex Hart
d9ffd67f36 Update My Stories logic when user has not sent to a distribution list. 2022-04-06 12:37:43 -04:00
Alex Hart
19861ef0d1 Implement specification testing for StoryViewerViewModel. 2022-04-06 12:37:43 -04:00
Alex Hart
469879c211 Implement proper story viewer ordering. 2022-04-06 12:37:43 -04:00
clado
157198fd17 Add content descriptions for incoming call buttons.
Fixes #11995
2022-04-06 12:37:43 -04:00
Greyson Parrelli
0d61b8db38 Fixed threading issues with Spinner recent queries. 2022-04-06 12:37:43 -04:00
Greyson Parrelli
593334456a Add a local query history to Spinner. 2022-04-06 12:37:43 -04:00
Greyson Parrelli
98b9cc23e4 Add extension functions to improve writability of database queries. 2022-04-06 12:37:43 -04:00
Alex Hart
3e42c044b8 Add RxStore and StoryViewerPage forward navigation. 2022-04-06 12:37:43 -04:00
Cody Henthorne
11c3ea769e Fix emoji keyboard bugs and group story replies. 2022-04-06 12:37:43 -04:00
Sgn-32
b8bb2e234b Elimination of country code 0 in Delete account 2022-04-06 12:37:43 -04:00
Cody Henthorne
87b00bb156 Fix various story reply bottom sheet issues. 2022-04-06 12:37:43 -04:00
Alex Hart
3da2fc4d9b Clear storage keys for deleted distribution lists. 2022-04-06 12:37:43 -04:00
Cody Henthorne
972ab9b368 Process incomming story views even if read receipts are disabled. 2022-04-06 12:37:43 -04:00
Alex Hart
c359b0134a Implement StoryDistributionListRecord and processing. 2022-04-06 12:37:43 -04:00
Alex Hart
2cd7462573 Add long-press action to mystories items for helpful debugging info. 2022-04-06 12:37:43 -04:00
Alex Hart
2a7d515932 Add handler to My Stories row to open my stories
If there are sent group stories.
2022-04-06 12:37:43 -04:00
Alex Hart
1bb04035ab Update playback to match specifications. 2022-04-06 12:37:43 -04:00
Alex Hart
267efb0763 Start viewer when clicking on story ring. 2022-04-06 12:37:43 -04:00
Alex Hart
0ef215dfc5 Size story bottomsheets to 60 percent height. 2022-04-06 12:37:43 -04:00
Cody Henthorne
50bea8140f Fix story reply mention fragment from taking over screen. 2022-04-06 12:37:43 -04:00
Chris Eager
086e3ed4ec Update message reporting to use sender ACI instead of E164.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2022-04-06 12:37:43 -04:00
Cody Henthorne
b02539684a Fix unresolved authors of story replies. 2022-04-06 12:37:43 -04:00
Alex Hart
b9747607ad Add blur to background for non 9:16 story content. 2022-04-06 12:37:43 -04:00
Cody Henthorne
284a6ae667 Add defaults for script/text font pairings and guessing of script based on body contents.
Co-authored-by: Alex Hart <alex@signal.org>
2022-04-06 12:37:43 -04:00
Alex Hart
116e711f1a Add logging for when we do nothing during a keep-alive job. 2022-04-06 12:36:32 -04:00
Cody Henthorne
7aeb641036 Include mentions on incoming story replies. 2022-04-06 12:36:32 -04:00
Fumiaki Yoshimatsu
e4f69c0b6f Add padding to the toolbar in media preview activities.
Fixes #10298
2022-04-06 12:36:32 -04:00
Sgn-32
48d7228ae7 Replace use of AlertDialog.Builder with MaterialAlertDialogBuilder. 2022-04-06 12:36:32 -04:00
Alex Hart
ae28df901f Hide view once for story first sending. 2022-04-06 12:36:32 -04:00
Alex Hart
f17f45f277 Fix bad check for story context. 2022-04-06 12:36:32 -04:00
Alex Hart
2b15fc2966 Do not drop group stories if not profile sharing with sender. 2022-04-06 12:36:32 -04:00
Alex Hart
76a9342afa Safety number check and profile refresh for group sends. 2022-04-06 12:36:32 -04:00
Greyson Parrelli
14849d6e45 Fix case-insensitive queries for groups with non-ASCII characters.
Fixes #11464
2022-04-06 12:36:32 -04:00
Greyson Parrelli
77ea2deada Move more util classes to core-util. 2022-04-06 12:36:32 -04:00
Greyson Parrelli
390b7ff834 Convert SqlUtil to Kotlin. 2022-04-06 12:36:32 -04:00
Greyson Parrelli
0e4187b062 Use existing contact type for our linked entry. Add test to sample app.
Fixes #9431
Closes #9434

Co-authored-by: swatts <github@stargw.net>
2022-04-06 12:36:32 -04:00
Cody Henthorne
4098f77e08 Bump version to 5.34.10 2022-04-06 12:18:57 -04:00
Cody Henthorne
a60eed35fe Updated language translations. 2022-04-06 12:18:10 -04:00
Cody Henthorne
904215fe38 Fix font networking main thread crash. 2022-04-06 12:04:55 -04:00
Greyson Parrelli
6376642d38 Pre-resolve recipient needed for group messages in the conversation list. 2022-04-06 11:13:56 -04:00
Cody Henthorne
6b36a446f0 Bump version to 5.34.9 2022-04-05 10:12:35 -04:00
Cody Henthorne
95ee7f5c00 Updated language translations. 2022-04-05 10:08:22 -04:00
Greyson Parrelli
b109effc94 Prevent possibility of recursively enqueuing early message jobs. 2022-04-04 19:26:08 -04:00
Alex Hart
44efda8318 Bump version to 5.34.8 2022-03-31 17:09:07 -03:00
Alex Hart
e6e1b6d746 Updated language translations. 2022-03-31 17:08:31 -03:00
Greyson Parrelli
e303cbcc22 Fix some toolbar theming issues. 2022-03-31 14:06:37 -04:00
Greyson Parrelli
c67aed5b65 Keep some crucial local logs (like crashes) for longer. 2022-03-30 16:32:45 -04:00
Greyson Parrelli
0278882c30 Prevent possible crash while reading contacts cursor. 2022-03-30 16:17:15 -04:00
Alex Hart
42ccd638bd Bump version to 5.34.7 2022-03-30 12:19:36 -03:00
Alex Hart
e11577bd23 Updated language translations. 2022-03-30 12:15:24 -03:00
Greyson Parrelli
3c0b87bbca Fix possible backup crash due to foreign key constraint. 2022-03-30 09:56:26 -04:00
Christian Clauss
84a61b01ca Fix syntax error in apntool.py
Old-style exceptions are syntax errors in Python 3 while new-style exceptions work as expected on both Python 2 and Python 3.

Closes #10622
2022-03-30 09:39:34 -04:00
Greyson Parrelli
c149e008fd Update apostrophes in code comments. 2022-03-30 09:37:12 -04:00
Greyson Parrelli
c7a345eb0b Ignore inbound SMS/MMS from yourself. 2022-03-29 18:39:46 -04:00
Sgn-32
348b6e9742 Enable hyphenation on signal bottom action bar item title.
Closes #11896
2022-03-29 18:21:25 -04:00
Greyson Parrelli
003b3e02e4 Add ability to view your own profile photo fullscreen.
From the profile management screen.

Fixes #11033
2022-03-29 18:21:16 -04:00
Fumiaki Yoshimatsu
5b668d7931 Refresh the storage managment title on long-click.
Fixes #9698
Closes #10021
2022-03-29 18:21:08 -04:00
Greyson Parrelli
87748fa80c Use an inset ripple for contact list items.
Closes #10786

Co-authored-by: Thore Goebel <hello@thore.io>
2022-03-29 18:20:58 -04:00
Greyson Parrelli
ad0482fb5b Bump version to 5.34.6 2022-03-29 13:58:01 -04:00
Greyson Parrelli
12ceb1cb32 Updated language translations. 2022-03-29 13:57:18 -04:00
Greyson Parrelli
9ec2c5da52 Show some buttons in media preview toolbar if there's room.
Fixes #11661
2022-03-29 10:43:04 -04:00
Greyson Parrelli
f742d34588 Remove unnecessary package exclusions.
Fixes #9540
2022-03-29 10:27:24 -04:00
Cody Henthorne
4d8e058d33 Allow hyphenation of enter sms code buttons. 2022-03-29 10:13:32 -04:00
Cody Henthorne
77ba6e0f7b Enable telecom integration for internal users. 2022-03-29 10:08:04 -04:00
Fumiaki Yoshimatsu
76a0e5c851 Allow search view to be full width of the screen in landscape.
Fixes #10299
Closes #10321
2022-03-29 10:05:45 -04:00
Greyson Parrelli
f3096cc24c Use more direct language in PIN reminder toast.
Fixes #10207
2022-03-29 09:37:20 -04:00
Greyson Parrelli
49957e1d95 Prevent changing disappearing message timer for blocked users.
Fixes #10973
2022-03-29 09:32:46 -04:00
Cody Henthorne
2f5cb5f090 Add story distribution list deduplication handling. 2022-03-28 19:43:42 -04:00
Ehren Kret
ba394e1021 Use notifyDataSetChanged for potentially structural modification.
This has the potential to change the structure of the result set
displayed in the recycler view. Thus the functions that tell it the
structure changed need to be called. For an immediate fix, changing
this back to notifyDataSetChanged seems to resolve the crash.
2022-03-28 19:22:02 -04:00
Greyson Parrelli
7611c64493 Do not bidi-isolate all-ASCII strings.
Fixes #11630
2022-03-28 19:07:21 -04:00
Greyson Parrelli
231248d20a Update android test orchestrator to 1.4.1 2022-03-28 19:07:21 -04:00
Alex Hart
a3a79fc58d Adjust text story button protections. 2022-03-28 19:07:21 -04:00
Alex Hart
6476e585c4 Fix cast in story landing fragment. 2022-03-28 19:07:21 -04:00
Greyson Parrelli
fd2961710d Fix androidTests. 2022-03-28 19:07:21 -04:00
Greyson Parrelli
7f4ab67f98 Fix timing issue with receipt updates. 2022-03-28 19:07:21 -04:00
Greyson Parrelli
b9ce38b85b Don't allow KEEP_LONGER logs to get too large. 2022-03-28 19:07:21 -04:00
Greyson Parrelli
50d5658add Fix possible NPE when backing out of conversation. 2022-03-28 19:07:21 -04:00
Greyson Parrelli
72777bc6cd Disallow some unicode sequences in link previews. 2022-03-28 19:07:21 -04:00
Rashad Sookram
f2046c3c05 Vibrate when entering call reconnecting state. 2022-03-28 19:07:21 -04:00
Sgn-32
745dfc3fbb Fix translation issue with media preview.
Fixes #12072
Closes #12092
2022-03-28 19:07:21 -04:00
Greyson Parrelli
cb77165b53 Fix issue where search results could flicker.
Shoutout to @clauz9 for the help in researching this bug!
2022-03-28 19:07:21 -04:00
Alex Hart
bde4700e87 Hide new user entry in story recipient selection. 2022-03-28 19:07:21 -04:00
Alex Hart
e58cea9a26 Fix bad use of toString in StripeApi. 2022-03-28 19:07:21 -04:00
Alex Hart
556dc0d1ec Show story rings for self and if you sent a story to a group. 2022-03-28 19:07:21 -04:00
Alex Hart
8c1ddcf1c0 Fix issue where thumb resource wasn't set to null after clear. 2022-03-28 19:07:21 -04:00
Alex Hart
2549c1f97d Add placeholder for text post thumbs. 2022-03-28 19:07:21 -04:00
Alex Hart
5faa497821 Get receipt credential presentation BEFORE recording receipt so that the retry does not add another receipt. 2022-03-28 19:07:21 -04:00
Alex Hart
d7a7e72c3a Fix flashing when entering text story. 2022-03-28 19:07:21 -04:00
Alex Hart
af1701e6fa Add HTTPS scheme when user enters a web address. 2022-03-28 19:07:21 -04:00
Rashad Sookram
32d1cc7d54 Recreate MainActivity on language change. 2022-03-28 19:07:20 -04:00
Alex Hart
783a615c07 Adjust text story creation layout size. 2022-03-28 19:07:20 -04:00
Alex Hart
65bfee6eba Display total unread messages including mark as unread instead of unread thread count. 2022-03-28 19:07:20 -04:00
Greyson Parrelli
8d4419705b Update to libsignal-client 0.15.0 2022-03-28 19:07:20 -04:00
Alex Hart
6c3baf229c Fix broadcast send when user sends to both story and non-story. 2022-03-28 19:07:20 -04:00
Rashad Sookram
6e9a6283fc Fix layout for long story replies. 2022-03-28 19:07:20 -04:00
Alex Hart
5b3899237b Trampoline call to generate preview if view is not laid out. 2022-03-28 19:07:20 -04:00
Greyson Parrelli
dddf830e47 Move system contact interactions into their own module. 2022-03-28 19:07:20 -04:00
Alex Hart
fd930d0b1d Conversation tab bar animations. 2022-03-28 19:07:20 -04:00
Greyson Parrelli
2b5d65ae04 Revert "Update to libsignal-client 0.15.0"
This reverts commit 3d5f04eba757563dd92366d994a96cf323b8d540.
2022-03-28 19:07:20 -04:00
clauz9
2ebaa04c2f Remove leftover code in SearchUtil.
Closes #12090
2022-03-28 19:07:20 -04:00
Jordan Rose
1e316ea19f Update to libsignal-client 0.15.0 2022-03-28 19:07:20 -04:00
Cody Henthorne
ac9257ec1c Revert "Track inconsistencies between new and old network availability for internal users."
This reverts commit 007975e7da.
2022-03-28 19:07:20 -04:00
Alex Hart
9b83c5e283 Ensure we do not send captions for non-story messages. 2022-03-28 19:07:20 -04:00
Alex Hart
a7a4972013 Check if there is an attachment available before trying to send it to a story. 2022-03-28 19:07:20 -04:00
Alex Hart
f6f4e6fde7 Add animations to camera toggle. 2022-03-28 19:07:20 -04:00
Alex Hart
e9160c2449 Suppress multiple clicks on tap to add. 2022-03-28 19:07:20 -04:00
Alex Hart
b0b1029d0f Add transition fixes and improvements. 2022-03-28 19:07:20 -04:00
Rashad Sookram
72b3a0555d Improve transition back to creation fragment. 2022-03-28 19:07:20 -04:00
Greyson Parrelli
135fde68c1 Migrate some cursor utils to core-util. 2022-03-28 19:07:20 -04:00
Alex Hart
954e45ed97 Fix capitalization retention for text stories. 2022-03-28 19:07:20 -04:00
Alex Hart
2b3f16d3ad Material3 Bottom Bar styling and fab for Stories only. 2022-03-28 19:07:20 -04:00
Alex Hart
6820b84921 Story Viewer shared element transition. 2022-03-28 19:07:20 -04:00
Cody Henthorne
6a5f5f4ffa Show verified badge on Note to Self. 2022-03-28 19:07:20 -04:00
Rashad Sookram
19381342b3 Update UI for replies to unavailable stories. 2022-03-28 19:07:20 -04:00
Greyson Parrelli
c2627dda8d Migrate contact interactions to SystemContactsRepository. 2022-03-28 19:07:20 -04:00
Alex Hart
db309b7930 Implement slide to close in Story viewer. 2022-03-28 19:07:20 -04:00
Cody Henthorne
403958fed3 Handle 1:1 call reconnecting events. 2022-03-28 19:07:20 -04:00
Cody Henthorne
3af53f2089 Allow ringrtc to decided how to respond to send message failure. 2022-03-28 19:07:20 -04:00
Cody Henthorne
e472760d92 Fix MessageCountsViewModel memory leak. 2022-03-28 19:07:20 -04:00
Alex Hart
b04acd8ae0 Add caption to outgoing stories. 2022-03-28 19:07:20 -04:00
Cody Henthorne
6890973ce8 Enforce limit for total number of blocked requests. 2022-03-28 19:07:20 -04:00
Alex Hart
b3d9a85fa2 Fix button alignment. 2022-03-28 19:07:20 -04:00
Greyson Parrelli
9f027ed584 Ensure unidentified access is correct when fetching own profile. 2022-03-28 19:07:20 -04:00
Greyson Parrelli
8fb598e60a Restart app after refreshing remote config via internal settings. 2022-03-28 19:07:20 -04:00
Greyson Parrelli
2edaba39a0 Fix support for PniIdentity sync message. 2022-03-28 19:07:20 -04:00
clauz9
b0be7effe8 Use notifyItemRangeChange in ConversationListFragment onResume().
Closes #12085
2022-03-28 19:07:20 -04:00
Greyson Parrelli
142979ce93 Add job to clean up early message receipts. 2022-03-28 19:07:20 -04:00
Rashad Sookram
093dd7c62c Update Material Design Components to 1.5.0. 2022-03-28 19:07:20 -04:00
Cody Henthorne
4acafc3d77 Fix translation issue with media preview.
Fixes #12072
2022-03-28 19:07:20 -04:00
Alex Hart
65bf0aad79 Fixes text story preview sizing on pixel 2. 2022-03-28 19:07:20 -04:00
Alex Hart
ef6e846512 Text Camera Switch view positioning fixes. 2022-03-28 19:07:20 -04:00
Alex Hart
782464f664 Make messaging in keepalive more explicit. 2022-03-28 19:07:20 -04:00
Jim Gustafson
c7352f62e5 Update to RingRTC v2.20.0 2022-03-28 19:07:20 -04:00
Rashad Sookram
90dd6b7cb3 Compile against API level 31. 2022-03-28 19:07:20 -04:00
Rashad Sookram
ddfb4bf0a5 Fix context menu animation crash on Kitkat. 2022-03-28 19:07:20 -04:00
Alex Hart
cdef21d6c0 Add animation when you directly react to a story. 2022-03-28 19:07:20 -04:00
Alex Hart
c0f843061e Donations logs. 2022-03-28 19:07:19 -04:00
Alex Hart
5774771ea6 Reverse direction of swipe to reply. 2022-03-28 19:07:19 -04:00
Rashad Sookram
ab8d5474e0 Handle keyboard resize when creating a text story. 2022-03-28 19:07:19 -04:00
Alex Hart
6497ec8098 Adjust sizes for text post text size. 2022-03-28 19:07:19 -04:00
Greyson Parrelli
83c3b16b92 Add ContactDiscovery abstraction for doing CDS refreshes. 2022-03-28 19:07:19 -04:00
Alex Hart
3c2bd032ba Fix NPE when secondary story does not have media and is not a text story. 2022-03-28 19:07:19 -04:00
Alex Hart
f798866619 Maintain app bar layout when switching tabs. 2022-03-28 19:07:19 -04:00
Greyson Parrelli
ffad2c7386 Bump version to 5.34.5 2022-03-28 12:53:12 -04:00
Greyson Parrelli
7252e54593 Updated language translations. 2022-03-28 12:53:12 -04:00
Greyson Parrelli
cfab4dc658 Update stale-bot config. 2022-03-28 12:53:11 -04:00
Cody Henthorne
7f5a8ce6bb Disable telecom integration. 2022-03-28 12:34:50 -04:00
Greyson Parrelli
d02a597451 Bump version to 5.34.4 2022-03-23 11:54:11 -04:00
Greyson Parrelli
8d92a1f195 Updated language translations. 2022-03-23 11:53:53 -04:00
Greyson Parrelli
74e630aacb Fix possible crash and remove old fastRecord system. 2022-03-23 11:44:37 -04:00
1816 changed files with 56544 additions and 23057 deletions

23
.github/stale.yml vendored
View File

@@ -0,0 +1,23 @@
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 60
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
issues:
exemptLabels:
- acknowledged
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale Issue or Pull Request.
closeComment: >
This issue has been closed due to inactivity.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 1

View File

@@ -4,7 +4,7 @@ on:
pull_request:
push:
branches:
- 'master'
- 'main'
- '4.**'
- '5.**'

View File

@@ -15,4 +15,4 @@ jobs:
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

View File

@@ -1,18 +1,18 @@
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.util.livedata.Store
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.thoughtcrime.securesms.util.rx.RxStore
#end
#parse("File Header.java")
class ${NAME}ViewModel : ViewModel() {
private val store = Store(${NAME}State())
private val store = RxStore(${NAME}State())
private val disposables = CompositeDisposable()
val state: LiveData<${NAME}State> = store.stateLiveData
val state: Flowable<${NAME}State> = store.stateFlowable
override fun onCleared() {
disposables.clear()

View File

@@ -28,7 +28,7 @@ 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/master/CONTRIBUTING.md), that might answer some of your questions.
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.
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.

View File

@@ -96,7 +96,7 @@ try:
print("\nTo include this in the distribution, copy it to the project's assets/databases/ directory.")
print("If you support API 10 or lower, you must use the gzipped version to avoid corruption.")
except sqlite3.Error, e:
except sqlite3.Error as e:
if connection:
connection.rollback()
print("Error: %s" % e.args[0])

View File

@@ -10,12 +10,6 @@ apply plugin: 'kotlin-parcelize'
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/"
content {
@@ -63,8 +57,8 @@ ktlint {
version = "0.43.2"
}
def canonicalVersionCode = 1029
def canonicalVersionName = "5.34.3"
def canonicalVersionCode = 1065
def canonicalVersionName = "5.41.0"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
@@ -157,6 +151,8 @@ android {
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
exclude 'libsignal_jni.dylib'
exclude 'signal_jni.dll'
}
@@ -179,7 +175,7 @@ android {
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_CDSH_URL", "\"https://cdsh.staging.signal.org\""
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
@@ -196,9 +192,8 @@ 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 "String", "CDSI_MRENCLAVE", "\"42e36b74794abe612d698308b148ff8a7dc5fdc6ad28d99bc5024ed6ece18dfe\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
@@ -455,8 +450,10 @@ dependencies {
implementation project(':device-transfer')
implementation project(':image-editor')
implementation project(':donations')
implementation project(':contacts')
implementation project(':qr')
implementation libs.signal.client.android
implementation libs.libsignal.android
implementation libs.google.protobuf.javalite
implementation(libs.mobilecoin) {
@@ -481,7 +478,6 @@ dependencies {
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
@@ -507,7 +503,6 @@ dependencies {
implementation libs.lottie
implementation libs.stickyheadergrid
implementation libs.circular.progress.button
implementation libs.signal.android.database.sqlcipher
implementation libs.androidx.sqlite
@@ -540,6 +535,9 @@ dependencies {
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
testImplementation testLibs.espresso.core
@@ -551,7 +549,7 @@ dependencies {
implementation libs.rxjava3.rxkotlin
implementation libs.rxdogtag
androidTestUtil 'androidx.test:orchestrator:1.4.0'
androidTestUtil 'androidx.test:orchestrator:1.4.1'
}
def getLastCommitTimestamp() {

View File

@@ -2,6 +2,7 @@
-dontobfuscate
-keepattributes SourceFile,LineNumberTable
-keep class org.whispersystems.** { *; }
-keep class org.signal.libsignal.protocol.** { *; }
-keep class org.thoughtcrime.securesms.** { *; }
-keepclassmembers class ** {
public void onEvent*(**);

View File

@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.conversation
import androidx.test.core.app.ActivityScenario
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog
import org.thoughtcrime.securesms.database.IdentityDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalActivityRule
/**
* Android test to help show SNC dialog quickly with custom data to make sure it displays properly.
*/
@RunWith(AndroidJUnit4::class)
class SafetyNumberChangeDialogPreviewer {
@get:Rule val harness = SignalActivityRule()
@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, IdentityDatabase.VerifiedStatus.VERIFIED)
harness.changeIdentityKey(other)
val scenario: ActivityScenario<ConversationActivity> = harness.launchActivity { putExtra("recipient_id", other.id.serialize()) }
scenario.onActivity {
SafetyNumberChangeDialog.show(it.supportFragmentManager, other.id)
}
// Uncomment to make dialog stay on screen, otherwise will show/dismiss immediately
// ThreadUtil.sleep(15000)
}
}

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 MmsDatabaseTest_gifts {
private lateinit var mms: MmsDatabase
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.mms
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

@@ -0,0 +1,241 @@
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)
}
@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,
threadId = -1L
)
// WHEN
val result = mms.isOutgoingStoryAlreadyInDatabase(myStory.id, 200)
// THEN
assertTrue(result)
}
}

View File

@@ -0,0 +1,67 @@
package org.thoughtcrime.securesms.database
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.recipients.Recipient
import java.util.Optional
/**
* Helper methods for inserting an MMS message into the MMS table.
*/
object MmsHelper {
fun insert(
recipient: Recipient = Recipient.UNKNOWN,
body: String = "body",
sentTimeMillis: Long = System.currentTimeMillis(),
subscriptionId: Int = -1,
expiresIn: Long = 0,
viewOnce: Boolean = false,
distributionType: Int = ThreadDatabase.DistributionTypes.DEFAULT,
threadId: Long = 1,
storyType: StoryType = StoryType.NONE,
giftBadge: GiftBadge? = null
): Long {
val message = OutgoingMediaMessage(
recipient,
body,
emptyList(),
sentTimeMillis,
subscriptionId,
expiresIn,
viewOnce,
distributionType,
storyType,
null,
false,
null,
emptyList(),
emptyList(),
emptyList(),
emptySet(),
emptySet(),
giftBadge
)
return insert(
message = message,
threadId = threadId
)
}
fun insert(
message: OutgoingMediaMessage,
threadId: Long
): Long {
return SignalDatabase.mms.insertMessageOutbox(message, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null)
}
fun insert(
message: IncomingMediaMessage,
threadId: Long
): Optional<MessageDatabase.InsertResult> {
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId)
}
}

View File

@@ -1,6 +1,9 @@
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
@@ -8,25 +11,55 @@ 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 {
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())
@@ -34,6 +67,19 @@ class RecipientDatabaseTest {
@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)
@@ -478,6 +524,140 @@ class RecipientDatabaseTest {
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
// ==============================================================
@@ -522,12 +702,59 @@ class RecipientDatabaseTest {
}
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
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

View File

@@ -1,271 +0,0 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.signal.zkgroup.groups.GroupMasterKey
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.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.IncomingTextMessage
import org.thoughtcrime.securesms.util.CursorUtil
import org.whispersystems.libsignal.IdentityKey
import org.whispersystems.libsignal.SignalProtocolAddress
import org.whispersystems.libsignal.state.SessionRecord
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_merges {
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
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
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
/** 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_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)
assertNull(threadDatabase.getThreadIdFor(recipientIdE164))
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)
assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
// Session validation
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)!!
assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
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)!!
assertThat("Distribution list should have updated $recipientIdE164 to $recipientIdAci", updatedList.members, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
}
private val context: Application
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
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 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)
)
}
}
private fun notificationProfile(name: String): NotificationProfile {
return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
}
/** 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
)
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 E164_A = "+12221234567"
val E164_B = "+13331234567"
}
}

View File

@@ -0,0 +1,206 @@
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.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.thoughtcrime.securesms.keyvalue.SignalStore
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
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_processCdsV2Result {
private lateinit var recipientDatabase: RecipientDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
ensureDbEmpty()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
@Test
fun processCdsV2Result_noMatch() {
// Note that we haven't inserted any test data
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(resultId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_fullMatch() {
val inputId: RecipientId = insert(E164_A, PNI_A, ACI_A)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_onlyE164Matches() {
val inputId: RecipientId = insert(E164_A, null, null)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_e164AndPniMatches() {
val inputId: RecipientId = insert(E164_A, PNI_A, null)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_e164AndAciMatches() {
val inputId: RecipientId = insert(E164_A, null, ACI_A)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_onlyPniMatches() {
val inputId: RecipientId = insert(null, PNI_A, null)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_pniAndAciMatches() {
val inputId: RecipientId = insert(null, PNI_A, ACI_A)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
@Test
fun processCdsV2Result_onlyAciMatches() {
val inputId: RecipientId = insert(null, null, ACI_A)
val resultId: RecipientId = recipientDatabase.processCdsV2Result(E164_A, PNI_A, ACI_A)
val record: IdRecord = require(resultId)
assertEquals(inputId, record.id)
assertEquals(E164_A, record.e164)
assertEquals(ACI_A, record.sid)
assertEquals(PNI_A, record.pni)
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientDatabase.TABLE_NAME,
null,
contentValuesOf(
RecipientDatabase.PHONE to e164,
RecipientDatabase.SERVICE_ID to (aci ?: pni)?.toString(),
RecipientDatabase.PNI_COLUMN to pni?.toString(),
RecipientDatabase.REGISTERED to RecipientDatabase.RegisteredState.REGISTERED.id
)
)
return RecipientId.from(id)
}
private fun require(id: RecipientId): IdRecord {
return get(id)!!
}
private fun get(id: RecipientId): IdRecord? {
SignalDatabase.rawDatabase
.select(RecipientDatabase.ID, RecipientDatabase.PHONE, RecipientDatabase.SERVICE_ID, RecipientDatabase.PNI_COLUMN)
.from(RecipientDatabase.TABLE_NAME)
.where("${RecipientDatabase.ID} = ?", id)
.run()
.use { cursor ->
return if (cursor.moveToFirst()) {
IdRecord(
id = RecipientId.from(cursor.requireLong(RecipientDatabase.ID)),
e164 = cursor.requireString(RecipientDatabase.PHONE),
sid = ServiceId.parseOrNull(cursor.requireString(RecipientDatabase.SERVICE_ID)),
pni = PNI.parseOrNull(cursor.requireString(RecipientDatabase.PNI_COLUMN))
)
} else {
null
}
}
}
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 data class IdRecord(
val id: RecipientId,
val e164: String?,
val sid: ServiceId?,
val pni: PNI?,
)
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

@@ -8,7 +8,8 @@ import org.hamcrest.Matchers.nullValue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.zkgroup.groups.GroupMasterKey
import org.signal.core.util.Hex
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
@@ -20,7 +21,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.thoughtcrime.securesms.util.Hex
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId

View File

@@ -0,0 +1,463 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.TestCase.assertNull
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.containsInAnyOrder
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is`
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
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.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class StorySendsDatabaseTest {
private val distributionId1 = DistributionId.from(UUID.randomUUID())
private val distributionId2 = DistributionId.from(UUID.randomUUID())
private val distributionId3 = DistributionId.from(UUID.randomUUID())
private lateinit var distributionList1: DistributionListId
private lateinit var distributionList2: DistributionListId
private lateinit var distributionList3: DistributionListId
private lateinit var distributionListRecipient1: Recipient
private lateinit var distributionListRecipient2: Recipient
private lateinit var distributionListRecipient3: Recipient
private lateinit var recipients1to10: List<RecipientId>
private lateinit var recipients11to20: List<RecipientId>
private lateinit var recipients6to15: List<RecipientId>
private lateinit var recipients6to10: List<RecipientId>
private var messageId1: Long = 0
private var messageId2: Long = 0
private var messageId3: Long = 0
private lateinit var storySends: StorySendsDatabase
@Before
fun setup() {
storySends = SignalDatabase.storySends
recipients1to10 = makeRecipients(10)
recipients11to20 = makeRecipients(10)
distributionList1 = SignalDatabase.distributionLists.createList("1", emptyList(), distributionId = distributionId1)!!
distributionList2 = SignalDatabase.distributionLists.createList("2", emptyList(), distributionId = distributionId2)!!
distributionList3 = SignalDatabase.distributionLists.createList("3", emptyList(), distributionId = distributionId3)!!
distributionListRecipient1 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList1))
distributionListRecipient2 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList2))
distributionListRecipient3 = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(distributionList3))
messageId1 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
)
messageId2 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITH_REPLIES,
)
messageId3 = MmsHelper.insert(
recipient = distributionListRecipient3,
storyType = StoryType.STORY_WITHOUT_REPLIES,
)
recipients6to15 = recipients1to10.takeLast(5) + recipients11to20.take(5)
recipients6to10 = recipients1to10.takeLast(5)
}
@Test
fun getRecipientsToSendTo_noOverlap() {
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 200, true)
assertThat(recipientIdsForMessage1, hasSize(10))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
}
@Test
fun getRecipientsToSendTo_overlap() {
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
storySends.insert(messageId2, recipients6to15, 100, true, distributionId2)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
assertThat(recipientIdsForMessage1, hasSize(5))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
}
@Test
fun getRecipientsToSendTo_overlapAll() {
val recipient1 = recipients1to10.first()
val recipient2 = recipients11to20.first()
storySends.insert(messageId1, listOf(recipient1, recipient2), 100, false, distributionId1)
storySends.insert(messageId2, listOf(recipient1), 100, true, distributionId2)
storySends.insert(messageId3, listOf(recipient2), 100, true, distributionId3)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, false)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, true)
val recipientIdsForMessage3 = storySends.getRecipientsToSendTo(messageId3, 100, true)
assertThat(recipientIdsForMessage1, hasSize(0))
assertThat(recipientIdsForMessage2, hasSize(1))
assertThat(recipientIdsForMessage2, containsInAnyOrder(recipient1))
assertThat(recipientIdsForMessage3, hasSize(1))
assertThat(recipientIdsForMessage3, containsInAnyOrder(recipient2))
}
@Test
fun getRecipientsToSendTo_overlapWithEarlierMessage() {
storySends.insert(messageId1, recipients6to15, 100, true, distributionId1)
storySends.insert(messageId2, recipients1to10, 100, false, distributionId2)
val recipientIdsForMessage1 = storySends.getRecipientsToSendTo(messageId1, 100, true)
val recipientIdsForMessage2 = storySends.getRecipientsToSendTo(messageId2, 100, false)
assertThat(recipientIdsForMessage1, hasSize(10))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients6to15.toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(5))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
}
@Test
fun getRemoteDeleteRecipients_noOverlap() {
storySends.insert(messageId1, recipients1to10, 100, false, distributionId1)
storySends.insert(messageId2, recipients11to20, 200, true, distributionId2)
storySends.insert(messageId3, recipients1to10, 300, false, distributionId3)
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 100)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
assertThat(recipientIdsForMessage1, hasSize(10))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients11to20.toTypedArray()))
}
@Test
fun getRemoteDeleteRecipients_overlapNoPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
val recipientIdsForMessage1 = storySends.getRemoteDeleteRecipients(messageId1, 200)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
assertThat(recipientIdsForMessage1, hasSize(5))
assertThat(recipientIdsForMessage1, containsInAnyOrder(*recipients1to10.take(5).toTypedArray()))
assertThat(recipientIdsForMessage2, hasSize(5))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.takeLast(5).toTypedArray()))
}
@Test
fun getRemoteDeleteRecipients_overlapWithPreviousDeletes() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
SignalDatabase.mms.markAsRemoteDelete(messageId1)
storySends.insert(messageId2, recipients6to15, 200, true, distributionId2)
val recipientIdsForMessage2 = storySends.getRemoteDeleteRecipients(messageId2, 200)
assertThat(recipientIdsForMessage2, hasSize(10))
assertThat(recipientIdsForMessage2, containsInAnyOrder(*recipients6to15.toTypedArray()))
}
@Test
fun canReply_storyWithReplies() {
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val canReply = storySends.canReply(recipients1to10[0], 200)
assertThat(canReply, `is`(true))
}
@Test
fun canReply_storyWithoutReplies() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
val canReply = storySends.canReply(recipients1to10[0], 200)
assertThat(canReply, `is`(false))
}
@Test
fun canReply_storyWithAndWithoutRepliesOverlap() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients6to10, 200, true, distributionId2)
val message1OnlyRecipientCanReply = storySends.canReply(recipients1to10[0], 200)
val message2RecipientCanReply = storySends.canReply(recipients6to10[0], 200)
assertThat(message1OnlyRecipientCanReply, `is`(false))
assertThat(message2RecipientCanReply, `is`(true))
}
@Test
fun givenASingleStory_whenIGetFullSentStorySyncManifest_thenIExpectNotNull() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)
assertNotNull(manifest)
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNull() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, false, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId2, 200)
assertNull(manifest)
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectOneManifestPerRecipient() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
assertEquals(recipients1to10, manifest.entries.map { it.recipientId })
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectTwoListsPerRecipient() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
manifest.entries.forEach { entry ->
assertEquals(listOf(distributionId1, distributionId2), entry.distributionLists)
}
}
@Test
fun givenTwoStories_whenIGetFullSentStorySyncManifestForStory1_thenIExpectAllRecipientsCanReply() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
val manifest = storySends.getFullSentStorySyncManifest(messageId1, 200)!!
manifest.entries.forEach { entry ->
assertTrue(entry.allowedToReply)
}
}
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetFullSentStorySyncManifestForStory2_thenIExpectNonNullResult() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.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)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
assertEquals(recipients1to10.toHashSet(), recipientIds)
}
@Test
fun givenTwoStoriesAndOneIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectOnlyRecipientsThatHadStory1() {
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)
val recipientIds = storySends.getRecipientIdsForManifestUpdate(200, messageId1)
val results = storySends.getSentStorySyncManifestForUpdate(200, recipientIds)
val manifestRecipients = results.entries.map { it.recipientId }
assertEquals(recipients1to10, manifestRecipients)
}
@Test
fun givenTwoStoriesAndTheOneThatAllowedRepliesIsRemoteDeleted_whenIGetPartialSentStorySyncManifest_thenIExpectAllowRepliesToBeTrue() {
storySends.insert(messageId1, recipients1to10, 200, false, distributionId1)
storySends.insert(messageId2, recipients1to10, 200, true, distributionId2)
SignalDatabase.mms.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)
val expected = storySends.getFullSentStorySyncManifest(messageId1, 200)
val emptyManifest = SentStorySyncManifest(emptyList())
storySends.applySentStoryManifest(emptyManifest, 200)
val result = storySends.getFullSentStorySyncManifest(messageId1, 200)
assertEquals(expected, result)
}
@Test
fun givenAnIdenticalManifest_whenIApplyRemoteManifest_thenNothingChanges() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
val expected = storySends.getFullSentStorySyncManifest(messageId4, 200)
storySends.applySentStoryManifest(expected!!, 200)
val result = storySends.getFullSentStorySyncManifest(messageId4, 200)
assertEquals(expected, result)
}
@Test
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectMessageToBeMarkedRemoteDeleted() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
val messageId5 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
storySends.applySentStoryManifest(remote, 200)
assertTrue(SignalDatabase.mms.getMessageRecord(messageId5).isRemoteDelete)
}
@Test
fun givenAManifest_whenIApplyRemoteManifestWithoutOneList_thenIExpectSharedMessageToNotBeMarkedRemoteDeleted() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
val messageId5 = MmsHelper.insert(
recipient = distributionListRecipient2,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 200
)
storySends.insert(messageId4, recipients1to10, 200, false, distributionId1)
val remote = storySends.getFullSentStorySyncManifest(messageId4, 200)!!
storySends.insert(messageId5, recipients1to10, 200, false, distributionId2)
storySends.applySentStoryManifest(remote, 200)
assertFalse(SignalDatabase.mms.getMessageRecord(messageId4).isRemoteDelete)
}
@Test
fun givenNoLocalEntries_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatch() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 2000
)
val remote = SentStorySyncManifest(
recipients1to10.map {
SentStorySyncManifest.Entry(
recipientId = it,
allowedToReply = true,
distributionLists = listOf(distributionId1)
)
}
)
storySends.applySentStoryManifest(remote, 2000)
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
assertEquals(remote, local)
}
@Test
fun givenNonStoryMessageAtSentTimestamp_whenIApplyRemoteManifest_thenIExpectLocalManifestToMatchAndNoCrashes() {
val messageId4 = MmsHelper.insert(
recipient = distributionListRecipient1,
storyType = StoryType.STORY_WITHOUT_REPLIES,
sentTimeMillis = 2000
)
MmsHelper.insert(
recipient = Recipient.resolved(recipients1to10.first()),
sentTimeMillis = 2000
)
val remote = SentStorySyncManifest(
recipients1to10.map {
SentStorySyncManifest.Entry(
recipientId = it,
allowedToReply = true,
distributionLists = listOf(distributionId1)
)
}
)
storySends.applySentStoryManifest(remote, 2000)
val local = storySends.getFullSentStorySyncManifest(messageId4, 2000)
assertEquals(remote, local)
}
private fun makeRecipients(count: Int): List<RecipientId> {
return (1..count).map {
SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID()))
}
}
}

View File

@@ -4,7 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.thoughtcrime.securesms.util.Hex;
import org.signal.core.util.Hex;
import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.whispersystems.signalservice.api.kbs.KbsData;
import org.whispersystems.signalservice.api.kbs.MasterKey;

View File

@@ -0,0 +1,119 @@
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 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.IdentityDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
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.internal.push.VerifyAccountResponse
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 : 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()
}
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)
registrationRepository.registerAccountWithoutRegistrationLock(
RegistrationData(
code = "123123",
e164 = "+15554045550101",
password = Util.getSecret(18),
registrationId = registrationRepository.registrationId,
profileKey = registrationRepository.getProfileKey("+15554045550101"),
fcmToken = null
),
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false)
).blockingGet()
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>()
for (i in 0..4) {
val aci = ACI.from(UUID.randomUUID())
val recipientId = RecipientId.from(aci, "+1555555101$i")
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
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: IdentityDatabase.VerifiedStatus) {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityDatabase.VerifiedStatus.VERIFIED)
}
}

View File

@@ -179,11 +179,12 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".sharing.ShareActivity"
<activity android:name=".sharing.v2.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
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" />
@@ -223,7 +224,7 @@
<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" />
@@ -257,7 +258,7 @@
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" />
@@ -274,7 +275,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" />
@@ -284,7 +285,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" />
@@ -402,9 +403,17 @@
<activity
android:name=".stories.viewer.StoryViewerActivity"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
android:theme="@style/TextSecure.DarkNoActionBar.StoryViewer"
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"
@@ -416,6 +425,11 @@
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".badges.gifts.flow.GiftFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -457,6 +471,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"/>
@@ -534,6 +549,7 @@
<activity android:name=".blocked.BlockedUsersActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".scribbles.ImageEditorStickerSelectActivity"
@@ -644,7 +660,7 @@
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.webrtc.WebRtcCallService" android:foregroundServiceType="camera|microphone"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
@@ -698,7 +714,9 @@
<service android:name=".service.GenericForegroundService"/>
<service android:name=".gcm.FcmFetchService" />
<service android:name=".gcm.FcmFetchBackgroundService" />
<service android:name=".gcm.FcmFetchForegroundService" />
<service android:name=".gcm.FcmReceiveService">
<intent-filter>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 91 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,14 @@
package com.google.android.material.bottomsheet
import android.view.View
import android.widget.FrameLayout
import java.lang.ref.WeakReference
/**
* Manually adjust the nested scrolling child for a given [BottomSheetBehavior].
*/
object BottomSheetBehaviorHack {
fun setNestedScrollingChild(behavior: BottomSheetBehavior<FrameLayout>, view: View) {
behavior.nestedScrollingChildRef = WeakReference(view)
}
}

View File

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

View File

@@ -35,6 +35,7 @@ import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
@@ -47,7 +48,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobmanager.impl.NewNetworkConnectivity;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
@@ -60,7 +60,7 @@ 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.RetrieveReleaseChannelJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
@@ -93,7 +93,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import java.net.SocketException;
import java.net.SocketTimeoutException;
@@ -199,12 +198,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
.addPostRender(RetrieveReleaseChannelJob::enqueue)
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
.addPostRender(NewNetworkConnectivity::start)
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -220,7 +218,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();

View File

@@ -14,6 +14,7 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@@ -58,6 +59,12 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, from, "avatar").toBundle();
}
@Override
protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
super.attachBaseContext(newBase);
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
@@ -132,7 +139,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), toolbar);
fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
}

View File

@@ -60,6 +60,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
// Intentionally Blank.
}
default void updateSelectedState() {
// Intentionall Blank.
}
interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
@@ -95,8 +99,12 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onCallToAction(@NonNull String action);
void onDonateClicked();
void onBlockJoinRequest(@NonNull Recipient recipient);
void onRecipientNameClicked(@NonNull RecipientId target);
/** @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

@@ -13,7 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.signal.core.util.concurrent.SimpleTask;
/**
* This should be used whenever we want to prompt the user to block/unblock a recipient.
@@ -83,7 +83,8 @@ public final class BlockUnblockDialog {
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
builder.setMessage(recipient.isRegistered() ? R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages
: R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_send_you_messages);
if (onBlockAndReportSpam != null) {
builder.setNeutralButton(android.R.string.cancel, null);
@@ -128,7 +129,8 @@ public final class BlockUnblockDialog {
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other);
builder.setMessage(recipient.isRegistered() ? R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other
: R.string.BlockUnblockDialog_you_will_be_able_to_message_each_other);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
}

View File

@@ -27,7 +27,7 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
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.DirectoryHelper;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -153,7 +153,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
@Override
protected Void doInBackground(Context... params) {
try {
DirectoryHelper.refreshDirectory(params[0], true);
ContactDiscovery.refreshAll(params[0], true);
} catch (IOException e) {
Log.w(TAG, e);
}

View File

@@ -58,9 +58,9 @@ 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.ContactSelectionListAdapter;
@@ -70,7 +70,7 @@ 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.sync.DirectoryHelper;
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;
@@ -85,7 +85,6 @@ 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.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException;
@@ -138,8 +137,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
private HorizontalScrollView chipGroupScrollContainer;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
private View shadowView;
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper;
private HeaderActionProvider headerActionProvider;
private TextView headerActionView;
@@ -253,14 +251,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
chipGroup = view.findViewById(R.id.chipGroup);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
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
@@ -576,7 +570,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
@Override
protected Boolean doInBackground(Void... voids) {
try {
DirectoryHelper.refreshDirectory(context, false);
ContactDiscovery.refreshAll(context, false);
return true;
} catch (IOException e) {
Log.w(TAG, e);

View File

@@ -20,22 +20,22 @@ import androidx.appcompat.widget.Toolbar;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.libsignal.protocol.IdentityKeyPair;
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;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECPublicKey;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;

View File

@@ -14,36 +14,34 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
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.util.FeatureFlags;
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;
@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);
}
QrScannerView 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 +57,30 @@ public class DeviceAddFragment extends LoggingFragment {
});
}
return this.container;
scannerView.start(getViewLifecycleOwner(), FeatureFlags.useQrLegacyScan());
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();
}
@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);
}
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

@@ -10,7 +10,6 @@ import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
@@ -19,13 +18,13 @@ import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLock
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsState;
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.SplashScreenUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
@@ -60,8 +59,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
ConversationListTabRepository repository = new ConversationListTabRepository();
ConversationListTabsViewModel.Factory factory = new ConversationListTabsViewModel.Factory(repository);
navigator.onCreate(savedInstanceState);
handleGroupLinkInIntent(getIntent());
handleProxyInIntent(getIntent());
handleSignalMeIntent(getIntent());
@@ -69,18 +66,6 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
CachedInflater.from(this).clear();
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
Transformations.distinctUntilChanged(Transformations.map(conversationListTabsViewModel.getState(), ConversationListTabsState::getTab))
.observe(this, tab -> {
switch (tab) {
case CHATS:
getSupportFragmentManager().popBackStack();
break;
case STORIES:
navigator.goToStories();
break;
}
});
updateTabVisibility();
}
@@ -116,6 +101,12 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
updateTabVisibility();
}
@Override
protected void onStop() {
super.onStop();
SplashScreenUtil.setSplashScreenThemeIfNecessary(this, SignalStore.settings().getTheme());
}
@Override
public void onBackPressed() {
if (!navigator.onBackPressed()) {
@@ -134,11 +125,11 @@ 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_background_secondary));
WindowUtil.setNavigationBarColor(getWindow(), 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));
navigator.goToChats();
WindowUtil.setNavigationBarColor(getWindow(), ContextCompat.getColor(this, R.color.signal_colorBackground));
conversationListTabsViewModel.onChatsSelected();
}
}

View File

@@ -2,26 +2,19 @@ package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment;
public class MainNavigator {
public static final String STORIES_TAG = "STORIES";
public static final int REQUEST_CONFIG_CHANGES = 901;
private final MainActivity activity;
@@ -38,16 +31,6 @@ public class MainNavigator {
return ((MainActivity) activity).getNavigator();
}
public void onCreate(@Nullable Bundle savedInstanceState) {
if (savedInstanceState != null) {
return;
}
getFragmentManager().beginTransaction()
.add(R.id.fragment_container, ConversationListFragment.newInstance())
.commit();
}
/**
* @return True if the back pressed was handled in our own custom way, false if it should be given
* to the system to do the default behavior.
@@ -76,29 +59,6 @@ public class MainNavigator {
activity.startActivityForResult(AppSettingsActivity.home(activity), REQUEST_CONFIG_CHANGES);
}
public void goToArchiveList() {
getFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
.replace(R.id.fragment_container, ConversationListArchiveFragment.newInstance())
.addToBackStack(null)
.commit();
}
public void goToStories() {
if (getFragmentManager().findFragmentByTag(STORIES_TAG) == null) {
getFragmentManager().beginTransaction()
.replace(R.id.fragment_container, new StoriesLandingFragment(), STORIES_TAG)
.addToBackStack(null)
.commit();
}
}
public void goToChats() {
if (getFragmentManager().findFragmentByTag(STORIES_TAG) != null) {
getFragmentManager().popBackStack();
}
}
public void goToGroupCreation() {
activity.startActivity(CreateGroupActivity.newIntent(activity));
}

View File

@@ -55,6 +55,10 @@ 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;
@@ -67,7 +71,6 @@ 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.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FullscreenHelper;
@@ -87,7 +90,8 @@ import java.util.Objects;
public final class MediaPreviewActivity extends PassphraseRequiredActivity
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
MediaRailAdapter.RailItemListener,
MediaPreviewFragment.Events
MediaPreviewFragment.Events,
VoiceNoteMediaControllerOwner
{
private final static String TAG = Log.tag(MediaPreviewActivity.class);
@@ -127,6 +131,8 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private MediaDatabase.Sorting sorting;
private FullscreenHelper fullscreenHelper;
private VoiceNoteMediaController voiceNoteMediaController;
private @Nullable Cursor cursor = null;
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
@@ -159,6 +165,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
setSupportActionBar(findViewById(R.id.toolbar));
voiceNoteMediaController = new VoiceNoteMediaController(this);
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
fullscreenHelper = new FullscreenHelper(this);
@@ -203,23 +210,25 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
else from = "";
if (showThread) {
String to = null;
String titleText = null;
Recipient threadRecipient = mediaItem.threadRecipient;
if (threadRecipient != null) {
if (mediaItem.outgoing || threadRecipient.isGroup()) {
if (mediaItem.outgoing) {
if (threadRecipient.isSelf()) {
from = getString(R.string.note_to_self);
titleText = getString(R.string.note_to_self);
} else {
to = threadRecipient.getDisplayName(this);
titleText = getString(R.string.MediaPreviewActivity_you_to_s, threadRecipient.getDisplayName(this));
}
} else {
to = getString(R.string.MediaPreviewActivity_you);
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 to != null ? getString(R.string.MediaPreviewActivity_s_to_s, from, to)
: from;
return titleText != null ? titleText : from;
} else {
return from;
}
@@ -286,7 +295,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
anchorMarginsToBottomInsets(detailsContainer);
fullscreenHelper.configureToolbarSpacer(findViewById(R.id.toolbar_cutout_spacer));
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), findViewById(R.id.toolbar));
fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
}
@@ -386,10 +395,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem != null) {
Intent composeIntent = new Intent(this, ShareActivity.class);
composeIntent.putExtra(Intent.EXTRA_STREAM, mediaItem.uri);
composeIntent.setType(mediaItem.type);
startActivity(composeIntent);
MultiselectForwardFragmentArgs.create(
this,
mediaItem.uri,
mediaItem.type,
args -> MultiselectForwardFragment.showBottomSheet(getSupportFragmentManager(), args)
);
}
}
@@ -597,6 +608,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
finish();
}
@Override
public void onMediaReady() {
}
@Override
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
return voiceNoteMediaController;
}
private class ViewPagerListener extends ExtendedOnPageChangedListener {
@Override

View File

@@ -25,7 +25,7 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
@@ -33,7 +33,7 @@ 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.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.io.IOException;
@@ -79,7 +79,7 @@ public class NewConversationActivity extends ContactSelectionActivity
if (!resolved.isRegistered() || !resolved.hasServiceId()) {
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
try {
DirectoryHelper.refreshDirectoryFor(this, resolved, false);
ContactDiscovery.refresh(this, resolved, false);
resolved = Recipient.resolved(resolved.getId());
} catch (IOException e) {
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");

View File

@@ -171,6 +171,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.storageService().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userHasSkippedOrForgottenPin()) {
return STATE_CREATE_SIGNAL_PIN;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;
} else if (userMustCreateSignalPin()) {
@@ -190,6 +192,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().lastPinCreateFailed() && !SignalStore.kbsValues().hasOptedOut();
}
private boolean userHasSkippedOrForgottenPin() {
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().hasOptedOut() && SignalStore.kbsValues().isPinForgottenOrSkipped();
}
private boolean userMustSetProfileName() {
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
}

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

@@ -39,16 +39,19 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.lifecycle.ViewModelProviders;
import androidx.lifecycle.ViewModelProvider;
import androidx.window.DisplayFeature;
import androidx.window.FoldingFeature;
import androidx.window.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.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
@@ -75,16 +78,19 @@ import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VibrateUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
@@ -92,6 +98,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private static final String TAG = Log.tag(WebRtcCallActivity.class);
private static final int STANDARD_DELAY_FINISH = 1000;
private static final int VIBRATE_DURATION = 50;
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
@@ -107,10 +114,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable;
private boolean hasWarnedAboutBluetooth;
private androidx.window.WindowManager windowManager;
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
private ThrottledDebouncer requestNewSizesThrottle;
private Disposable ephemeralStateDisposable = Disposable.empty();
@Override
protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
@@ -153,6 +163,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
}
@Override
protected void onStart() {
super.onStart();
ephemeralStateDisposable = ApplicationDependencies.getSignalCallManager()
.ephemeralStates()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(viewModel::updateFromEphemeralState);
}
@Override
public void onResume() {
Log.i(TAG, "onResume()");
@@ -193,6 +213,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
Log.i(TAG, "onStop");
super.onStop();
ephemeralStateDisposable.dispose();
if (!isInPipMode() || isFinishing()) {
EventBus.getDefault().unregister(this);
requestNewSizesThrottle.clear();
@@ -285,7 +307,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor);
viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class);
viewModel = new ViewModelProvider(this, factory).get(WebRtcCallViewModel.class);
viewModel.setIsLandscapeEnabled(isLandscapeEnabled);
viewModel.setIsInPipMode(isInPipMode());
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
@@ -295,7 +317,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
viewModel.getOrientationAndLandscapeEnabled(),
(s, o) -> new CallParticipantsViewState(s, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
viewModel.getEphemeralState(),
(s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
.observe(this, p -> callScreen.updateCallParticipants(p));
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
@@ -501,6 +524,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
}
private void handleCallReconnecting() {
callScreen.setStatus(getString(R.string.WebRtcCallActivity__reconnecting));
VibrateUtil.vibrate(this, VIBRATE_DURATION);
}
private void handleRecipientUnavailable() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
@@ -623,6 +651,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
handleCallPreJoin(event); break;
case CALL_CONNECTED:
handleCallConnected(event); break;
case CALL_RECONNECTING:
handleCallReconnecting(); break;
case NETWORK_FAILURE:
handleServerFailure(); break;
case CALL_RINGING:
@@ -659,6 +689,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
enableVideoIfAvailable = false;
handleSetMuteVideo(false);
}
if (event.getBluetoothPermissionDenied() && !hasWarnedAboutBluetooth && !isFinishing()) {
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.WebRtcCallActivity__bluetooth_permission_denied)
.setMessage(R.string.WebRtcCallActivity__please_enable_the_nearby_devices_permission_to_use_bluetooth_during_a_call)
.setPositiveButton(R.string.WebRtcCallActivity__open_settings, (d, w) -> startActivity(Permissions.getApplicationSettingsIntent(this)))
.setNegativeButton(R.string.WebRtcCallActivity__not_now, null)
.show();
hasWarnedAboutBluetooth = true;
}
}
private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
@@ -807,7 +848,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
if (feature.isPresent()) {
FoldingFeature foldingFeature = (FoldingFeature) feature.get();
Rect bounds = foldingFeature.getBounds();
if (foldingFeature.getState() == FoldingFeature.State.HALF_OPENED && bounds.top == bounds.bottom) {
if (foldingFeature.isSeparating()) {
Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top display mode");
viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top));
} else {

View File

@@ -0,0 +1,63 @@
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.animation.doOnEnd
import androidx.core.animation.doOnStart
@RequiresApi(21)
class CrossfaderTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
companion object {
private const val WIDTH = "CrossfaderTransition.WIDTH"
}
override fun captureStartValues(transitionValues: TransitionValues) {
if (transitionValues.view is Crossfadeable) {
transitionValues.values[WIDTH] = transitionValues.view.width
}
}
override fun captureEndValues(transitionValues: TransitionValues) {
if (transitionValues.view is Crossfadeable) {
transitionValues.values[WIDTH] = transitionValues.view.width
}
}
override fun createAnimator(sceneRoot: ViewGroup?, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues == null || endValues == null) {
return null
}
val startWidth = (startValues.values[WIDTH] ?: 0) as Int
val endWidth = (endValues.values[WIDTH] ?: 0) as Int
val view: Crossfadeable = endValues.view as? Crossfadeable ?: return null
val reverse = startWidth > endWidth
return ValueAnimator.ofFloat(0f, 1f).apply {
addUpdateListener {
view.onCrossfadeAnimationUpdated(it.animatedValue as Float, reverse)
}
doOnStart {
view.onCrossfadeStarted(reverse)
}
doOnEnd {
view.onCrossfadeFinished(reverse)
}
}
}
interface Crossfadeable {
fun onCrossfadeAnimationUpdated(progress: Float, reverse: Boolean)
fun onCrossfadeStarted(reverse: Boolean)
fun onCrossfadeFinished(reverse: Boolean)
}
}

View File

@@ -0,0 +1,53 @@
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 com.google.android.material.floatingactionbutton.FloatingActionButton
@RequiresApi(21)
class FabElevationFadeTransform(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
companion object {
private const val ELEVATION = "CrossfaderTransition.ELEVATION"
}
override fun captureStartValues(transitionValues: TransitionValues) {
if (transitionValues.view is FloatingActionButton) {
transitionValues.values[ELEVATION] = transitionValues.view.elevation
}
}
override fun captureEndValues(transitionValues: TransitionValues) {
if (transitionValues.view is FloatingActionButton) {
transitionValues.values[ELEVATION] = transitionValues.view.elevation
}
}
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
if (startElevation == endElevation) {
return null
}
return ValueAnimator.ofFloat(
startValues.values[ELEVATION] as Float,
endValues.values[ELEVATION] as Float
).apply {
addUpdateListener {
val elevation = it.animatedValue as Float
endValues.view.elevation = elevation
}
}
}
}

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(

View File

@@ -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

@@ -40,8 +40,8 @@ class AvatarView @JvmOverloads constructor(
storyRing.visible = true
storyRing.isActivated = hasUnreadStory
avatar.scaleX = 0.82f
avatar.scaleY = 0.82f
avatar.scaleX = 0.8f
avatar.scaleY = 0.8f
}
private fun hideStoryRing() {
@@ -51,6 +51,10 @@ class AvatarView @JvmOverloads constructor(
avatar.scaleY = 1f
}
fun hasStory(): Boolean {
return storyRing.visible
}
fun setStoryRingFromState(storyViewState: StoryViewState) {
when (storyViewState) {
StoryViewState.NONE -> hideStoryRing()

View File

@@ -23,6 +23,8 @@ import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -110,7 +112,7 @@ public class BackupDialog {
@RequiresApi(29)
public static void showChooseBackupLocationDialog(@NonNull Fragment fragment, int requestCode) {
new AlertDialog.Builder(fragment.requireContext())
new MaterialAlertDialogBuilder(fragment.requireContext())
.setView(R.layout.backup_choose_location_dialog)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
@@ -141,7 +143,7 @@ public class BackupDialog {
}
public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) {
new AlertDialog.Builder(context)
new MaterialAlertDialogBuilder(context)
.setTitle(R.string.BackupDialog_delete_backups)
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
.setNegativeButton(android.R.string.cancel, null)

View File

@@ -5,7 +5,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.whispersystems.libsignal.util.ByteUtil;
import org.signal.libsignal.protocol.util.ByteUtil;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

View File

@@ -18,6 +18,8 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.kdf.HKDFv3;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
@@ -46,13 +48,11 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil;
import java.io.File;
import java.io.FileOutputStream;
@@ -197,7 +197,9 @@ public class FullBackupExporter extends FullBackupBase {
throwIfCanceled(cancellationSignal);
if (avatar != null) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength());
try (InputStream inputStream = avatar.getInputStream()) {
outputStream.write(avatar.getFilename(), inputStream, avatar.getLength());
}
}
}
@@ -377,6 +379,7 @@ public class FullBackupExporter extends FullBackupBase {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
inputStream.close();
}
} catch (IOException e) {
Log.w(TAG, e);
@@ -395,8 +398,9 @@ public class FullBackupExporter extends FullBackupBase {
if (!TextUtils.isEmpty(data) && size > 0) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
outputStream.writeSticker(rowId, inputStream, size);
try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
outputStream.writeSticker(rowId, inputStream, size);
}
}
} catch (IOException e) {
Log.w(TAG, e);

View File

@@ -17,6 +17,8 @@ import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.kdf.HKDFv3;
import org.signal.libsignal.protocol.util.ByteUtil;
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment;
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion;
@@ -36,9 +38,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil;
import org.signal.core.util.SqlUtil;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -77,7 +77,8 @@ public class FullBackupImporter extends FullBackupBase {
"msl_message",
"reaction",
"notification_profile_schedule",
"notification_profile_allowed_members"
"notification_profile_allowed_members",
"story_sends"
};
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
@@ -96,12 +97,12 @@ public class FullBackupImporter extends FullBackupBase {
int count = 0;
SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).getSqlCipherDatabase();
db.beginTransaction();
keyValueDatabase.beginTransaction();
try {
BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase);
db.beginTransaction();
keyValueDatabase.beginTransaction();
dropAllTables(db);
BackupFrame frame;
@@ -293,6 +294,7 @@ public class FullBackupImporter extends FullBackupBase {
String type = cursor.getString(1);
if ("table".equals(type) && !name.startsWith("sqlite_")) {
Log.i(TAG, "Dropping table: " + name);
db.execSQL("DROP TABLE IF EXISTS " + name);
}
}

View File

@@ -8,9 +8,12 @@ import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.glide.GiftBadgeModel
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ScreenDensity
import org.thoughtcrime.securesms.util.ThemeUtil
class BadgeImageView @JvmOverloads constructor(
@@ -77,6 +80,20 @@ class BadgeImageView @JvmOverloads constructor(
}
}
fun setGiftBadge(badge: GiftBadge?, glideRequests: GlideRequests) {
if (badge != null) {
glideRequests
.load(GiftBadgeModel(badge))
.downsample(DownsampleStrategy.NONE)
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), ScreenDensity.getBestDensityBucketForDevice(), ThemeUtil.isDarkTheme(context)))
.into(this)
} else {
glideRequests
.clear(this)
clearDrawable()
}
}
private fun clearDrawable() {
setImageDrawable(null)
isClickable = false

View File

@@ -1,41 +1,83 @@
package org.thoughtcrime.securesms.badges
import android.content.Context
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceProfileContentUpdateJob
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ProfileUtil
import java.io.IOException
class BadgeRepository(context: Context) {
companion object {
private val TAG = Log.tag(BadgeRepository::class.java)
}
private val context = context.applicationContext
/**
* Sets the visibility for each badge on a user's profile, and uploads them to the server.
* Does not write to the local database. The caller must either do that themselves or schedule
* a refresh own profile job.
*
* @return A list of the badges, properly modified to either visible or not visible, according to user preferences.
*/
@Throws(IOException::class)
@WorkerThread
fun setVisibilityForAllBadgesSync(
displayBadgesOnProfile: Boolean,
selfBadges: List<Badge>
): List<Badge> {
Log.d(TAG, "[setVisibilityForAllBadgesSync] Setting badge visibility...", true)
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
Log.d(TAG, "[setVisibilityForAllBadgesSync] Uploading profile...", true)
ProfileUtil.uploadProfileWithBadges(context, badges)
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile)
recipientDatabase.markNeedsSync(Recipient.self().id)
Log.d(TAG, "[setVisibilityForAllBadgesSync] Requesting data change sync...", true)
StorageSyncHelper.scheduleSyncForDataChange()
return badges
}
fun setVisibilityForAllBadges(
displayBadgesOnProfile: Boolean,
selfBadges: List<Badge> = Recipient.self().badges
): Completable = Completable.fromAction {
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
setVisibilityForAllBadgesSync(displayBadgesOnProfile, selfBadges)
ProfileUtil.uploadProfileWithBadges(context, badges)
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile)
recipientDatabase.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
recipientDatabase.setBadges(Recipient.self().id, badges)
Log.d(TAG, "[setVisibilityForAllBadges] Enqueueing profile refresh...", true)
ApplicationDependencies.getJobManager()
.startChain(RefreshOwnProfileJob())
.then(MultiDeviceProfileContentUpdateJob())
.enqueue()
}.subscribeOn(Schedulers.io())
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
val badges = Recipient.self().badges
val reOrderedBadges = listOf(featuredBadge.copy(visible = true)) + (badges.filterNot { it.id == featuredBadge.id })
Log.d(TAG, "[setFeaturedBadge] Uploading profile with reordered badges...", true)
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges)
Log.d(TAG, "[setFeaturedBadge] Enqueueing profile refresh...", true)
ApplicationDependencies.getJobManager()
.startChain(RefreshOwnProfileJob())
.then(MultiDeviceProfileContentUpdateJob())
.enqueue()
}.subscribeOn(Schedulers.io())
}

View File

@@ -9,16 +9,17 @@ import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.flexbox.JustifyContent
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.util.Pair
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
import org.thoughtcrime.securesms.util.ScreenDensity
import org.whispersystems.libsignal.util.Pair
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import java.math.BigDecimal
import java.sql.Timestamp
import java.util.concurrent.TimeUnit
object Badges {
@@ -93,7 +94,8 @@ object Badges {
Uri.parse(badge.imageUrl),
badge.imageDensity,
badge.expiration,
badge.visible
badge.visible,
0L
)
}
@@ -122,7 +124,8 @@ object Badges {
uriAndDensity.first(),
uriAndDensity.second(),
serviceBadge.expiration?.let { getTimestamp(it) } ?: 0,
serviceBadge.isVisible
serviceBadge.isVisible,
TimeUnit.SECONDS.toMillis(serviceBadge.duration)
)
}
}

View File

@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.badges.gifts
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import org.thoughtcrime.securesms.badges.gifts.ExpiredGiftSheetConfiguration.forExpiredBadge
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.fragments.requireListener
/**
* Displays expired gift information and gives the user the option to start a recurring monthly donation.
*/
class ExpiredGiftSheet : DSLSettingsBottomSheetFragment() {
companion object {
private const val ARG_BADGE = "arg.badge"
fun show(fragmentManager: FragmentManager, badge: Badge) {
ExpiredGiftSheet().apply {
arguments = Bundle().apply {
putParcelable(ARG_BADGE, badge)
}
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
private val badge: Badge
get() = requireArguments().getParcelable(ARG_BADGE)!!
override fun bindAdapter(adapter: DSLSettingsAdapter) {
ExpiredGiftSheetConfiguration.register(adapter)
adapter.submitList(
configure {
forExpiredBadge(
badge = badge,
onMakeAMonthlyDonation = {
requireListener<Callback>().onMakeAMonthlyDonation()
},
onNotNow = {
dismissAllowingStateLoss()
}
)
}.toMappingModelList()
)
}
interface Callback {
fun onMakeAMonthlyDonation()
}
}

View File

@@ -0,0 +1,81 @@
package org.thoughtcrime.securesms.badges.gifts
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
/**
* Contains shared DSL layout for expired gifts, creatable using a GiftBadge or a Badge.
*/
object ExpiredGiftSheetConfiguration {
fun register(mappingAdapter: MappingAdapter) {
BadgeDisplay112.register(mappingAdapter)
}
fun DSLConfiguration.forExpiredBadge(badge: Badge, onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
customPref(BadgeDisplay112.Model(badge, withDisplayText = false))
expiredSheet(onMakeAMonthlyDonation, onNotNow)
}
fun DSLConfiguration.forExpiredGiftBadge(giftBadge: GiftBadge, onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
customPref(BadgeDisplay112.GiftModel(giftBadge))
expiredSheet(onMakeAMonthlyDonation, onNotNow)
}
private fun DSLConfiguration.expiredSheet(onMakeAMonthlyDonation: () -> Unit, onNotNow: () -> Unit) {
textPref(
title = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__your_gift_badge_has_expired,
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
)
)
textPref(
title = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__your_gift_badge_has_expired_and_is,
DSLSettingsText.CenterModifier
)
)
if (SignalStore.donationsValues().isLikelyASustainer()) {
primaryButton(
text = DSLSettingsText.from(
stringId = android.R.string.ok
),
onClick = {
onNotNow()
}
)
} else {
textPref(
title = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__to_continue,
DSLSettingsText.CenterModifier
)
)
primaryButton(
text = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__make_a_monthly_donation
),
onClick = {
onMakeAMonthlyDonation()
}
)
secondaryButtonNoOutline(
text = DSLSettingsText.from(
stringId = R.string.ExpiredGiftSheetConfiguration__not_now
),
onClick = {
onNotNow()
}
)
}
}
}

View File

@@ -0,0 +1,111 @@
package org.thoughtcrime.securesms.badges.gifts
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.content.res.use
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.button.MaterialButton
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.badges.gifts.Gifts.formatExpiry
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.GlideRequests
/**
* Displays a gift badge sent to or received from a user, and allows the user to
* perform an action based off the badge's redemption state.
*/
class GiftMessageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
init {
inflate(context, R.layout.gift_message_view, this)
}
private val badgeView: BadgeImageView = findViewById(R.id.gift_message_view_badge)
private val titleView: TextView = findViewById(R.id.gift_message_view_title)
private val descriptionView: TextView = findViewById(R.id.gift_message_view_description)
private val actionView: MaterialButton = findViewById(R.id.gift_message_view_action)
init {
context.obtainStyledAttributes(attrs, R.styleable.GiftMessageView).use {
val textColor = it.getColor(R.styleable.GiftMessageView_giftMessageView__textColor, Color.RED)
titleView.setTextColor(textColor)
descriptionView.setTextColor(textColor)
val buttonTextColor = it.getColor(R.styleable.GiftMessageView_giftMessageView__buttonTextColor, Color.RED)
actionView.setTextColor(buttonTextColor)
actionView.iconTint = ColorStateList.valueOf(buttonTextColor)
val buttonBackgroundTint = it.getColor(R.styleable.GiftMessageView_giftMessageView__buttonBackgroundTint, Color.RED)
actionView.backgroundTintList = ColorStateList.valueOf(buttonBackgroundTint)
}
}
fun setGiftBadge(glideRequests: GlideRequests, giftBadge: GiftBadge, isOutgoing: Boolean, callback: Callback) {
titleView.setText(R.string.GiftMessageView__gift_badge)
descriptionView.text = giftBadge.formatExpiry(context)
actionView.icon = null
actionView.setOnClickListener { callback.onViewGiftBadgeClicked() }
actionView.isEnabled = true
if (isOutgoing) {
actionView.setText(R.string.GiftMessageView__view)
} else {
when (giftBadge.redemptionState) {
GiftBadge.RedemptionState.REDEEMED -> {
stopAnimationIfNeeded()
actionView.setIconResource(R.drawable.ic_check_circle_24)
}
GiftBadge.RedemptionState.STARTED -> actionView.icon = CircularProgressDrawable(context).apply {
actionView.isEnabled = false
setColorSchemeColors(ContextCompat.getColor(context, R.color.core_ultramarine))
strokeWidth = DimensionUnit.DP.toPixels(2f)
start()
}
else -> {
stopAnimationIfNeeded()
actionView.icon = null
}
}
actionView.setText(
when (giftBadge.redemptionState ?: GiftBadge.RedemptionState.UNRECOGNIZED) {
GiftBadge.RedemptionState.PENDING -> R.string.GiftMessageView__redeem
GiftBadge.RedemptionState.STARTED -> R.string.GiftMessageView__redeeming
GiftBadge.RedemptionState.REDEEMED -> R.string.GiftMessageView__redeemed
GiftBadge.RedemptionState.FAILED -> R.string.GiftMessageView__redeem
GiftBadge.RedemptionState.UNRECOGNIZED -> R.string.GiftMessageView__redeem
}
)
}
badgeView.setGiftBadge(giftBadge, glideRequests)
}
fun onGiftNotOpened() {
actionView.isClickable = false
}
fun onGiftOpened() {
actionView.isClickable = true
}
private fun stopAnimationIfNeeded() {
val icon = actionView.icon
if (icon is CircularProgressDrawable) {
icon.stop()
}
}
interface Callback {
fun onViewGiftBadgeClicked()
}
}

View File

@@ -0,0 +1,86 @@
package org.thoughtcrime.securesms.badges.gifts
import android.content.Context
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.ThreadDatabase
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.Base64
import java.lang.Integer.min
import java.util.concurrent.TimeUnit
/**
* Helper object for Gift badges
*/
object Gifts {
/**
* Request Code for getting token from Google Pay
*/
const val GOOGLE_PAY_REQUEST_CODE = 3000
/**
* Creates an OutgoingSecureMediaMessage which contains the given gift badge.
*/
fun createOutgoingGiftMessage(
recipient: Recipient,
giftBadge: GiftBadge,
sentTimestamp: Long,
expiresIn: Long
): OutgoingMediaMessage {
return OutgoingSecureMediaMessage(
recipient,
Base64.encodeBytes(giftBadge.toByteArray()),
listOf(),
sentTimestamp,
ThreadDatabase.DistributionTypes.CONVERSATION,
expiresIn,
false,
StoryType.NONE,
null,
false,
null,
listOf(),
listOf(),
listOf(),
giftBadge
)
}
/**
* @return the expiration time from the redemption token, in UNIX epoch seconds.
*/
private fun GiftBadge.getExpiry(): Long {
return try {
ReceiptCredentialPresentation(redemptionToken.toByteArray()).receiptExpirationTime
} catch (e: InvalidInputException) {
return 0L
}
}
fun GiftBadge.formatExpiry(context: Context): String {
val expiry = getExpiry()
val timeRemaining = TimeUnit.SECONDS.toMillis(expiry) - System.currentTimeMillis()
if (timeRemaining <= 0) {
return context.getString(R.string.Gifts__expired)
}
val days = TimeUnit.MILLISECONDS.toDays(timeRemaining).toInt()
if (days > 0) {
return context.resources.getQuantityString(R.plurals.Gifts__d_days_remaining, days, days)
}
val hours = TimeUnit.MILLISECONDS.toHours(timeRemaining).toInt()
if (hours > 0) {
return context.resources.getQuantityString(R.plurals.Gifts__d_hours_remaining, hours, hours)
}
val minutes = min(1, TimeUnit.MILLISECONDS.toMinutes(timeRemaining).toInt())
return context.resources.getQuantityString(R.plurals.Gifts__d_minutes_remaining, minutes, minutes)
}
}

View File

@@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.badges.gifts
import org.thoughtcrime.securesms.util.Projection
/**
* Notes that a given item can have a gift box drawn over it.
*/
interface OpenableGift {
/**
* Returns a projection to draw a top, or null to not do so.
*/
fun getOpenableGiftProjection(isAnimating: Boolean): Projection?
/**
* Returns a unique id assosicated with this gift.
*/
fun getGiftId(): Long
/**
* Registers a callback to start the open animation
*/
fun setOpenGiftCallback(openGift: (OpenableGift) -> Unit)
/**
* Clears any callback created to start the open animation
*/
fun clearOpenGiftCallback()
}

View File

@@ -0,0 +1,255 @@
package org.thoughtcrime.securesms.badges.gifts
import android.animation.FloatEvaluator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.provider.Settings
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.AnticipateInterpolator
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.graphics.toRect
import androidx.core.graphics.withSave
import androidx.core.graphics.withTranslation
import androidx.core.view.children
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.RecyclerView
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.Projection
import kotlin.math.PI
import kotlin.math.max
import kotlin.math.sin
/**
* Controls the gift box top and related animations for Gift bubbles.
*/
class OpenableGiftItemDecoration(context: Context) : RecyclerView.ItemDecoration(), DefaultLifecycleObserver {
private val animatorDurationScale = Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f)
private val messageIdsShakenThisSession = mutableSetOf<Long>()
private val messageIdsOpenedThisSession = mutableSetOf<Long>()
private val animationState = mutableMapOf<Long, GiftAnimationState>()
private val rect = RectF()
private val lineWidth = DimensionUnit.DP.toPixels(16f).toInt()
private val boxPaint = Paint().apply {
isAntiAlias = true
color = ContextCompat.getColor(context, R.color.core_ultramarine)
}
private val bowPaint = Paint().apply {
isAntiAlias = true
color = Color.WHITE
}
private val bowWidth = DimensionUnit.DP.toPixels(80f)
private val bowHeight = DimensionUnit.DP.toPixels(60f)
private val bowDrawable: Drawable = AppCompatResources.getDrawable(context, R.drawable.ic_gift_bow)!!
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
animationState.clear()
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
var needsInvalidation = false
val openableChildren = parent.children.filterIsInstance(OpenableGift::class.java)
val deadKeys = animationState.keys.filterNot { giftId -> openableChildren.any { it.getGiftId() == giftId } }
deadKeys.forEach {
animationState.remove(it)
}
val notAnimated = openableChildren.filterNot { animationState.containsKey(it.getGiftId()) }
notAnimated.filterNot { messageIdsOpenedThisSession.contains(it.getGiftId()) }.forEach { child ->
val projection = child.getOpenableGiftProjection(false)
if (projection != null) {
child.setOpenGiftCallback {
child.clearOpenGiftCallback()
val proj = it.getOpenableGiftProjection(true)
if (proj != null) {
messageIdsOpenedThisSession.add(it.getGiftId())
startOpenAnimation(it)
parent.invalidate()
}
}
if (messageIdsShakenThisSession.contains(child.getGiftId())) {
drawGiftBox(c, projection)
drawGiftBow(c, projection)
} else {
messageIdsShakenThisSession.add(child.getGiftId())
startShakeAnimation(child)
drawGiftBox(c, projection)
drawGiftBow(c, projection)
needsInvalidation = true
}
projection.release()
}
}
openableChildren.filter { animationState.containsKey(it.getGiftId()) }.forEach { child ->
val runningAnimation = animationState[child.getGiftId()]!!
c.withSave {
val isThisAnimationRunning = runningAnimation.update(
animatorDurationScale = animatorDurationScale,
canvas = c,
drawBox = this@OpenableGiftItemDecoration::drawGiftBox,
drawBow = this@OpenableGiftItemDecoration::drawGiftBow
)
if (!isThisAnimationRunning) {
animationState.remove(child.getGiftId())
}
needsInvalidation = true
}
}
if (needsInvalidation) {
parent.invalidate()
}
}
private fun drawGiftBox(canvas: Canvas, projection: Projection) {
canvas.drawPath(projection.path, boxPaint)
rect.set(
projection.x + (projection.width / 2) - lineWidth / 2,
projection.y,
projection.x + (projection.width / 2) + lineWidth / 2,
projection.y + projection.height
)
canvas.drawRect(rect, bowPaint)
rect.set(
projection.x,
projection.y + (projection.height / 2) - lineWidth / 2,
projection.x + projection.width,
projection.y + (projection.height / 2) + lineWidth / 2
)
canvas.drawRect(rect, bowPaint)
}
private fun drawGiftBow(canvas: Canvas, projection: Projection) {
rect.set(
projection.x + (projection.width / 2) - (bowWidth / 2),
projection.y,
projection.x + (projection.width / 2) + (bowWidth / 2),
projection.y + bowHeight
)
val padTop = (projection.height - rect.height()) * (48f / 89f)
bowDrawable.bounds = rect.toRect()
canvas.withTranslation(y = padTop) {
bowDrawable.draw(canvas)
}
}
private fun startShakeAnimation(child: OpenableGift) {
animationState[child.getGiftId()] = GiftAnimationState.ShakeAnimationState(child, System.currentTimeMillis())
}
private fun startOpenAnimation(child: OpenableGift) {
animationState[child.getGiftId()] = GiftAnimationState.OpenAnimationState(child, System.currentTimeMillis())
}
sealed class GiftAnimationState(val openableGift: OpenableGift, val startTime: Long, val duration: Long) {
/**
* Shakes the gift box to the left and right, slightly revealing the contents underneath.
* Uses a lag value to keep the bow one "frame" behind the box, to give it the effect of
* following behind.
*/
class ShakeAnimationState(openableGift: OpenableGift, startTime: Long) : GiftAnimationState(openableGift, startTime, SHAKE_DURATION_MILLIS) {
override fun update(canvas: Canvas, projection: Projection, progress: Float, lastFrameProgress: Float, drawBox: (Canvas, Projection) -> Unit, drawBow: (Canvas, Projection) -> Unit) {
canvas.withTranslation(x = getTranslation(progress).toFloat()) {
drawBox(canvas, projection)
}
canvas.withTranslation(x = getTranslation(lastFrameProgress).toFloat()) {
drawBow(canvas, projection)
}
}
private fun getTranslation(progress: Float): Double {
val interpolated = INTERPOLATOR.getInterpolation(progress)
val evaluated = EVALUATOR.evaluate(interpolated, 0f, 360f)
return 0.25f * sin(4 * evaluated * PI / 180f) * 180f / PI
}
}
class OpenAnimationState(openableGift: OpenableGift, startTime: Long) : GiftAnimationState(openableGift, startTime, OPEN_DURATION_MILLIS) {
override fun update(canvas: Canvas, projection: Projection, progress: Float, lastFrameProgress: Float, drawBox: (Canvas, Projection) -> Unit, drawBow: (Canvas, Projection) -> Unit) {
val interpolatedProgress = INTERPOLATOR.getInterpolation(progress)
val evaluatedValue = EVALUATOR.evaluate(interpolatedProgress, 0f, DimensionUnit.DP.toPixels(161f))
val interpolatedY = TRANSLATION_Y_INTERPOLATOR.getInterpolation(progress)
val evaluatedY = EVALUATOR.evaluate(interpolatedY, 0f, DimensionUnit.DP.toPixels(355f))
canvas.translate(evaluatedValue, evaluatedY)
drawBox(canvas, projection)
drawBow(canvas, projection)
}
}
fun update(animatorDurationScale: Float, canvas: Canvas, drawBox: (Canvas, Projection) -> Unit, drawBow: (Canvas, Projection) -> Unit): Boolean {
val projection = openableGift.getOpenableGiftProjection(true) ?: return false
if (animatorDurationScale <= 0f) {
update(canvas, projection, 0f, 0f, drawBox, drawBow)
projection.release()
return false
}
val currentFrameTime = System.currentTimeMillis()
val lastFrameProgress = max(0f, (currentFrameTime - startTime - ONE_FRAME_RELATIVE_TO_30_FPS_MILLIS) / (duration.toFloat() * animatorDurationScale))
val progress = (currentFrameTime - startTime) / (duration.toFloat() * animatorDurationScale)
if (progress > 1f) {
update(canvas, projection, 1f, 1f, drawBox, drawBow)
projection.release()
return false
}
update(canvas, projection, progress, lastFrameProgress, drawBox, drawBow)
projection.release()
return true
}
protected abstract fun update(
canvas: Canvas,
projection: Projection,
progress: Float,
lastFrameProgress: Float,
drawBox: (Canvas, Projection) -> Unit,
drawBow: (Canvas, Projection) -> Unit
)
}
companion object {
private val TRANSLATION_Y_INTERPOLATOR = AnticipateInterpolator(3f)
private val INTERPOLATOR = AccelerateDecelerateInterpolator()
private val EVALUATOR = FloatEvaluator()
private const val SHAKE_DURATION_MILLIS = 1000L
private const val OPEN_DURATION_MILLIS = 700L
private const val ONE_FRAME_RELATIVE_TO_30_FPS_MILLIS = 33
}
}

View File

@@ -0,0 +1,8 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import org.signal.core.util.money.FiatMoney
/**
* Convenience wrapper for a gift at a particular price point.
*/
data class Gift(val level: Long, val price: FiatMoney)

View File

@@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.content.Intent
import android.os.Bundle
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.FragmentWrapperActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
/**
* Activity which houses the gift flow.
*/
class GiftFlowActivity : FragmentWrapperActivity(), DonationPaymentComponent {
override val donationPaymentRepository: DonationPaymentRepository by lazy { DonationPaymentRepository(this) }
override val googlePayResultPublisher: Subject<DonationPaymentComponent.GooglePayResult> = PublishSubject.create()
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
super.onCreate(savedInstanceState, ready)
onBackPressedDispatcher.addCallback(this, OnBackPressed())
}
override fun getFragment(): Fragment {
return NavHostFragment.create(R.navigation.gift_flow)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
googlePayResultPublisher.onNext(DonationPaymentComponent.GooglePayResult(requestCode, resultCode, data))
}
private inner class OnBackPressed : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (!findNavController(R.id.fragment_container).popBackStack()) {
finish()
}
}
}
}

View File

@@ -0,0 +1,279 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.content.DialogInterface
import android.view.KeyEvent
import android.widget.FrameLayout
import android.widget.ImageView
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.InputAwareLayout
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
import org.thoughtcrime.securesms.components.settings.models.TextInput
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.keyboard.KeyboardPage
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel
import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment
import org.thoughtcrime.securesms.util.Debouncer
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.fragments.requireListener
/**
* Allows the user to confirm details about a gift, add a message, and finally make a payment.
*/
class GiftFlowConfirmationFragment :
DSLSettingsFragment(
titleId = R.string.GiftFlowConfirmationFragment__confirm_gift,
layoutId = R.layout.gift_flow_confirmation_fragment
),
EmojiKeyboardPageFragment.Callback,
EmojiEventListener,
EmojiSearchFragment.Callback {
companion object {
private val TAG = Log.tag(GiftFlowConfirmationFragment::class.java)
}
private val viewModel: GiftFlowViewModel by viewModels(
ownerProducer = { requireActivity() }
)
private val keyboardPagerViewModel: KeyboardPagerViewModel by viewModels(
ownerProducer = { requireActivity() }
)
private lateinit var inputAwareLayout: InputAwareLayout
private lateinit var emojiKeyboard: MediaKeyboard
private val lifecycleDisposable = LifecycleDisposable()
private var errorDialog: DialogInterface? = null
private lateinit var processingDonationPaymentDialog: AlertDialog
private lateinit var verifyingRecipientDonationPaymentDialog: AlertDialog
private lateinit var donationPaymentComponent: DonationPaymentComponent
private lateinit var textInputViewHolder: TextInput.MultilineViewHolder
private val eventPublisher = PublishSubject.create<TextInput.TextInputEvent>()
private val debouncer = Debouncer(100L)
override fun bindAdapter(adapter: DSLSettingsAdapter) {
RecipientPreference.register(adapter)
GiftRowItem.register(adapter)
keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI)
donationPaymentComponent = requireListener()
processingDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.processing_payment_dialog)
.setCancelable(false)
.create()
verifyingRecipientDonationPaymentDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.verifying_recipient_payment_dialog)
.setCancelable(false)
.create()
inputAwareLayout = requireView().findViewById(R.id.input_aware_layout)
emojiKeyboard = requireView().findViewById(R.id.emoji_drawer)
emojiKeyboard.setFragmentManager(childFragmentManager)
val googlePayButton = requireView().findViewById<GooglePayButton>(R.id.google_pay_button)
googlePayButton.setOnGooglePayClickListener {
viewModel.requestTokenFromGooglePay(getString(R.string.preferences__one_time))
}
val textInput = requireView().findViewById<FrameLayout>(R.id.text_input)
val emojiToggle = textInput.findViewById<ImageView>(R.id.emoji_toggle)
textInputViewHolder = TextInput.MultilineViewHolder(textInput, eventPublisher)
textInputViewHolder.onAttachedToWindow()
inputAwareLayout.addOnKeyboardShownListener {
if (emojiKeyboard.isEmojiSearchMode) {
return@addOnKeyboardShownListener
}
inputAwareLayout.hideAttachedInput(true)
emojiToggle.setImageResource(R.drawable.ic_emoji_smiley_24)
}
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (inputAwareLayout.isInputOpen) {
inputAwareLayout.hideAttachedInput(true)
emojiToggle.setImageResource(R.drawable.ic_emoji_smiley_24)
} else {
findNavController().popBackStack()
}
}
}
)
textInputViewHolder.bind(
TextInput.MultilineModel(
text = viewModel.snapshot.additionalMessage,
hint = DSLSettingsText.from(R.string.GiftFlowConfirmationFragment__add_a_message),
onTextChanged = {
viewModel.setAdditionalMessage(it)
},
onEmojiToggleClicked = {
if (inputAwareLayout.isKeyboardOpen || (!inputAwareLayout.isKeyboardOpen && !inputAwareLayout.isInputOpen)) {
inputAwareLayout.show(it, emojiKeyboard)
emojiToggle.setImageResource(R.drawable.ic_keyboard_24)
} else {
inputAwareLayout.showSoftkey(it)
emojiToggle.setImageResource(R.drawable.ic_emoji_smiley_24)
}
}
)
)
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
if (state.stage == GiftFlowState.Stage.RECIPIENT_VERIFICATION) {
debouncer.publish { verifyingRecipientDonationPaymentDialog.show() }
} else {
debouncer.clear()
verifyingRecipientDonationPaymentDialog.dismiss()
}
if (state.stage == GiftFlowState.Stage.PAYMENT_PIPELINE) {
processingDonationPaymentDialog.show()
} else {
processingDonationPaymentDialog.dismiss()
}
}
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += DonationError
.getErrorsForSource(DonationErrorSource.GIFT)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { donationError ->
onPaymentError(donationError)
}
lifecycleDisposable += viewModel.events.observeOn(AndroidSchedulers.mainThread()).subscribe { donationEvent ->
when (donationEvent) {
is DonationEvent.PaymentConfirmationSuccess -> onPaymentConfirmed()
DonationEvent.RequestTokenSuccess -> Log.i(TAG, "Successfully got request token from Google Pay")
DonationEvent.SubscriptionCancelled -> Unit
is DonationEvent.SubscriptionCancellationFailed -> Unit
}
}
lifecycleDisposable += donationPaymentComponent.googlePayResultPublisher.subscribe {
viewModel.onActivityResult(it.requestCode, it.resultCode, it.data)
}
}
override fun onDestroyView() {
super.onDestroyView()
textInputViewHolder.onDetachedFromWindow()
processingDonationPaymentDialog.dismiss()
debouncer.clear()
verifyingRecipientDonationPaymentDialog.dismiss()
}
private fun getConfiguration(giftFlowState: GiftFlowState): DSLConfiguration {
return configure {
if (giftFlowState.giftBadge != null) {
giftFlowState.giftPrices[giftFlowState.currency]?.let {
customPref(
GiftRowItem.Model(
giftBadge = giftFlowState.giftBadge,
price = it
)
)
}
}
sectionHeaderPref(R.string.GiftFlowConfirmationFragment__send_to)
customPref(
RecipientPreference.Model(
recipient = giftFlowState.recipient!!
)
)
textPref(
summary = DSLSettingsText.from(R.string.GiftFlowConfirmationFragment__your_gift_will_be_sent_in)
)
}
}
private fun onPaymentConfirmed() {
val mainActivityIntent = MainActivity.clearTop(requireContext())
val conversationIntent = ConversationIntents
.createBuilder(requireContext(), viewModel.snapshot.recipient!!.id, -1L)
.withGiftBadge(viewModel.snapshot.giftBadge!!)
.build()
requireActivity().startActivities(arrayOf(mainActivityIntent, conversationIntent))
}
private fun onPaymentError(throwable: Throwable?) {
Log.w(TAG, "onPaymentError", throwable, true)
if (errorDialog != null) {
Log.i(TAG, "Already displaying an error dialog. Skipping.")
return
}
errorDialog = DonationErrorDialogs.show(
requireContext(), throwable,
object : DonationErrorDialogs.DialogCallback() {
override fun onDialogDismissed() {
requireActivity().finish()
}
}
)
}
override fun onToolbarNavigationClicked() {
findNavController().popBackStack()
}
override fun openEmojiSearch() {
emojiKeyboard.onOpenEmojiSearch()
}
override fun closeEmojiSearch() {
emojiKeyboard.onCloseEmojiSearch()
}
override fun onEmojiSelected(emoji: String?) {
if (emoji?.isNotEmpty() == true) {
eventPublisher.onNext(TextInput.TextInputEvent.OnEmojiEvent(emoji))
}
}
override fun onKeyEvent(keyEvent: KeyEvent?) {
if (keyEvent != null) {
eventPublisher.onNext(TextInput.TextInputEvent.OnKeyEvent(keyEvent))
}
}
}

View File

@@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.contacts.paged.ContactSearchState
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
import org.thoughtcrime.securesms.conversation.mutiselect.forward.SearchConfigurationProvider
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Allows the user to select a recipient to send a gift to.
*/
class GiftFlowRecipientSelectionFragment : Fragment(R.layout.gift_flow_recipient_selection_fragment), MultiselectForwardFragment.Callback, SearchConfigurationProvider {
private val viewModel: GiftFlowViewModel by viewModels(
ownerProducer = { requireActivity() }
)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar = view.findViewById<Toolbar>(R.id.toolbar)
toolbar.setNavigationOnClickListener { requireActivity().onBackPressed() }
if (savedInstanceState == null) {
childFragmentManager.beginTransaction()
.replace(
R.id.multiselect_container,
MultiselectForwardFragment.create(
MultiselectForwardFragmentArgs(
canSendToNonPush = false,
multiShareArgs = emptyList(),
forceDisableAddMessage = true,
selectSingleRecipient = true
)
)
)
.commit()
}
}
override fun getSearchConfiguration(fragmentManager: FragmentManager, contactSearchState: ContactSearchState): ContactSearchConfiguration {
return ContactSearchConfiguration.build {
query = contactSearchState.query
if (query.isNullOrEmpty()) {
addSection(
ContactSearchConfiguration.Section.Recents(
includeSelf = false,
includeHeader = true,
mode = ContactSearchConfiguration.Section.Recents.Mode.INDIVIDUALS
)
)
}
addSection(
ContactSearchConfiguration.Section.Individuals(
includeSelf = false,
transportType = ContactSearchConfiguration.TransportType.PUSH,
includeHeader = true
)
)
}
}
override fun onFinishForwardAction() = Unit
override fun exitFlow() = Unit
override fun onSearchInputFocused() = Unit
override fun setResult(bundle: Bundle) {
val parcelableContacts: List<ContactSearchKey.ParcelableRecipientSearchKey> = bundle.getParcelableArrayList(MultiselectForwardFragment.RESULT_SELECTION)!!
val contacts = parcelableContacts.map { it.asRecipientSearchKey() }
if (contacts.isNotEmpty()) {
viewModel.setSelectedContact(contacts.first())
findNavController().safeNavigate(R.id.action_giftFlowRecipientSelectionFragment_to_giftFlowConfirmationFragment)
}
}
override fun getContainer(): ViewGroup = requireView() as ViewGroup
override fun getDialogBackgroundColor(): Int = Color.TRANSPARENT
}

View File

@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.util.PlatformCurrencyUtil
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.internal.ServiceResponse
import java.util.Currency
import java.util.Locale
/**
* Repository for grabbing gift badges and supported currency information.
*/
class GiftFlowRepository {
fun getGiftBadge(): Single<Pair<Long, Badge>> {
return ApplicationDependencies.getDonationsService()
.getGiftBadges(Locale.getDefault())
.flatMap(ServiceResponse<Map<Long, SignalServiceProfile.Badge>>::flattenResult)
.map { gifts -> gifts.map { it.key to Badges.fromServiceBadge(it.value) } }
.map { it.first() }
.subscribeOn(Schedulers.io())
}
fun getGiftPricing(): Single<Map<Currency, FiatMoney>> {
return ApplicationDependencies.getDonationsService()
.giftAmount
.subscribeOn(Schedulers.io())
.flatMap { it.flattenResult() }
.map { result ->
result
.filter { PlatformCurrencyUtil.getAvailableCurrencyCodes().contains(it.key) }
.mapKeys { (code, _) -> Currency.getInstance(code) }
.mapValues { (currency, price) -> FiatMoney(price, currency) }
}
}
}

View File

@@ -0,0 +1,95 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.view.View
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentComponent
import org.thoughtcrime.securesms.components.settings.app.subscription.models.CurrencySelection
import org.thoughtcrime.securesms.components.settings.app.subscription.models.NetworkFailure
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
import org.thoughtcrime.securesms.util.LifecycleDisposable
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.navigation.safeNavigate
/**
* Landing fragment for sending gifts.
*/
class GiftFlowStartFragment : DSLSettingsFragment(
layoutId = R.layout.gift_flow_start_fragment
) {
private val viewModel: GiftFlowViewModel by viewModels(
ownerProducer = { requireActivity() },
factoryProducer = { GiftFlowViewModel.Factory(GiftFlowRepository(), requireListener<DonationPaymentComponent>().donationPaymentRepository) }
)
private val lifecycleDisposable = LifecycleDisposable()
override fun bindAdapter(adapter: DSLSettingsAdapter) {
CurrencySelection.register(adapter)
GiftRowItem.register(adapter)
NetworkFailure.register(adapter)
IndeterminateLoadingCircle.register(adapter)
val next = requireView().findViewById<View>(R.id.next)
next.setOnClickListener {
findNavController().safeNavigate(R.id.action_giftFlowStartFragment_to_giftFlowRecipientSelectionFragment)
}
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
next.isEnabled = state.stage == GiftFlowState.Stage.READY
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
override fun onResume() {
super.onResume()
ViewUtil.hideKeyboard(requireContext(), requireView())
}
private fun getConfiguration(state: GiftFlowState): DSLConfiguration {
return configure {
customPref(
CurrencySelection.Model(
selectedCurrency = state.currency,
isEnabled = state.stage == GiftFlowState.Stage.READY,
onClick = {
val action = GiftFlowStartFragmentDirections.actionGiftFlowStartFragmentToSetCurrencyFragment(true, viewModel.getSupportedCurrencyCodes().toTypedArray())
findNavController().safeNavigate(action)
}
)
)
@Suppress("CascadeIf")
if (state.stage == GiftFlowState.Stage.FAILURE) {
customPref(
NetworkFailure.Model(
onRetryClick = {
viewModel.retry()
}
)
)
} else if (state.stage == GiftFlowState.Stage.INIT) {
customPref(IndeterminateLoadingCircle)
} else if (state.giftBadge != null) {
state.giftPrices[state.currency]?.let {
customPref(
GiftRowItem.Model(
giftBadge = state.giftBadge,
price = it
)
)
}
}
}
}
}

View File

@@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.recipients.Recipient
import java.util.Currency
/**
* State maintained by the GiftFlowViewModel
*/
data class GiftFlowState(
val currency: Currency,
val giftLevel: Long? = null,
val giftBadge: Badge? = null,
val giftPrices: Map<Currency, FiatMoney> = emptyMap(),
val stage: Stage = Stage.INIT,
val recipient: Recipient? = null,
val additionalMessage: CharSequence? = null
) {
enum class Stage {
INIT,
READY,
RECIPIENT_VERIFICATION,
TOKEN_REQUEST,
PAYMENT_PIPELINE,
FAILURE;
}
}

View File

@@ -0,0 +1,251 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.content.Intent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.google.android.gms.wallet.PaymentData
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.GooglePayApi
import org.thoughtcrime.securesms.badges.gifts.Gifts
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationEvent
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationPaymentRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.InternetConnectionObserver
import org.thoughtcrime.securesms.util.rx.RxStore
import java.util.Currency
/**
* Maintains state as a user works their way through the gift flow.
*/
class GiftFlowViewModel(
val repository: GiftFlowRepository,
val donationPaymentRepository: DonationPaymentRepository
) : ViewModel() {
private var giftToPurchase: Gift? = null
private val store = RxStore(
GiftFlowState(
currency = SignalStore.donationsValues().getOneTimeCurrency()
)
)
private val disposables = CompositeDisposable()
private val eventPublisher: PublishSubject<DonationEvent> = PublishSubject.create()
private val networkDisposable: Disposable
val state: Flowable<GiftFlowState> = store.stateFlowable
val events: Observable<DonationEvent> = eventPublisher
val snapshot: GiftFlowState get() = store.state
init {
refresh()
networkDisposable = InternetConnectionObserver
.observe()
.distinctUntilChanged()
.subscribe { isConnected ->
if (isConnected) {
retry()
}
}
}
fun retry() {
if (!disposables.isDisposed && store.state.stage == GiftFlowState.Stage.FAILURE) {
store.update { it.copy(stage = GiftFlowState.Stage.INIT) }
refresh()
}
}
fun refresh() {
disposables.clear()
disposables += SignalStore.donationsValues().observableOneTimeCurrency.subscribe { currency ->
store.update {
it.copy(
currency = currency
)
}
}
disposables += repository.getGiftPricing().subscribe { giftPrices ->
store.update {
it.copy(
giftPrices = giftPrices,
stage = getLoadState(it, giftPrices = giftPrices)
)
}
}
disposables += repository.getGiftBadge().subscribeBy(
onSuccess = { (giftLevel, giftBadge) ->
store.update {
it.copy(
giftLevel = giftLevel,
giftBadge = giftBadge,
stage = getLoadState(it, giftBadge = giftBadge)
)
}
},
onError = { throwable ->
Log.w(TAG, "Could not load gift badge", throwable)
store.update {
it.copy(
stage = GiftFlowState.Stage.FAILURE
)
}
}
)
}
override fun onCleared() {
disposables.clear()
}
fun setSelectedContact(selectedContact: ContactSearchKey.RecipientSearchKey) {
store.update {
it.copy(recipient = Recipient.resolved(selectedContact.recipientId))
}
}
fun getSupportedCurrencyCodes(): List<String> {
return store.state.giftPrices.keys.map { it.currencyCode }
}
fun requestTokenFromGooglePay(label: String) {
val giftLevel = store.state.giftLevel ?: return
val giftPrice = store.state.giftPrices[store.state.currency] ?: return
val giftRecipient = store.state.recipient?.id ?: return
this.giftToPurchase = Gift(giftLevel, giftPrice)
store.update { it.copy(stage = GiftFlowState.Stage.RECIPIENT_VERIFICATION) }
disposables += donationPaymentRepository.verifyRecipientIsAllowedToReceiveAGift(giftRecipient)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy(
onComplete = {
store.update { it.copy(stage = GiftFlowState.Stage.TOKEN_REQUEST) }
donationPaymentRepository.requestTokenFromGooglePay(giftToPurchase!!.price, label, Gifts.GOOGLE_PAY_REQUEST_CODE)
},
onError = this::onPaymentFlowError
)
}
fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
val gift = giftToPurchase
giftToPurchase = null
val recipient = store.state.recipient?.id
donationPaymentRepository.onActivityResult(
requestCode, resultCode, data, Gifts.GOOGLE_PAY_REQUEST_CODE,
object : GooglePayApi.PaymentRequestCallback {
override fun onSuccess(paymentData: PaymentData) {
if (gift != null && recipient != null) {
eventPublisher.onNext(DonationEvent.RequestTokenSuccess)
store.update { it.copy(stage = GiftFlowState.Stage.PAYMENT_PIPELINE) }
donationPaymentRepository.continuePayment(gift.price, paymentData, recipient, store.state.additionalMessage?.toString(), gift.level).subscribeBy(
onError = this@GiftFlowViewModel::onPaymentFlowError,
onComplete = {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
eventPublisher.onNext(DonationEvent.PaymentConfirmationSuccess(store.state.giftBadge!!))
}
)
} else {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
}
}
override fun onError(googlePayException: GooglePayApi.GooglePayException) {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
DonationError.routeDonationError(ApplicationDependencies.getApplication(), DonationError.getGooglePayRequestTokenError(DonationErrorSource.GIFT, googlePayException))
}
override fun onCancelled() {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
}
}
)
}
private fun onPaymentFlowError(throwable: Throwable) {
store.update { it.copy(stage = GiftFlowState.Stage.READY) }
val donationError: DonationError = if (throwable is DonationError) {
throwable
} else {
Log.w(TAG, "Failed to complete payment or redemption", throwable, true)
DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT)
}
DonationError.routeDonationError(ApplicationDependencies.getApplication(), donationError)
}
private fun getLoadState(
oldState: GiftFlowState,
giftPrices: Map<Currency, FiatMoney>? = null,
giftBadge: Badge? = null,
): GiftFlowState.Stage {
if (oldState.stage != GiftFlowState.Stage.INIT) {
return oldState.stage
}
if (giftPrices?.isNotEmpty() == true) {
return if (oldState.giftBadge != null) {
GiftFlowState.Stage.READY
} else {
GiftFlowState.Stage.INIT
}
}
if (giftBadge != null) {
return if (oldState.giftPrices.isNotEmpty()) {
GiftFlowState.Stage.READY
} else {
GiftFlowState.Stage.INIT
}
}
return GiftFlowState.Stage.INIT
}
fun setAdditionalMessage(additionalMessage: CharSequence) {
store.update { it.copy(additionalMessage = additionalMessage) }
}
companion object {
private val TAG = Log.tag(GiftFlowViewModel::class.java)
}
class Factory(
private val repository: GiftFlowRepository,
private val donationPaymentRepository: DonationPaymentRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(
GiftFlowViewModel(
repository,
donationPaymentRepository
)
) as T
}
}
}

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.view.View
import android.widget.TextView
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import org.thoughtcrime.securesms.util.visible
import java.util.concurrent.TimeUnit
/**
* A line item for gifts, displayed in the Gift flow's start and confirmation fragments.
*/
object GiftRowItem {
fun register(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.subscription_preference))
}
class Model(val giftBadge: Badge, val price: FiatMoney) : MappingModel<Model> {
override fun areItemsTheSame(newItem: Model): Boolean = giftBadge.id == newItem.giftBadge.id
override fun areContentsTheSame(newItem: Model): Boolean = giftBadge == newItem.giftBadge && price == newItem.price
}
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
private val badgeView = itemView.findViewById<BadgeImageView>(R.id.badge)
private val titleView = itemView.findViewById<TextView>(R.id.title)
private val checkView = itemView.findViewById<View>(R.id.check)
private val taglineView = itemView.findViewById<TextView>(R.id.tagline)
private val priceView = itemView.findViewById<TextView>(R.id.price)
override fun bind(model: Model) {
checkView.visible = false
badgeView.setBadge(model.giftBadge)
titleView.text = model.giftBadge.name
taglineView.setText(R.string.GiftRowItem__send_a_gift_badge)
val price = FiatMoneyUtil.format(
context.resources,
model.price,
FiatMoneyUtil.formatOptions()
.trimZerosAfterDecimal()
.withDisplayTime(false)
)
val duration = TimeUnit.MILLISECONDS.toDays(model.giftBadge.duration)
priceView.text = context.resources.getQuantityString(R.plurals.GiftRowItem_s_dot_d_day_duration, duration.toInt(), price, duration)
}
}
}

View File

@@ -0,0 +1,22 @@
package org.thoughtcrime.securesms.badges.gifts.flow
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import org.thoughtcrime.securesms.R
/**
* Wraps the google pay button in a convenient frame layout.
*/
class GooglePayButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
init {
inflate(context, R.layout.donate_with_googlepay_button, this)
}
fun setOnGooglePayClickListener(action: () -> Unit) {
getChildAt(0).setOnClickListener { action() }
}
}

View File

@@ -0,0 +1,80 @@
package org.thoughtcrime.securesms.badges.gifts.thanks
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.BadgePreview
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable
/**
* Displays a "Thank you" message in a conversation when redirected
* there after purchasing and sending a gift badge.
*/
class GiftThanksSheet : DSLSettingsBottomSheetFragment() {
companion object {
private const val ARGS_RECIPIENT_ID = "args.recipient.id"
private const val ARGS_BADGE = "args.badge"
@JvmStatic
fun show(fragmentManager: FragmentManager, recipientId: RecipientId, badge: Badge) {
GiftThanksSheet().apply {
arguments = Bundle().apply {
putParcelable(ARGS_RECIPIENT_ID, recipientId)
putParcelable(ARGS_BADGE, badge)
}
}.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
private val lifecycleDisposable = LifecycleDisposable()
private val recipientId: RecipientId
get() = requireArguments().getParcelable(ARGS_RECIPIENT_ID)!!
private val badge: Badge
get() = requireArguments().getParcelable(ARGS_BADGE)!!
override fun bindAdapter(adapter: DSLSettingsAdapter) {
BadgePreview.register(adapter)
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += Recipient.observable(recipientId).observeOn(AndroidSchedulers.mainThread()).subscribe {
adapter.submitList(getConfiguration(it).toMappingModelList())
}
}
private fun getConfiguration(recipient: Recipient): DSLConfiguration {
return configure {
textPref(
title = DSLSettingsText.from(R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__thanks_for_your_support, DSLSettingsText.TitleLargeModifier, DSLSettingsText.CenterModifier)
)
noPadTextPref(
title = DSLSettingsText.from(getString(R.string.GiftThanksSheet__youve_gifted_a_badge_to_s, recipient.getDisplayName(requireContext())))
)
space(DimensionUnit.DP.toPixels(37f).toInt())
customPref(
BadgePreview.BadgeModel.GiftedBadgeModel(
badge = badge,
recipient = recipient
)
)
space(DimensionUnit.DP.toPixels(60f).toInt())
}
}
}

View File

@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.badges.gifts.viewgift
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import java.util.Locale
/**
* Shared repository for getting information about a particular gift.
*/
class ViewGiftRepository {
fun getBadge(giftBadge: GiftBadge): Single<Badge> {
val presentation = ReceiptCredentialPresentation(giftBadge.redemptionToken.toByteArray())
return ApplicationDependencies
.getDonationsService()
.getGiftBadge(Locale.getDefault(), presentation.receiptLevel)
.flatMap { it.flattenResult() }
.map { Badges.fromServiceBadge(it) }
.subscribeOn(Schedulers.io())
}
fun getGiftBadge(messageId: Long): Observable<GiftBadge> {
return Observable.create { emitter ->
fun refresh() {
val record = SignalDatabase.mms.getMessageRecord(messageId)
val giftBadge: GiftBadge = (record as MmsMessageRecord).giftBadge!!
emitter.onNext(giftBadge)
}
val messageObserver = DatabaseObserver.MessageObserver {
if (it.mms && messageId == it.id) {
refresh()
}
}
ApplicationDependencies.getDatabaseObserver().registerMessageUpdateObserver(messageObserver)
emitter.setCancellable {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver)
}
refresh()
}
}
}

View File

@@ -0,0 +1,281 @@
package org.thoughtcrime.securesms.badges.gifts.viewgift.received
import android.content.DialogInterface
import android.os.Bundle
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.receipts.ReceiptCredentialPresentation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.gifts.ExpiredGiftSheetConfiguration.forExpiredGiftBadge
import org.thoughtcrime.securesms.badges.gifts.viewgift.ViewGiftRepository
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
import org.thoughtcrime.securesms.badges.models.BadgeDisplay160
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorDialogs
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.models.IndeterminateLoadingCircle
import org.thoughtcrime.securesms.components.settings.models.OutlinedSwitch
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable
import java.util.concurrent.TimeUnit
/**
* Handles all interactions for received gift badges.
*/
class ViewReceivedGiftBottomSheet : DSLSettingsBottomSheetFragment() {
companion object {
private val TAG = Log.tag(ViewReceivedGiftBottomSheet::class.java)
private const val ARG_GIFT_BADGE = "arg.gift.badge"
private const val ARG_SENT_FROM = "arg.sent.from"
private const val ARG_MESSAGE_ID = "arg.message.id"
@JvmField
val REQUEST_KEY: String = TAG
const val RESULT_NOT_NOW = "result.not.now"
@JvmStatic
fun show(fragmentManager: FragmentManager, messageRecord: MmsMessageRecord) {
ViewReceivedGiftBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(ARG_SENT_FROM, messageRecord.recipient.id)
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.toByteArray())
putLong(ARG_MESSAGE_ID, messageRecord.id)
}
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}
private val lifecycleDisposable = LifecycleDisposable()
private val sentFrom: RecipientId
get() = requireArguments().getParcelable(ARG_SENT_FROM)!!
private val messageId: Long
get() = requireArguments().getLong(ARG_MESSAGE_ID)
private val viewModel: ViewReceivedGiftViewModel by viewModels(
factoryProducer = { ViewReceivedGiftViewModel.Factory(sentFrom, messageId, ViewGiftRepository(), BadgeRepository(requireContext())) }
)
private var errorDialog: DialogInterface? = null
private lateinit var progressDialog: AlertDialog
override fun bindAdapter(adapter: DSLSettingsAdapter) {
BadgeDisplay112.register(adapter)
OutlinedSwitch.register(adapter)
BadgeDisplay160.register(adapter)
IndeterminateLoadingCircle.register(adapter)
progressDialog = MaterialAlertDialogBuilder(requireContext())
.setView(R.layout.redeeming_gift_dialog)
.setCancelable(false)
.create()
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += DonationError
.getErrorsForSource(DonationErrorSource.GIFT_REDEMPTION)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { donationError ->
onRedemptionError(donationError)
}
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
override fun onDestroy() {
super.onDestroy()
progressDialog.hide()
}
private fun onRedemptionError(throwable: Throwable?) {
Log.w(TAG, "onRedemptionError", throwable, true)
if (errorDialog != null) {
Log.i(TAG, "Already displaying an error dialog. Skipping.")
return
}
errorDialog = DonationErrorDialogs.show(
requireContext(), throwable,
object : DonationErrorDialogs.DialogCallback() {
override fun onDialogDismissed() {
findNavController().popBackStack()
}
}
)
}
private fun getConfiguration(state: ViewReceivedGiftState): DSLConfiguration {
return configure {
if (state.giftBadge == null) {
customPref(IndeterminateLoadingCircle)
} else if (isGiftBadgeExpired(state.giftBadge)) {
forExpiredGiftBadge(
giftBadge = state.giftBadge,
onMakeAMonthlyDonation = {
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
requireActivity().finish()
},
onNotNow = {
dismissAllowingStateLoss()
}
)
} else {
if (state.giftBadge.redemptionState == GiftBadge.RedemptionState.STARTED) {
progressDialog.show()
} else {
progressDialog.hide()
}
if (state.recipient != null && !isGiftBadgeRedeemed(state.giftBadge)) {
noPadTextPref(
title = DSLSettingsText.from(
charSequence = requireContext().getString(R.string.ViewReceivedGiftBottomSheet__s_sent_you_a_gift, state.recipient.getShortDisplayName(requireContext())),
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
)
)
space(DimensionUnit.DP.toPixels(12f).toInt())
presentSubheading(state.recipient)
space(DimensionUnit.DP.toPixels(37f).toInt())
}
if (state.badge != null && state.controlState != null) {
presentForUnexpiredGiftBadge(state, state.giftBadge, state.controlState, state.badge)
space(DimensionUnit.DP.toPixels(16f).toInt())
}
}
}
}
private fun DSLConfiguration.presentSubheading(recipient: Recipient) {
noPadTextPref(
title = DSLSettingsText.from(
charSequence = requireContext().getString(R.string.ViewReceivedGiftBottomSheet__youve_received_a_gift_badge, recipient.getDisplayName(requireContext())),
DSLSettingsText.CenterModifier
)
)
}
private fun DSLConfiguration.presentForUnexpiredGiftBadge(
state: ViewReceivedGiftState,
giftBadge: GiftBadge,
controlState: ViewReceivedGiftState.ControlState,
badge: Badge
) {
when (giftBadge.redemptionState) {
GiftBadge.RedemptionState.REDEEMED -> {
customPref(
BadgeDisplay160.Model(
badge = badge
)
)
state.recipient?.run {
presentSubheading(this)
}
}
else -> {
customPref(
BadgeDisplay112.Model(
badge = badge
)
)
customPref(
OutlinedSwitch.Model(
text = DSLSettingsText.from(
when (controlState) {
ViewReceivedGiftState.ControlState.DISPLAY -> R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__display_on_profile
ViewReceivedGiftState.ControlState.FEATURE -> R.string.SubscribeThanksForYourSupportBottomSheetDialogFragment__make_featured_badge
}
),
isEnabled = giftBadge.redemptionState != GiftBadge.RedemptionState.STARTED,
isChecked = state.getControlChecked(),
onClick = {
viewModel.setChecked(!it.isChecked)
}
)
)
if (state.hasOtherBadges && state.displayingOtherBadges) {
noPadTextPref(DSLSettingsText.from(R.string.ThanksForYourSupportBottomSheetFragment__when_you_have_more))
}
space(DimensionUnit.DP.toPixels(36f).toInt())
primaryButton(
text = DSLSettingsText.from(R.string.ViewReceivedGiftSheet__redeem),
isEnabled = giftBadge.redemptionState != GiftBadge.RedemptionState.STARTED,
onClick = {
lifecycleDisposable += viewModel.redeem().subscribeBy(
onComplete = {
dismissAllowingStateLoss()
},
onError = {
onRedemptionError(it)
}
)
}
)
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.ViewReceivedGiftSheet__not_now),
isEnabled = giftBadge.redemptionState != GiftBadge.RedemptionState.STARTED,
onClick = {
setFragmentResult(
REQUEST_KEY,
Bundle().apply {
putBoolean(RESULT_NOT_NOW, true)
}
)
dismissAllowingStateLoss()
}
)
}
}
}
private fun isGiftBadgeRedeemed(giftBadge: GiftBadge): Boolean {
return giftBadge.redemptionState == GiftBadge.RedemptionState.REDEEMED
}
private fun isGiftBadgeExpired(giftBadge: GiftBadge): Boolean {
return try {
val receiptCredentialPresentation = ReceiptCredentialPresentation(giftBadge.redemptionToken.toByteArray())
receiptCredentialPresentation.receiptExpirationTime <= TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis())
} catch (e: InvalidInputException) {
Log.w(TAG, "Failed to check expiration of given badge.", e)
true
}
}
}

View File

@@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.badges.gifts.viewgift.received
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.recipients.Recipient
data class ViewReceivedGiftState(
val recipient: Recipient? = null,
val giftBadge: GiftBadge? = null,
val badge: Badge? = null,
val controlState: ControlState? = null,
val hasOtherBadges: Boolean = false,
val displayingOtherBadges: Boolean = false,
val userCheckSelection: Boolean? = false,
val redemptionState: RedemptionState = RedemptionState.NONE
) {
fun getControlChecked(): Boolean {
return when {
userCheckSelection != null -> userCheckSelection
controlState == ControlState.FEATURE -> false
!displayingOtherBadges -> false
else -> true
}
}
enum class ControlState {
DISPLAY,
FEATURE
}
enum class RedemptionState {
NONE,
IN_PROGRESS
}
}

View File

@@ -0,0 +1,150 @@
package org.thoughtcrime.securesms.badges.gifts.viewgift.received
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.badges.BadgeRepository
import org.thoughtcrime.securesms.badges.gifts.viewgift.ViewGiftRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationError
import org.thoughtcrime.securesms.components.settings.app.subscription.errors.DonationErrorSource
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.jobs.DonationReceiptRedemptionJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.rx.RxStore
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
class ViewReceivedGiftViewModel(
sentFrom: RecipientId,
private val messageId: Long,
repository: ViewGiftRepository,
val badgeRepository: BadgeRepository
) : ViewModel() {
companion object {
private val TAG = Log.tag(ViewReceivedGiftViewModel::class.java)
}
private val store = RxStore(ViewReceivedGiftState())
private val disposables = CompositeDisposable()
val state: Flowable<ViewReceivedGiftState> = store.stateFlowable
init {
disposables += Recipient.observable(sentFrom).subscribe { recipient ->
store.update { it.copy(recipient = recipient) }
}
disposables += repository.getGiftBadge(messageId).subscribe { giftBadge ->
store.update {
it.copy(giftBadge = giftBadge)
}
}
disposables += repository
.getGiftBadge(messageId)
.firstOrError()
.flatMap { repository.getBadge(it) }
.subscribe { badge ->
val otherBadges = Recipient.self().badges.filterNot { it.id == badge.id }
val hasOtherBadges = otherBadges.isNotEmpty()
val displayingBadges = SignalStore.donationsValues().getDisplayBadgesOnProfile()
val displayingOtherBadges = hasOtherBadges && displayingBadges
store.update {
it.copy(
badge = badge,
hasOtherBadges = hasOtherBadges,
displayingOtherBadges = displayingOtherBadges,
controlState = if (displayingBadges) ViewReceivedGiftState.ControlState.FEATURE else ViewReceivedGiftState.ControlState.DISPLAY
)
}
}
}
override fun onCleared() {
disposables.dispose()
}
fun setChecked(isChecked: Boolean) {
store.update { state ->
state.copy(
userCheckSelection = isChecked
)
}
}
fun redeem(): Completable {
val snapshot = store.state
return if (snapshot.controlState != null && snapshot.badge != null) {
if (snapshot.controlState == ViewReceivedGiftState.ControlState.DISPLAY) {
badgeRepository.setVisibilityForAllBadges(snapshot.getControlChecked()).andThen(awaitRedemptionCompletion(false))
} else if (snapshot.getControlChecked()) {
awaitRedemptionCompletion(true)
} else {
awaitRedemptionCompletion(false)
}
} else {
Completable.error(Exception("Cannot enqueue a redemption without a control state or badge."))
}
}
private fun awaitRedemptionCompletion(setAsPrimary: Boolean): Completable {
return Completable.create {
Log.i(TAG, "Enqueuing gift redemption and awaiting result...", true)
var finalJobState: JobTracker.JobState? = null
val countDownLatch = CountDownLatch(1)
DonationReceiptRedemptionJob.createJobChainForGift(messageId, setAsPrimary).enqueue { _, state ->
if (state.isComplete) {
finalJobState = state
countDownLatch.countDown()
}
}
try {
if (countDownLatch.await(10, TimeUnit.SECONDS)) {
when (finalJobState) {
JobTracker.JobState.SUCCESS -> {
Log.d(TAG, "Gift redemption job chain succeeded.", true)
it.onComplete()
}
JobTracker.JobState.FAILURE -> {
Log.d(TAG, "Gift redemption job chain failed permanently.", true)
it.onError(DonationError.genericBadgeRedemptionFailure(DonationErrorSource.GIFT_REDEMPTION))
}
else -> {
Log.w(TAG, "Gift redemption job chain ignored due to in-progress jobs.", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
}
}
} else {
Log.w(TAG, "Timeout awaiting for gift token redemption and profile refresh", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
}
} catch (e: InterruptedException) {
Log.w(TAG, "Interrupted awaiting for gift token redemption and profile refresh", true)
it.onError(DonationError.timeoutWaitingForToken(DonationErrorSource.GIFT_REDEMPTION))
}
}
}
class Factory(
private val sentFrom: RecipientId,
private val messageId: Long,
private val repository: ViewGiftRepository,
private val badgeRepository: BadgeRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(ViewReceivedGiftViewModel(sentFrom, messageId, repository, badgeRepository)) as T
}
}
}

View File

@@ -0,0 +1,95 @@
package org.thoughtcrime.securesms.badges.gifts.viewgift.sent
import android.os.Bundle
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.gifts.viewgift.ViewGiftRepository
import org.thoughtcrime.securesms.badges.models.BadgeDisplay112
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.LifecycleDisposable
/**
* Handles all interactions for received gift badges.
*/
class ViewSentGiftBottomSheet : DSLSettingsBottomSheetFragment() {
companion object {
private const val ARG_GIFT_BADGE = "arg.gift.badge"
private const val ARG_SENT_TO = "arg.sent.to"
@JvmStatic
fun show(fragmentManager: FragmentManager, messageRecord: MmsMessageRecord) {
ViewSentGiftBottomSheet().apply {
arguments = Bundle().apply {
putParcelable(ARG_SENT_TO, messageRecord.recipient.id)
putByteArray(ARG_GIFT_BADGE, messageRecord.giftBadge!!.toByteArray())
}
show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}
private val sentTo: RecipientId
get() = requireArguments().getParcelable(ARG_SENT_TO)!!
private val giftBadge: GiftBadge
get() = GiftBadge.parseFrom(requireArguments().getByteArray(ARG_GIFT_BADGE))
private val lifecycleDisposable = LifecycleDisposable()
private val viewModel: ViewSentGiftViewModel by viewModels(
factoryProducer = { ViewSentGiftViewModel.Factory(sentTo, giftBadge, ViewGiftRepository()) }
)
override fun bindAdapter(adapter: DSLSettingsAdapter) {
BadgeDisplay112.register(adapter)
lifecycleDisposable.bindTo(viewLifecycleOwner)
lifecycleDisposable += viewModel.state.observeOn(AndroidSchedulers.mainThread()).subscribe { state ->
adapter.submitList(getConfiguration(state).toMappingModelList())
}
}
private fun getConfiguration(state: ViewSentGiftState): DSLConfiguration {
return configure {
noPadTextPref(
title = DSLSettingsText.from(
stringId = R.string.ViewSentGiftBottomSheet__thanks_for_your_support,
DSLSettingsText.CenterModifier, DSLSettingsText.TitleLargeModifier
)
)
space(DimensionUnit.DP.toPixels(8f).toInt())
if (state.recipient != null) {
noPadTextPref(
title = DSLSettingsText.from(
charSequence = getString(R.string.ViewSentGiftBottomSheet__youve_gifted_a_badge, state.recipient.getDisplayName(requireContext())),
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(30f).toInt())
}
if (state.badge != null) {
customPref(
BadgeDisplay112.Model(
badge = state.badge
)
)
}
}
}
}

View File

@@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.badges.gifts.viewgift.sent
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.recipients.Recipient
data class ViewSentGiftState(
val recipient: Recipient? = null,
val badge: Badge? = null
)

View File

@@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.badges.gifts.viewgift.sent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import org.thoughtcrime.securesms.badges.gifts.viewgift.ViewGiftRepository
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.rx.RxStore
class ViewSentGiftViewModel(
sentFrom: RecipientId,
giftBadge: GiftBadge,
repository: ViewGiftRepository
) : ViewModel() {
private val store = RxStore(ViewSentGiftState())
private val disposables = CompositeDisposable()
val state: Flowable<ViewSentGiftState> = store.stateFlowable
init {
disposables += Recipient.observable(sentFrom).subscribe { recipient ->
store.update { it.copy(recipient = recipient) }
}
disposables += repository.getBadge(giftBadge).subscribe { badge ->
store.update {
it.copy(
badge = badge
)
}
}
}
override fun onCleared() {
disposables.dispose()
}
class Factory(
private val sentFrom: RecipientId,
private val giftBadge: GiftBadge,
private val repository: ViewGiftRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(ViewSentGiftViewModel(sentFrom, giftBadge, repository)) as T
}
}
}

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