Compare commits

..

975 Commits

Author SHA1 Message Date
Cody Henthorne 0e9344c8e3 Bump version to 5.42.1 2022-06-29 19:34:53 -04:00
Cody Henthorne d562ba090e Updated language translations. 2022-06-29 19:32:21 -04:00
Jim Gustafson 9c665d3a71 Update to RingRTC v2.20.10.1 2022-06-29 19:28:21 -04:00
Cody Henthorne f2e919f39f Bump version to 5.42.0 2022-06-29 15:43:53 -04:00
Cody Henthorne 19080a8a5e Updated language translations. 2022-06-29 15:36:18 -04:00
Greyson Parrelli 61ce39b5b6 Improve implementation and testing on PNP contact merging. 2022-06-29 15:32:26 -04:00
Alex Hart c64be82710 Add context menus to story contacts in contact selection. 2022-06-29 15:32:25 -04:00
Alex Hart 7bd34d2b99 Reimplement contact chips with a recyclerview. 2022-06-29 15:32:25 -04:00
Cody Henthorne 4215b0391d Fix leak in Message Details for disappearing messages. 2022-06-29 15:32:25 -04:00
Cody Henthorne 96ea4c0cc2 Fix gift plurals resource. 2022-06-29 15:32:25 -04:00
Cody Henthorne 1129ca28fb Revert "Disable voice note proximity sensor when using bluetooth headset. (#2448)"
This reverts commit 9c7a5e3cc8.
2022-06-29 15:32:25 -04:00
Alex Hart ba6e1b5dd5 Fix attachment deduplication issue with Stories. 2022-06-29 15:32:25 -04:00
Cody Henthorne ed25be2e23 Fix couple more places where Note to Self should be used. 2022-06-29 15:32:25 -04:00
Cody Henthorne 7a0bd3315b Update release channel with material 3 changes. 2022-06-29 15:32:25 -04:00
Alex Hart 8b806a8ac5 Isolate and add unit testing to new link logic.
Co-Authored-By: ylpoonlg <56300571+ylpoonlg@users.noreply.github.com>
2022-06-29 15:32:25 -04:00
Alex Hart 0ac5782f1f Ensure stub is never resolved if not needed. 2022-06-29 15:32:25 -04:00
Alex Hart e10c20ffd7 Fix issue with getUnreadStories query. 2022-06-29 15:32:25 -04:00
ylpoonlg 86227fbd67 Fix url trailing symbol.
Fixes #12309
Fixes #10898
Fixes #11310
2022-06-29 15:32:25 -04:00
Alex Hart 1cfa5c31f2 Implement correct video story sound behaviour. 2022-06-29 15:32:25 -04:00
Alex Hart 521bd2cce4 Implement first-time-nav screen for stories. 2022-06-29 15:32:25 -04:00
Alex Hart 858c7a7f2e Implement "unviewed only" mode for story viewer. 2022-06-29 15:32:25 -04:00
Cody Henthorne 89a6730efe Add Storage Service plugin to Spinner. 2022-06-29 15:32:25 -04:00
Cody Henthorne 9bc25132c3 Add new My Story privacy settings. 2022-06-29 15:32:25 -04:00
Alex Hart ebc556801e Ensure story media is only uploaded once. 2022-06-29 15:32:25 -04:00
Alex Hart 6b745ba58a Allow swipe up to close viewer when viewing last story. 2022-06-29 15:32:25 -04:00
Alex Hart 6ddb5b983f Implement proper error handling for charge failure on initial subscription attempt. 2022-06-29 15:32:25 -04:00
Alex Hart 8efd07b3e2 Fix diplay issue with note to self banner. 2022-06-29 15:32:25 -04:00
Alex Hart e85adad2b4 Add safety net for when the user has disabled their contacts app. 2022-06-29 15:32:25 -04:00
Alex Hart 678a6f86ab Change several creations of alertdialogs to use materialalertdialogbuilder. 2022-06-29 15:32:25 -04:00
Jim Gustafson 9dc061e64f Update to RingRTC v2.20.10 2022-06-29 15:32:25 -04:00
Frazer Smith 2fed3f7e90 Update github actions with latest versions.
Closes #12294
2022-06-29 15:32:25 -04:00
Alex Hart af362736de Update help categories. 2022-06-29 15:32:25 -04:00
Cody Henthorne d39a4b14e7 Only add one sustainer request message per release notes update. 2022-06-29 15:32:25 -04:00
Alex Hart 6a385c7a22 Implement video length enforcement for Stories. 2022-06-28 15:42:15 -04:00
Alex Hart 2c3d8337c3 Include self in recents section. 2022-06-28 15:42:15 -04:00
Alex Hart 28feba6a6c Add proper catch for ISE in video thumb extractor. 2022-06-28 15:42:15 -04:00
Greyson Parrelli 6ec7834046 Add the ability to see replies. 2022-06-28 15:42:15 -04:00
Alex Hart ee4f3abf22 Add unit testing for pinned last message deletion fix. 2022-06-28 15:42:14 -04:00
Alex Hart dc66583ef1 Update camera UX to match Material3 Spec. 2022-06-28 15:42:14 -04:00
Alex Hart d30714bfd4 Update coloring of capture first flow toggle.: 2022-06-28 15:42:14 -04:00
Alex Hart d04d2f7e93 Fix bad centering of emoji button in add message fragment. 2022-06-28 15:42:14 -04:00
Alex Hart 1328aab939 Add material3 coloring to story reply dialog. 2022-06-28 15:42:14 -04:00
Alex Hart 2a9d2cf580 Remove bottomsheet elevation tinting. 2022-06-28 15:42:14 -04:00
Jim Gustafson a316650aee Update to RingRTC v2.20.9 2022-06-28 15:42:14 -04:00
Alex Hart 4d1e8b8f75 Update several story ui elements for Material3. 2022-06-28 15:42:14 -04:00
Alex Hart 9c7a5e3cc8 Disable voice note proximity sensor when using bluetooth headset. (#2448) 2022-06-28 15:42:14 -04:00
Alex Hart 2022dae37a Draw pulse outliner in onDrawForeground instead of in onDraw. 2022-06-28 15:42:14 -04:00
Chris Eager 05b7055678 Update device-transfer app build to work with the latest libsignal 2022-06-28 15:42:14 -04:00
Alex Hart 53c60e1f6d Add proper coloring to send buttons. 2022-06-28 15:42:14 -04:00
Alex Hart cd8fa58d7e Fix voice note playback bar for RTL regions. 2022-06-28 15:42:14 -04:00
Cody Henthorne c2ffc8332d Bump version to 5.41.11 2022-06-28 15:41:43 -04:00
Cody Henthorne 343a49fa26 Updated language translations. 2022-06-28 15:41:20 -04:00
Cody Henthorne 2c700c7e0e Fix broken Material3 changes on Android 6. 2022-06-28 15:20:19 -04:00
Alex Hart 105d0c778c Bump version to 5.41.10 2022-06-23 17:10:08 -03:00
Alex Hart d8bf2392ae Updated language translations. 2022-06-23 17:09:32 -03:00
Cody Henthorne 4585b439d5 Remove notification creation in WebRtcCallSerivce onCreate. 2022-06-23 15:55:57 -04:00
Alex Hart 587aa49db8 Bump version to 5.41.9 2022-06-22 14:51:18 -03:00
Alex Hart 1ef576f6f8 Updated language translations. 2022-06-22 14:50:37 -03:00
Greyson Parrelli d070ebcd2f Fix splash screen when app theme mismatches system theme. 2022-06-22 09:02:15 -04:00
Alex Hart 2c779e700d Adjust message request padding for better localization support. 2022-06-22 09:52:01 -03:00
Alex Hart feadde8737 Bump version to 5.41.8 2022-06-21 19:16:38 -03:00
Alex Hart 64dca6f60b Updated language translations. 2022-06-21 19:16:17 -03:00
Greyson Parrelli 4c4cfe917d Always ensure the send type matches the send button. 2022-06-21 19:11:27 -03:00
Alex Hart 852989ce48 Manually set nav bar background to 50 transparent black with wallpaper. 2022-06-21 19:11:27 -03:00
Alex Hart 3cecd503ab Bump version to 5.41.7 2022-06-21 15:45:16 -03:00
Alex Hart d8b97d8f87 Updated language translations. 2022-06-21 15:44:50 -03:00
Alex Hart 611950a589 Utilize translucent navigation bar for now. 2022-06-21 14:40:35 -03:00
Alex Hart 73be74dac1 Bump version to 5.41.6 2022-06-20 16:37:30 -03:00
Alex Hart 94fc7ad3c0 Updated language translations. 2022-06-20 16:36:37 -03:00
Greyson Parrelli 290fbbb9ee Update backoff logic of ClearFallbackKbsEnclaveJob. 2022-06-20 15:30:50 -04:00
Alex Hart c0735c8119 Clear snippet when the last message in a pinned thread is deleted. 2022-06-20 16:15:24 -03:00
Greyson Parrelli 8f5fc83529 Remove inactive KBS fallback. 2022-06-20 12:20:23 -04:00
Alex Hart ac2cbba067 Fix pin reminder dialog submit button. 2022-06-20 10:15:27 -03:00
Alex Hart 1bcfbaf16e Fix bottom nav overlay issue with react-with-any sheet. 2022-06-20 10:10:51 -03:00
Greyson Parrelli c950c2bdd2 Fix reaction overlay issue in dark theme. 2022-06-19 11:18:53 -04:00
Greyson Parrelli 38cecf68b5 Bump version to 5.41.5 2022-06-17 19:27:34 -04:00
Greyson Parrelli f932ed6c6a Updated language translations. 2022-06-17 19:21:40 -04:00
Greyson Parrelli 0209db4531 Show/hide attachment keyboard with the reaction overlay. 2022-06-17 19:13:14 -04:00
Greyson Parrelli 2e9f43cf94 Fix gap in reaction overlay.
I use the term 'fix' lightly. Used a stupid hack that we should revisit.
2022-06-17 18:57:52 -04:00
Alex Hart 897e176f0d Revert shade removal and add nav bar coloring. 2022-06-17 14:22:14 -03:00
Greyson Parrelli fcb4c627e4 Bump version to 5.41.4 2022-06-17 11:06:19 -04:00
Greyson Parrelli c85076138a Updated language translations. 2022-06-17 11:05:44 -04:00
Greyson Parrelli 8877603e13 Fix another possible crash with available message types. 2022-06-17 11:05:44 -04:00
Alex Hart ae6ca49e4e Fix toolbar overlap in all media screen. 2022-06-17 11:05:44 -04:00
Greyson Parrelli 2620a8fc51 Address corner case where contact details may not be synced.
Relates to #12293
2022-06-17 11:05:44 -04:00
Alex Hart 008f153b66 Adjust wallpaper preview. 2022-06-17 11:05:44 -04:00
Alex Hart 539a0182e0 Fix navigation bar color issues. 2022-06-17 11:05:44 -04:00
Alex Hart ff64f7368b Update background color for attachment keyboard. 2022-06-17 11:05:44 -04:00
Greyson Parrelli 211361684d Bump version to 5.41.3 2022-06-16 13:22:39 -04:00
Greyson Parrelli 268b00bbf9 Updated language translations. 2022-06-16 13:22:39 -04:00
Alex Hart a593bc0b7a Implement composer tweaks to allow for better contrast. 2022-06-16 13:22:39 -04:00
Greyson Parrelli b6d1af3760 Add possible fix for weird send button state. 2022-06-16 12:02:36 -04:00
Alex Hart 3acbcf54db Fix wrong color flashing when scrolling conversation settings. 2022-06-16 12:02:36 -04:00
Greyson Parrelli 673a8f540b Fix some lifecycle-related crashes. 2022-06-16 12:02:36 -04:00
Alex Hart 69e2a138d9 Fix in-call audio output picker dialog. 2022-06-16 12:02:36 -04:00
Sgn-32 11c6e748f7 Use MaterialAlertDialogBuilder in AddToGroupsActivity.
Closes #12291
2022-06-16 10:59:42 -04:00
Greyson Parrelli 33187ea12f Update the color preview to tint the send button. 2022-06-16 10:47:19 -04:00
Greyson Parrelli 6c5ceab4e5 Bump version to 5.41.2 2022-06-15 11:57:02 -04:00
Greyson Parrelli f2dc454727 Updated language translations. 2022-06-15 11:57:02 -04:00
Greyson Parrelli 8cb0898f1f Capitalize log field. 2022-06-15 11:57:02 -04:00
Greyson Parrelli 2a2809c17c Update send button color after chat color change. 2022-06-15 11:57:02 -04:00
Greyson Parrelli 9eeecaa73d Initialize WAL mode earlier. 2022-06-15 11:57:02 -04:00
Alex Hart c83a888ed0 Fix banner input overlap in some situations. 2022-06-15 11:57:02 -04:00
Alex Hart 6854632fec Allow separate specification of status and toolbar active/inactive coloring. 2022-06-15 09:45:37 -03:00
Greyson Parrelli e6cc49368e Update some dialogs to MaterialAlertDialog. 2022-06-15 08:32:20 -04:00
Greyson Parrelli 18bf00eb7a Bump version to 5.41.1 2022-06-14 17:47:08 -04:00
Greyson Parrelli fcef6f965d Fix crash that can occur when using non-standard font sizes. 2022-06-14 17:32:26 -04:00
Greyson Parrelli fb9a9b7c96 Bump version to 5.41.0 2022-06-14 15:20:43 -04:00
Greyson Parrelli d662bddeb1 Updated language translations. 2022-06-14 15:20:43 -04:00
Greyson Parrelli c5afeb6d71 Update contact photo syncing for linked devices. 2022-06-14 15:20:43 -04:00
Greyson Parrelli c66a2b8c61 Add autoVerify to some intent filters. 2022-06-14 15:20:43 -04:00
Alex Hart 88a66b49ff Apply new story list ordering rules.
Co-authored-by: Cody Henthorne <cody@signal.org>
2022-06-14 15:20:43 -04:00
Alex Hart 3b07f4a8ca Do not wait on content to launch story viewer. 2022-06-14 15:20:43 -04:00
Alex Hart f6fd1e1c91 Fix strange scale behaviour on long press of conversation item. 2022-06-14 15:20:42 -04:00
Alex Hart 2412f6f63a Fix outgoing quote over media. 2022-06-14 15:20:42 -04:00
Greyson Parrelli ce1983a3b1 Updated libphonenumber to 8.12.50 2022-06-14 15:20:42 -04:00
Greyson Parrelli 523f9c7409 Be more resistent to android disallowing service starts. 2022-06-14 15:20:42 -04:00
Cody Henthorne d5d7c73ebf Remove bad quantity strings. 2022-06-14 15:20:42 -04:00
Cody Henthorne ce93537fee Update incoming call handling.
* Fix crash with incoming ringer when custom ringtone isn't found.
* Stop notification profiles from terminating calls on linked devices.
2022-06-14 15:20:42 -04:00
Cody Henthorne 5df20d755a Fix FCM not initialized crash. 2022-06-14 15:20:42 -04:00
Alex Hart 2eb933c2d4 Implement animated color lerp for material toolbars. 2022-06-14 15:20:42 -04:00
Alex Hart ef3c776b4b Fix reaction pill background color. 2022-06-14 15:20:42 -04:00
Alex Hart bf156ad7d2 Apply Material3 spec to dialogs. 2022-06-14 15:20:42 -04:00
Alex Hart 56a2b27745 Refactor reactions dialog to match Material3 spec. 2022-06-14 15:20:42 -04:00
Rashad Sookram 0e7ace0da4 Remove unused libsignal files from APK. 2022-06-09 12:00:24 -04:00
Alex Hart 6743861630 Account for archival and meaningful message status in unread count query. 2022-06-09 12:40:35 -03:00
Alex Hart 92c6a84075 Ensure shared background for all generated text stories in a set. 2022-06-09 09:20:04 -03:00
Alex Hart b8a7748dc1 Update verify safety number display fragment. 2022-06-08 17:00:51 -03:00
Alex Hart 8b5c630303 Adjust padding below indicator. 2022-06-08 16:42:08 -03:00
Alex Hart 9ac5db2f0c Remove more solid icons. 2022-06-08 16:37:56 -03:00
Alex Hart a7380b33c7 Add proper padding and outline to invite sheet. 2022-06-08 16:30:55 -03:00
Alex Hart 4779096ac5 Update reaction pill colors. 2022-06-08 16:26:05 -03:00
Alex Hart 43be54ec42 Fix padding on expired messages save button. 2022-06-08 15:41:08 -03:00
Alex Hart 7010985be8 Update colors for scroll-to buttons to match material3 spec. 2022-06-06 13:20:19 -03:00
Alex Hart 5080dd4c4b Update emoji keyboard to be aligned with Material3 spec. 2022-06-06 13:08:17 -03:00
Alex Hart 1b1acf0aa5 Modify colorOnSurfaceVariant for dark themes. 2022-06-06 12:23:19 -03:00
Alex Hart 5527269283 Update quoteview background colors. 2022-06-06 12:21:39 -03:00
Alex Hart af32e156c2 Update message details fragment with material3 spec. 2022-06-06 12:12:13 -03:00
Felix Nüsse 9c7c94b2d4 Allow camera to rotate even when screen is locked
Fixes #8611
Closes #12247

Signed-off-by: Felix Nüsse <felix.nuesse@t-online.de>
2022-06-06 08:51:03 -04:00
Alex Hart 796e5f6f86 Add proper tinting to typing indicator. 2022-06-06 09:45:31 -03:00
Sgn-32 b282b775d0 Add LogSectionSMS to debug log.
Closes #12273
2022-06-05 12:25:26 -04:00
Greyson Parrelli 4da422fd3c Refactor how message send types are selected. 2022-06-03 18:07:29 -04:00
Jim Gustafson bf90909496 Update to RingRTC v2.20.8 2022-06-03 08:22:23 -07:00
Alex Hart 7aa99ce9a7 Remove a few more unnecessary styles. 2022-06-02 11:52:10 -04:00
Alex Hart b6767b02ed Remove several old and unnecessary styles. 2022-06-02 11:52:10 -04:00
Alex Hart cf5f7ef634 Update styles on several group bottom sheets. 2022-06-02 11:52:10 -04:00
Alex Hart 3ca4ff9a94 Update tonal buttons to utilize primaryContainer. 2022-06-02 11:52:10 -04:00
Alex Hart 28edd18e55 Fix quote text sizing. 2022-06-02 11:52:10 -04:00
Alex Hart 7bf2ae3d5e Fix-up iconography in recipient bottom sheet. 2022-06-02 11:52:10 -04:00
Alex Hart 7896a525f2 Fix devicelistfragment and remove two dependencies. 2022-06-02 11:52:10 -04:00
Alex Hart f2d5bfe51d Fix overlap of multiselect in toolbar. 2022-06-02 11:52:10 -04:00
Alex Hart b2b6f98294 Fix launch responsiveness of story viewer. 2022-06-02 11:52:10 -04:00
Alex Hart 4758369f79 Fix background of sticker management row item. 2022-06-02 11:52:10 -04:00
Alex Hart e10e629d13 Add proper text size to LabelMedium. 2022-06-02 11:52:10 -04:00
Alex Hart 0e9e39a4eb Bump ConversationListItem avatar down by 4dp. 2022-06-02 11:52:10 -04:00
Alex Hart 93e5052d6b Fix bad donor badge input behaviour. 2022-06-02 11:52:10 -04:00
Alex Hart 1b471e163d Implement new Material3 spec. 2022-06-02 11:52:10 -04:00
Greyson Parrelli 556e480b06 Bump version to 5.40.4 2022-06-02 11:50:22 -04:00
Greyson Parrelli d08bee3413 Updated language translations. 2022-06-02 11:49:57 -04:00
Cody Henthorne e83cb6fa8b Fix QR scanning bug when using camerax. 2022-06-02 11:42:25 -04:00
Greyson Parrelli 499cdd9f29 Make Github action build a specific variant. 2022-06-02 08:50:21 -04:00
Greyson Parrelli 13aa150206 Bump version to 5.40.3 2022-06-02 00:32:17 -04:00
Greyson Parrelli 63d6bab7d6 Updated language translations. 2022-06-02 00:31:39 -04:00
Cody Henthorne d6108fbbf3 Add force legacy QR scanning switch. 2022-06-01 16:38:15 -04:00
Cody Henthorne 4c44f1ee02 Tweak new QR processing some more. 2022-06-01 14:39:42 -04:00
Greyson Parrelli f4c728f57c Bump version to 5.40.2 2022-05-31 10:16:30 -04:00
Greyson Parrelli 58cebf7346 Updated language translations. 2022-05-31 10:15:40 -04:00
Cody Henthorne 2446792c62 Tweak QR code capture configuration. 2022-05-30 15:37:01 -04:00
Cody Henthorne 259a86b605 Fix lost scroll position in conversation list bug. 2022-05-30 14:33:04 -04:00
Jim Gustafson fafe795f39 Update to RingRTC v2.20.7 2022-05-27 13:50:00 -07:00
Alex Hart 9a9636b58f Bump version to 5.40.1 2022-05-27 16:57:05 -03:00
Alex Hart d48a686d98 Updated language translations. 2022-05-27 16:56:13 -03:00
Cody Henthorne d69d1c8967 Fix IAE crash in link device transition. 2022-05-27 09:51:20 -04:00
Alex Hart 60b6a9ff3f Prevent crash when ConversationListFragment list is nullified. 2022-05-27 09:55:12 -03:00
Alex Hart f85803c1fe Bump version to 5.40.0 2022-05-26 14:24:57 -03:00
Alex Hart 9dea815fce Updated language translations. 2022-05-26 14:24:57 -03:00
Cody Henthorne 4e01336b2f Fix unclosed streams during backup export. 2022-05-26 14:24:57 -03:00
Cody Henthorne 9fbc7c0f65 Fix stories viewed not updating in UI. 2022-05-26 14:24:57 -03:00
Cody Henthorne 4d028d1867 Improve messaging around story send failures. 2022-05-26 14:24:57 -03:00
Cody Henthorne 95a46f1ce5 Show user a toast when an unexpected send text story fails. 2022-05-26 14:24:57 -03:00
Cody Henthorne 26a84c5546 Show calling service notification immediately. 2022-05-26 14:24:57 -03:00
Cody Henthorne 652d0d46ed Add foreground service type to WebRtcCallService. 2022-05-26 14:24:57 -03:00
Cody Henthorne e0f3e34899 Attempt to get service name in start foreground exception stack trace. 2022-05-26 14:24:57 -03:00
Cody Henthorne 4a8083f7b1 Fix Vivo NPE quirk. 2022-05-26 14:24:57 -03:00
Cody Henthorne 08556b111b Fix ISE crash. 2022-05-26 14:24:57 -03:00
Cody Henthorne 7e7bc13b62 Swallow too many pending intents exception. 2022-05-26 14:24:56 -03:00
Cody Henthorne 5115eb125d Fix conversation search not showing after entering via settings. 2022-05-26 14:24:56 -03:00
Ahmad Alturki 3ec55b24f8 Fix swapped group title & avatar on rtl layout.
Fixes #12612
2022-05-26 14:24:56 -03:00
Cody Henthorne 5dba1067d6 Fix conversation list memory leak. 2022-05-26 14:24:56 -03:00
Cody Henthorne 2a91c67c51 Add sending and error states for story group replies. 2022-05-26 14:24:56 -03:00
Alex Hart a29bc1da8c Add proper hyphenation break to badge name. 2022-05-26 14:24:56 -03:00
Alex Hart 32b4d11a82 Fix crash in onPlaying if fragment is detached. 2022-05-26 14:24:56 -03:00
Jim Gustafson f013f7357f Update to RingRTC v2.20.6 2022-05-26 14:24:56 -03:00
Alex Hart e37150e98a Update capabilities logging. 2022-05-26 14:24:56 -03:00
Alex Hart eaa7262b2f Add debug log entry for video player pool usage. 2022-05-26 14:24:56 -03:00
Alex Hart 63f4f0bcec Fix note to self label in conversation settings. 2022-05-26 14:24:56 -03:00
Alex Hart 6dec6cef27 Add decline code messages into expiration sheet. 2022-05-24 15:03:54 -03:00
Greyson Parrelli 4d8faffb75 Notify recipient changes after bulk registration update. 2022-05-24 15:03:54 -03:00
Alex Hart fa6bb07e8a Update strings for unclear translations. 2022-05-24 15:03:54 -03:00
Cody Henthorne d260c48393 Fix device linking issues on newer devices. 2022-05-24 15:03:54 -03:00
Cody Henthorne cc31417c97 Fix desugar crash on spinner builds. 2022-05-24 15:03:54 -03:00
Alex Hart 4d2af5b536 Rotate gift badges flag. 2022-05-24 15:03:54 -03:00
Alex Hart 6029c8ae4a Bump version to 5.38.3 2022-05-24 14:31:18 -03:00
Alex Hart 3bc18c3300 Updated language translations. 2022-05-24 14:30:15 -03:00
Cody Henthorne a652bc65cc Fix font version check timeout. 2022-05-23 22:06:18 -04:00
Cody Henthorne 53252aa797 Bump version to 5.39.2 2022-05-19 16:36:06 -04:00
Cody Henthorne 956c1d96af Updated language translations. 2022-05-19 16:31:46 -04:00
Greyson Parrelli d0ecbda962 Hide attachment keyboard if system keyboard shows. 2022-05-19 08:53:12 -04:00
Greyson Parrelli 5d880e2b2a Fix bug where searching emoji would dismiss the view. 2022-05-19 08:50:40 -04:00
Greyson Parrelli bb13be1e7a Bump version to 5.39.1 2022-05-18 17:57:56 -04:00
Greyson Parrelli 05975a0068 Fix scrolling to last seen. 2022-05-18 17:51:00 -04:00
Greyson Parrelli 153feb002e Update R8 to 3.3.28 2022-05-18 17:34:04 -04:00
Cody Henthorne f63ed8f269 Bump version to 5.39.0 2022-05-18 14:18:31 -04:00
Cody Henthorne 139a503403 Updated language translations. 2022-05-18 14:11:46 -04:00
Cody Henthorne db4d072bd9 Upgrade kotlin to 1.6.21
Also fix a collection of warnings.
2022-05-18 14:05:17 -04:00
Cody Henthorne 42b0842aab Fix ANR when showing media in notifications. 2022-05-18 11:54:17 -04:00
Greyson Parrelli 9ab275195f Add support for CDSI. 2022-05-18 11:54:17 -04:00
Alex Hart 8407f2ff69 Add guard against out of bounds indices in story viewer. 2022-05-18 11:54:17 -04:00
Greyson Parrelli 9d518879dd Update libsignal-client to 0.17.0 2022-05-18 11:54:17 -04:00
Alex Hart 588663b3c2 Add better handling for unexpected cancellations. 2022-05-18 11:54:17 -04:00
Alex Hart 77f8489e51 Scroll to top on chat press when already on that tab. 2022-05-18 11:54:17 -04:00
Cody Henthorne 3c08b070fc Fetch PNI Credential during own profile refresh. 2022-05-18 11:54:17 -04:00
Greyson Parrelli dda5ce4809 Add basic CDSv2 database writes and unit tests. 2022-05-18 11:54:17 -04:00
Alex Hart 307be5c75e Ensure callback is registered for shaking gifts. 2022-05-18 11:54:17 -04:00
Alex Hart a0b89051cf Add duration info to gift row item. 2022-05-18 11:54:17 -04:00
Alex Hart a1025a8e9a Add expiry information to gift conversation items. 2022-05-18 11:54:17 -04:00
Alex Hart ce2418ce9f Consolidate local badge writes. 2022-05-18 11:54:17 -04:00
Cody Henthorne ec3540e200 Fix long text in Safety Number Change dialog. 2022-05-18 11:54:17 -04:00
Alex Hart ff4311d114 Add outline around sent gift reply thumb. 2022-05-18 11:54:17 -04:00
Alex Hart 425a13e68c Mark sent gift viewed when opened. 2022-05-18 11:54:17 -04:00
Alex Hart 15af1d3bd1 Add default animations to gift flow. 2022-05-18 11:54:17 -04:00
Alex Hart 2d57cb4ed0 Enqueue profile refresh sync after badge redemption. 2022-05-18 11:54:17 -04:00
Alex Hart 25788ef751 Do not include self in recents list for gift badging. 2022-05-18 11:54:17 -04:00
Greyson Parrelli b3086e595f Fix abbreviations with some emoji.
Fixes #12212
2022-05-18 11:54:17 -04:00
Greyson Parrelli 57e233413a Update string for group invite. 2022-05-18 11:54:17 -04:00
Greyson Parrelli f5777d58fc Fix situation where two keyboards could be showing. 2022-05-18 11:54:17 -04:00
Alex Hart 6b55cd0128 Always pop open keyboard when opening group reply sheet. 2022-05-18 11:54:17 -04:00
Alex Hart a03c49e12c Implement group story notifications. 2022-05-18 11:54:17 -04:00
Alex Hart 01543dd52b Utilize round outline for deleted messages. 2022-05-18 11:54:17 -04:00
Alex Hart 987f69227a Add polish to story replies button and direct reply sheet. 2022-05-18 11:54:17 -04:00
Alex Hart e51841a28b Fix freeze of first story first post. 2022-05-18 11:54:17 -04:00
Alex Hart bcfe2fef72 Hide gift badge row if user does not have capability set and rotate flag. 2022-05-18 11:54:17 -04:00
Alex Hart 9ed3f95ab8 Ignore duplicate stories in sync messages. 2022-05-18 11:54:17 -04:00
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
Greyson Parrelli 65a12767f9 Bump version to 5.34.3 2022-03-21 18:46:52 -04:00
Greyson Parrelli ab9d813636 Updated language translations. 2022-03-21 18:46:07 -04:00
Cody Henthorne 007975e7da Track inconsistencies between new and old network availability for internal users. 2022-03-21 17:21:42 -04:00
Greyson Parrelli 86ca1ebda0 Add basic handling for ProofRequiredExceptions on other message types. 2022-03-21 16:36:07 -04:00
Cody Henthorne 57fb3e6377 Fix crash when accidently triggering a second voice note record. 2022-03-21 13:36:03 -04:00
Cody Henthorne 7e6fcb80a3 Revert all new network detection API usage and refactorings. 2022-03-21 12:21:46 -04:00
Cody Henthorne 5e46e1e3d9 Bump version to 5.34.2 2022-03-18 15:48:26 -04:00
Cody Henthorne 21e370de9b Updated language translations. 2022-03-18 15:45:33 -04:00
Cody Henthorne 77ef877c59 Show block request if request follows a collapsed event. 2022-03-18 15:26:38 -04:00
Greyson Parrelli 77caedb3bb Avoid recipient resolves in Recipient#getGroupName(). 2022-03-18 14:49:06 -04:00
Cody Henthorne 3e77975c17 Revert "Fix soft keyboard popping up when the text was selected when the other keyboard was open."
This reverts commit 6d41d1f6d2.
2022-03-18 14:09:52 -04:00
Alex Hart 75e15c81e1 Ensure messages are marked read when entering a conversation with an active typing indicator. 2022-03-18 13:15:37 -03:00
Cody Henthorne 782a1ce301 Fix NPE in conversation update group info. 2022-03-18 12:00:04 -04:00
Cody Henthorne be21b9e163 Bump version to 5.34.1 2022-03-18 11:14:35 -04:00
Cody Henthorne 284140871e Updated language translations. 2022-03-18 11:06:32 -04:00
Cody Henthorne e6ac40a07c Fix various corner case block/reject join request bugs. 2022-03-18 11:03:38 -04:00
Rashad Sookram b8e98350c1 Add back aapt2 checksums for osx/windows. 2022-03-18 11:03:38 -04:00
Cody Henthorne 445ff263c6 Fix receiving group invite bug. 2022-03-18 11:03:38 -04:00
Alex Hart e10c40d2b8 Fix infinite loop when dealing with large video files. 2022-03-18 11:03:38 -04:00
Alex Hart a41a2b3e64 Fix wonky bottom sheet animations. 2022-03-18 11:03:38 -04:00
Alex Hart e603391c35 Fix several theming issues for Contact Selection. 2022-03-18 10:32:03 -03:00
Cody Henthorne 7e0cd99f48 Fix block request button not showing up always. 2022-03-17 19:51:22 -04:00
Cody Henthorne daedb8261d Fix empty group update bug. 2022-03-17 19:28:50 -04:00
Cody Henthorne 2c8744a319 Bump version to 5.34.0 2022-03-17 16:18:31 -04:00
Cody Henthorne a7c441225b Updated language translations. 2022-03-17 16:06:22 -04:00
Alex Hart e3c491860a Allow forwarding of Text Stories. 2022-03-17 16:02:43 -04:00
Greyson Parrelli 43ad0b2294 Fix issue where group read was happening on main thread. 2022-03-17 16:02:43 -04:00
Alex Hart bf897d10d2 Add avatar to my story row and wire in badges. 2022-03-17 16:02:43 -04:00
Cody Henthorne 0b1a93d3e6 Disable Telecom integration solely for OnePlus devices. 2022-03-17 16:02:43 -04:00
Alex Hart 7edef20f4f Guard first time on add to my story. 2022-03-17 16:02:43 -04:00
Greyson Parrelli 945c308cf5 Update group update messages faster. 2022-03-17 16:02:43 -04:00
Cody Henthorne f91494f813 Remove newer network detection APIs. 2022-03-17 16:02:43 -04:00
Cody Henthorne 9d28caac00 Fix bug preventing adding and inviting by phone number. 2022-03-17 16:02:43 -04:00
Alex Hart 798f3a7b0e Only display tilted image for My Story. 2022-03-17 16:02:43 -04:00
Alex Hart 768e170ed4 Fix hidden story behaviour. 2022-03-17 16:02:43 -04:00
Greyson Parrelli a0ebb891de Resolve multiple times when generating static IPs.
An attempt to make the list somewhat more stable.
2022-03-17 16:02:43 -04:00
Greyson Parrelli 570b39f82e Debounce menu invalidations in conversation. 2022-03-17 16:02:43 -04:00
Alex Hart dc50899fe0 Allow addition of text to text slide by tapping anywhere. 2022-03-17 16:02:43 -04:00
Alex Hart 0f889e0259 Add progress indicator to story text post link loader. 2022-03-17 16:02:43 -04:00
Greyson Parrelli cb906edd11 More accurate timings of conversation-open component. 2022-03-17 16:02:43 -04:00
Greyson Parrelli 604f6709db Fix bug where wallpaper didn't update after changing. 2022-03-17 16:02:43 -04:00
Greyson Parrelli 0359f27cd9 Fix true update queries for blobs. 2022-03-17 16:02:43 -04:00
Greyson Parrelli 0ca438ed25 Update MSL appending to create a new entry if the original one is gone. 2022-03-17 16:02:43 -04:00
Rashad Sookram 6b6e9e92e8 Fix remote delete for private stories. 2022-03-17 16:02:43 -04:00
Greyson Parrelli b5e0991f5e Log early delivery receipts. 2022-03-17 16:02:43 -04:00
Alex Hart f06f0e7ae0 Pop open keyboard when we enter the link entry fragment. 2022-03-17 16:02:43 -04:00
Alex Hart 0fcbb5ffda Send user to My Stories from failure notification. 2022-03-17 16:02:42 -04:00
Alex Hart b1f7dbefd8 Drop stories from users we would normally show a message request for. 2022-03-17 16:02:42 -04:00
Alex Hart 8fc2d5be37 Add 32dp space to bottom of choose story type bottom sheet. 2022-03-17 16:02:42 -04:00
Alex Hart 40020728de Ensure proper text size is used when displaying and editing text stories. 2022-03-17 16:02:42 -04:00
Greyson Parrelli 4abb169568 Do not suggest SMS during onboarding. 2022-03-17 16:02:42 -04:00
Alex Hart da1ac5358f Add initial support for rendering link previews in text story previews. 2022-03-17 16:02:42 -04:00
Greyson Parrelli d504bd593a Improve wallpaper load speed. 2022-03-17 16:02:42 -04:00
Alex Hart 63e48efdfe Allow user to launch directly to a specific story, fix story chronology. 2022-03-17 16:02:42 -04:00
Greyson Parrelli 8bb27b60fa Revert "Fix data race preventing some story sends."
This reverts commit cde70269817464880ddb8e2c67e59ca8b571073b.
2022-03-17 16:02:42 -04:00
Alex Hart 437c1e2f21 Implement UI and backend for sending story reactions.
Co-authored-by: Rashad Sookram <rashad@signal.org>
2022-03-17 16:02:42 -04:00
Alex Hart 7f4a12c179 Fix issue where stories with links would fail to send. 2022-03-17 16:02:42 -04:00
Alex Hart 19d3bbc70a Order recipients in viewer by story sent date. 2022-03-17 16:02:42 -04:00
Alex Hart 559561bf72 Add support for message resends. 2022-03-17 16:02:42 -04:00
Rashad Sookram c8c0589ac4 Hide spell check errors in post preview. 2022-03-17 16:02:42 -04:00
Greyson Parrelli 666218773c Improve conversation open speed.
Co-authored-by: Cody Henthorne <cody@signal.org>
2022-03-17 16:02:42 -04:00
Cody Henthorne d3049a3433 Add block request action button to collapsed join request events. 2022-03-17 12:12:56 -04:00
Greyson Parrelli 130d5a8945 Add index to improve speed of MMS count. 2022-03-17 12:12:56 -04:00
Greyson Parrelli 172751cd42 Iterate over a snapshot of transaction listeners. 2022-03-17 12:12:56 -04:00
Rashad Sookram 3ad7c96a3c Fix ellipsis appearing in the middle of a message. 2022-03-17 12:12:56 -04:00
Fumiaki Yoshimatsu 6d41d1f6d2 Fix soft keyboard popping up when the text was selected when the other keyboard was open.
Fixes #11780
2022-03-17 12:12:56 -04:00
Rashad Sookram cb74833dc2 Fix scheduling of ExpireStoriesAlarm. 2022-03-17 12:12:56 -04:00
Alex Hart 8c7b6293fb Fix data race preventing some story sends. 2022-03-17 12:12:56 -04:00
Cody Henthorne 9d1f46da9f Collapse multiple join request/cancels when from a single person. 2022-03-17 12:12:56 -04:00
Greyson Parrelli 216059b659 Fix layout for long text in SMS verification buttons.
Fixes #12037
2022-03-17 12:12:56 -04:00
Greyson Parrelli 18392ed0a4 Render date dividers based on sent time.
The time we use to render date headers needs to match the time we use to
render timestamps in the footer. We should be using sent time in both
cases.

Fixes #11589
2022-03-17 12:12:56 -04:00
Rashad Sookram 63a4d20ea9 Keep 1:1 replies after expiry and fix queries. 2022-03-17 12:12:56 -04:00
Greyson Parrelli 057231b9c3 Update libsignal-client to 0.14.0 2022-03-17 12:12:56 -04:00
clauz9 749bbf428d Make sure isSearchRequest is true when searchViewItem is expanded.
fixes signalapp#12054
2022-03-17 12:12:56 -04:00
Alex Hart b0458f10a3 Ensure identity records are good before trying to send media. 2022-03-17 12:12:56 -04:00
Greyson Parrelli 5b91c927b6 Refresh our own profile before rotating our profile key. 2022-03-17 12:12:56 -04:00
Greyson Parrelli b45740884b Only upload your avatar if it's being changed.
New server param means we don't have to upload the avatar if we want to
keep it the same.
2022-03-17 12:12:56 -04:00
Alex Hart 87ad4be117 Fix issue where user could not select a group story. 2022-03-17 12:12:56 -04:00
Greyson Parrelli 78de70881f Fix responsiveness of profile photo edit UI.
There were various issues around the profile photo updating correctly in
the edit view. We want to make sure that what the user sees there is
what other people are seeing.

So I made some changes to make sure that when you remove your profile
photo the UI updates right away, as well as fixed most flickering
issues.
2022-03-17 12:12:56 -04:00
Greyson Parrelli e7a370a549 Fix paging issue where DataStatus was not updated on insert. 2022-03-17 12:12:56 -04:00
Alex Hart 54eb579558 Allow external shares to a story. 2022-03-17 12:12:56 -04:00
Alex Hart 732b67d8cb Allow injectable typefaces in image text editor.
Co-authored-by: Rashad Sookram <rashad@signal.org>
2022-03-17 12:12:56 -04:00
Cody Henthorne eed45b57a1 Prevent rejected/kicked group members from joining again via group link. 2022-03-17 12:12:56 -04:00
Greyson Parrelli 3503c60fd1 Add routine check to ensure GV2 profiles are up-to-date. 2022-03-17 12:12:56 -04:00
Cody Henthorne c17ba30cfc Show different messages based on join group link error header. 2022-03-17 12:12:56 -04:00
Rashad Sookram 5167c7235d Don't animate to replies tab during open. 2022-03-17 12:12:56 -04:00
Greyson Parrelli 803f94012a Handle profile key changes consistently. 2022-03-17 12:12:56 -04:00
Alex Hart 9281bcdd7d Only display stories if you entered through stories. 2022-03-17 12:12:56 -04:00
Alex Hart 4dca554967 Add better text reflow as font changes. 2022-03-17 12:12:55 -04:00
Alex Hart 7c45fb6c17 Fix issue where names with emoji would not display. 2022-03-17 12:12:55 -04:00
Alex Hart 8aa283488f Clear search query in Story recipient selection after a selection is made. 2022-03-17 12:12:55 -04:00
Alex Hart 604c65c7fb Add finalized story icon assets. 2022-03-17 12:12:55 -04:00
Alex Hart 711148423d Excise PowerMock and reenable like a bunch of ignored tests.
Co-authored-by: Rashad Sookram <rashad@signal.org>
2022-03-17 12:12:55 -04:00
Alex Hart 1f82ceecc6 Story Status for landing page and my stories. 2022-03-17 12:12:55 -04:00
Rashad Sookram 1ac8701ada Update Gradle to 7.4.1. 2022-03-17 12:12:55 -04:00
Alex Hart d61e33fdf3 Fix story display size logic. 2022-03-17 12:12:55 -04:00
Greyson Parrelli e552b5160f Implement CdshV2Service. 2022-03-17 12:12:55 -04:00
Greyson Parrelli 7e063e8ad8 Refactor CDSH to allow for code reuse. 2022-03-17 12:12:55 -04:00
Greyson Parrelli 88a34936cd Add more device info at the top of Spinner. 2022-03-17 12:12:55 -04:00
Greyson Parrelli c1181478dd Remove GV2 capability check. 2022-03-17 12:12:55 -04:00
Greyson Parrelli d13d8628b5 Small UI tweaks.
- Update distance below avatar in conversation settings.
- Slightly increase bubble corner radius.
2022-03-17 12:12:55 -04:00
Alex Hart 6048208c8c Fix crash with incorrectly tagged story. 2022-03-17 12:12:55 -04:00
Alex Hart 78214fb39b Update click boundaries in story viewer. 2022-03-17 12:12:55 -04:00
Alex Hart ff8d7fa6c2 Add send/recv/render support for text stories. 2022-03-17 12:12:55 -04:00
Greyson Parrelli 3a2e8b9b19 Add internal button to force an emoji search index download. 2022-03-17 12:12:55 -04:00
Cody Henthorne bca4289c96 Updated language translations. 2022-03-17 12:12:55 -04:00
Cody Henthorne 3fbd9baf0c Disable telecom integration. 2022-03-17 12:11:51 -04:00
Cody Henthorne e12c96f4b2 Bump version to 5.33.6 2022-03-17 12:09:16 -04:00
Cody Henthorne eec26aa481 Updated language translations. 2022-03-17 12:07:17 -04:00
Cody Henthorne 865aeda6f2 Disable telecom integration. 2022-03-17 12:04:33 -04:00
Cody Henthorne 2c4ebedda4 Bump version to 5.33.5 2022-03-16 13:57:08 -04:00
Cody Henthorne 042bc8d79a Updated language translations. 2022-03-16 13:56:52 -04:00
Cody Henthorne 4c7bd80f72 Fix early ringing state on slow connections. 2022-03-16 13:37:28 -04:00
Cody Henthorne 3a8591fdfb Bump version to 5.33.4 2022-03-16 10:22:25 -04:00
Cody Henthorne 629aaa2093 Updated language translations. 2022-03-16 10:18:41 -04:00
Cody Henthorne 5b5b118b7a Fix disconnect sound on call termination. 2022-03-16 10:08:06 -04:00
Greyson Parrelli c7016aa462 Fallback to legacy network detection. 2022-03-15 11:28:31 -04:00
Cody Henthorne cf857e109a Fix mention rendering bug. 2022-03-15 10:32:59 -04:00
Alex Hart 1c79840684 Bump version to 5.33.3 2022-03-11 16:10:06 -04:00
Alex Hart 4ba7de9519 Updated language translations. 2022-03-11 16:09:14 -04:00
Cody Henthorne 2eb8df347e Fix mention rendering regression. 2022-03-11 14:18:42 -05:00
Rashad Sookram 9056371c41 Fix quote preview being cut off.
When determining the height to force for the animation, the text was
being measured assuming it had infinite width, which made
it seem like it could fit on one line.
2022-03-11 11:38:56 -05:00
Greyson Parrelli 1f57e1f366 Add more logging around network changes. 2022-03-11 10:35:40 -05:00
Alex Hart aeb568bcf4 Fix crash when recreating conversation react with any emoji fragment. 2022-03-11 09:44:47 -04:00
Cody Henthorne b7afe4411e Fix NPE in telecom integration. 2022-03-10 16:17:13 -05:00
Alex Hart cba784b8ec Bump version to 5.33.2 2022-03-10 16:53:01 -04:00
Alex Hart 3aba15e88d Updated language translations. 2022-03-10 16:52:01 -04:00
Cody Henthorne fa384e93dc Fix crash importing backups. 2022-03-10 15:46:45 -05:00
Alex Hart 1f3e04da29 Fix text size in generated text drawables. 2022-03-10 12:32:42 -04:00
Greyson Parrelli a484d48377 Update network connectivity observer to be more optimistic. 2022-03-10 11:21:21 -05:00
Greyson Parrelli 15f51ea26e Fix crash if synced pinned contact is malformed. 2022-03-10 11:14:55 -05:00
Greyson Parrelli 80bfa103ab Fix narrow race around generation of some ACI keys. 2022-03-10 11:11:14 -05:00
Cody Henthorne 66f93e0d32 Do not drop 1:1 messages with mentions due to iOS and desktop regression.
iOS and Desktop both regressed in multi-forwarding by including mentions
in 1:1 forwards instead of replacing them with plain text. Android by
default drops these as invalid messages. Since there are clients in the
wild that do this now, we have to stop dropping them and try to resolve
them per normal mechanisms.
2022-03-09 12:09:46 -05:00
Rashad Sookram 366780f6cb Revert "Avoid querying conversation size twice."
This reverts commit fe088c39c7.
2022-03-09 11:55:52 -05:00
Alex Hart fb4c1fc268 Bump version to 5.33.1 2022-03-08 16:11:46 -04:00
Alex Hart e3bb7ccbd3 Updated language translations. 2022-03-08 16:11:09 -04:00
Alex Hart e3fb8a2137 Only update tab if it has actually changed. 2022-03-08 12:01:21 -04:00
Alex Hart ba4c0386ef Bump version to 5.33.0 2022-03-08 10:41:37 -04:00
Alex Hart 644945825b Updated language translations. 2022-03-08 10:41:37 -04:00
Alex Hart 7590c6dcbb Add warnings to FFs. 2022-03-08 10:41:37 -04:00
Cody Henthorne b25cef86ee Fix different dates being used when saving attachments. 2022-03-08 10:41:37 -04:00
Jim Gustafson fdaaa560e7 Update to RingRTC v2.19.2 2022-03-08 10:41:37 -04:00
Alex Hart 4cd438b2db Fix avatar view clickability. 2022-03-08 10:41:37 -04:00
Greyson Parrelli c0e1507ef4 Don't cancel KeyCachingService if not necessary.
This relates to #12043. There's some xiaomi-specific issue, and this
code was causing a pending intent creation on every app startup,
preventing it from opening. This call shouldn't be necessary unless
screenlock is active.
2022-03-08 10:41:37 -04:00
Alex Hart 8a75d78ce7 Restrict text story post sends to stories only. 2022-03-08 10:41:37 -04:00
Jordy 8176d25b4c Changed copyright to 2022.
Closes #11897
2022-03-08 10:41:37 -04:00
Greyson Parrelli 21273bc165 Fix syncing 'prefer system photos' setting. 2022-03-08 10:41:37 -04:00
Greyson Parrelli 213517f875 Reduce sensitivity of swipe-to-archive. 2022-03-08 10:41:37 -04:00
Greyson Parrelli b1c006657a Fix read receipt timestamp log. 2022-03-08 10:41:37 -04:00
Greyson Parrelli 852dcd9711 Show megaphone to improve network reliability. 2022-03-08 10:41:37 -04:00
Greyson Parrelli 427e73f7fd Improve payment withdrawals. 2022-03-08 10:41:37 -04:00
Alex Hart eae6a971e6 Update volume output stream when audioAttributes change. 2022-03-08 10:41:37 -04:00
Alex Hart 4b23e60dd6 Fix gallery media toast when selected item is too large.
Fixes #12011
2022-03-08 10:41:37 -04:00
Alex Hart f0988f37f3 Update UI for View More in contact lists. 2022-03-08 10:41:37 -04:00
Alex Hart e2e3617be9 Ensure groups stories are sent to are retained in the UI. 2022-03-08 10:41:37 -04:00
Greyson Parrelli 3ac63cc59d Implement new feature flag strategy for AEC selection. 2022-03-08 10:41:37 -04:00
Jim Gustafson d935d1deca Update to RingRTC v2.19.1 2022-03-08 10:41:37 -04:00
Alex Hart 3b1b00027b Fix conversation list tab bar icon colors. 2022-03-08 10:41:37 -04:00
Alex Hart a1bc1aaa98 Only show stories and send stories with respect to capability. 2022-03-08 10:41:37 -04:00
Rashad Sookram 0ccaad1462 Update quote UI for story replies in chat. 2022-03-08 10:41:37 -04:00
Chris Eager ad57e62680 Add staging registration constant to build config. 2022-03-08 10:41:37 -04:00
Alex Hart 4e57432dbb Improve smoothness of segmented progress bar and respect video duration. 2022-03-08 10:41:37 -04:00
Greyson Parrelli 63412b0153 Remove leftover Valentine's Day assets. 2022-03-08 10:41:37 -04:00
Cody Henthorne 35199abf1f Fix rejoining group on linked device not showing as joined. 2022-03-08 10:41:37 -04:00
Rashad Sookram 41b5813984 Open story viewer from MyStoriesFragment. 2022-03-08 10:41:37 -04:00
Greyson Parrelli 83215bb98f Additional work on not sending to blocked recipients. 2022-03-08 10:41:37 -04:00
clauz9 eb12395b8e Do not send to blocked recipients. 2022-03-08 10:41:37 -04:00
Cody Henthorne 4b07da4978 Inline change number flag. 2022-03-08 10:41:37 -04:00
Cody Henthorne 3fbc5423e5 Use jumbo emoji in reaction pickers. 2022-03-08 10:41:37 -04:00
Greyson Parrelli 9d9e6e2972 Ensure inner html is escaped when bolding.
Fixes #12033
2022-03-08 10:41:37 -04:00
Greyson Parrelli 56a8451d07 Add fallback static DNS resolver. 2022-03-08 10:41:37 -04:00
Alex Hart 2483a92975 Implement story error slates.
Co-authored-by: Rashad Sookram <rashad@signal.org>
2022-03-08 10:41:37 -04:00
Alex Hart 34bbb98c96 Do not allow forwarding of unsupported content to stories. 2022-03-08 10:41:37 -04:00
Alex Hart 155bdf6164 Fix storyType selection issue in forwarder. 2022-03-08 10:41:37 -04:00
Rashad Sookram 5358ed6eff Open camera after granting permission. 2022-03-08 10:41:37 -04:00
Greyson Parrelli 4f3bb39e5c Double-pulse message highlights. 2022-03-08 10:41:37 -04:00
clauz9 8a49534e2b Ensure bubble is highlighted after jumping.
Fixes #12017
2022-03-08 10:41:37 -04:00
Greyson Parrelli 2c3228d6df Fix issue where send button is invisible in voice note draft.
Fixes #12029
2022-03-08 10:41:37 -04:00
pauliancu97 c82d518d4d Make date view in voice note footer slightly wider.
Fixes #11728
2022-03-08 10:41:37 -04:00
Alex Hart 35cd36e9fe Implement support for 'allows replies' toggle. 2022-03-08 10:41:37 -04:00
Alex Hart ee176cbe3d Never send a link preview via MMS. 2022-03-08 10:41:37 -04:00
Rashad Sookram bd915cdd7f Fix crash from using a closed Cursor.
The call to setActive was causing the cursor held by the ViewModel to be
used, which hadn't been updated yet.
2022-03-08 10:41:37 -04:00
Rashad Sookram c27f5787fe Fix reaction overlay shade with gesture nav. 2022-03-08 10:41:37 -04:00
Alex Hart f6cdf459bb Update Views repo to pull view receipts instead of read receipts. 2022-03-08 10:41:37 -04:00
Alex Hart 4e851f90df Hide empty text until after we've tried to load stories. 2022-03-08 10:41:37 -04:00
Cody Henthorne 8d8a2a8eef Fix navigation crash in welcome fragment. 2022-03-08 10:41:37 -04:00
Cody Henthorne 277c17de83 Fix reactions vibrating in release notes channel. 2022-03-08 10:41:37 -04:00
Alex Hart d5fd424b95 Fix several over-the-wire story issues.
Co-authored-by: Rashad Sookram <rashad@signal.org>
2022-03-08 10:41:37 -04:00
Cody Henthorne e701e4bff0 Don't allow rate limit responses to end all group sends. 2022-03-08 10:41:37 -04:00
Alex Hart 0ddfb4456b Implement better stability while scrolling between pages. 2022-03-08 10:41:37 -04:00
Cody Henthorne 69dc31681d Apply server returned group patch instead of local only. 2022-03-08 10:41:37 -04:00
Alex Hart 2d7655a6bb Implement story ring support. 2022-03-08 10:41:37 -04:00
Greyson Parrelli fe088c39c7 Avoid querying conversation size twice. 2022-03-08 10:41:37 -04:00
Greyson Parrelli 731714d263 Remove unnecessary entry from spinner manifest. 2022-03-08 10:41:37 -04:00
Greyson Parrelli c165636180 Make perf builds profileable. 2022-03-08 10:41:37 -04:00
Jon Chambers 372dd13eba Accept both HTTP/413 and HTTP/429 as rate-limit responses. 2022-03-08 10:41:37 -04:00
Alex Hart b35ef0bb4d Send viewed receipts for stories. 2022-03-08 10:41:37 -04:00
Alex Hart bd58c91d2c Refactor viewer to prepare for enhanced video duration support. 2022-03-08 10:41:36 -04:00
Cody Henthorne 9a5fcdbe4d Fix emoji search showing in recent emoji bug. 2022-03-08 10:41:36 -04:00
Alex Hart 2452056cbe Fix issue where Story preview was not clickable. 2022-03-08 10:41:36 -04:00
Alex Hart bdf7e5d367 Prevent displaying my stories page when none are present in viewer. 2022-03-08 10:41:36 -04:00
Alex Hart aae683af41 Fix ConnectivityManager leak in MediaSelectionActivity. 2022-03-08 10:41:36 -04:00
Alex Hart 174cd860a0 Implement Stories feature behind flag.
Co-Authored-By: Greyson Parrelli <37311915+greyson-signal@users.noreply.github.com>
Co-Authored-By: Rashad Sookram <95182499+rashad-signal@users.noreply.github.com>
2022-03-08 10:41:36 -04:00
Alex Hart 765185952e Do not hook up check changed listener until after view state is restored. 2022-03-08 10:41:36 -04:00
Greyson Parrelli f4002850bb Add a ColumnTransformer system to Spinner. 2022-03-08 10:41:36 -04:00
Greyson Parrelli 935dd7de45 Remove E164s most places and prefer ServiceId more places.\ 2022-03-08 10:41:36 -04:00
Cody Henthorne d6b6884c69 Integrate calling with Android Telecom system. 2022-03-08 10:41:36 -04:00
Alex Hart 2ed39e4448 Add subscription cancellation step during account deletion. 2022-03-01 10:47:24 -05:00
Cody Henthorne 2de5ea43fb Add message type description to spinner as meta_type. 2022-03-01 10:47:23 -05:00
gram-signal 88d2d4d9c7 Switch from binary to streaming protos when using CDSHv1.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2022-03-01 10:47:23 -05:00
Cody Henthorne aff0c43b39 Prevent internal shake to report dialog from showing after locking. 2022-03-01 10:47:23 -05:00
Cody Henthorne bd18b731c8 Add metrics logging around get resumable upload url request. 2022-03-01 10:47:23 -05:00
Alex Hart 7b499f96be Implement donation receipts. 2022-03-01 10:47:23 -05:00
Alex Hart 63dab3f4b0 Add support for specific toasts when backup restoration cannot proceed.
Fixes #10918
2022-03-01 10:47:23 -05:00
Greyson Parrelli 80598814bd Remove unused PushServiceSocket method. 2022-03-01 10:47:23 -05:00
clauz9 b00abf1667 Fix internal-only crash when submitting a debuglog during registration. 2022-03-01 10:47:23 -05:00
Greyson Parrelli 9594be8fcf Add a 'Recent' tab to Spinner. 2022-03-01 10:47:23 -05:00
Greyson Parrelli acecd5f013 Update Spinner font styles. 2022-03-01 10:47:23 -05:00
Greyson Parrelli 2d1efb604c Add paging to Spinner browser. 2022-03-01 10:47:23 -05:00
Greyson Parrelli a84c971cbe Bump version to 5.32.15 2022-03-01 09:17:58 -05:00
Greyson Parrelli 7564ef4811 Updated language translations. 2022-03-01 09:17:23 -05:00
Greyson Parrelli 01e75120a7 Improve network reliability. 2022-03-01 09:17:22 -05:00
Cody Henthorne 1314b04994 Bump version to 5.32.14 2022-02-24 13:06:36 -05:00
Cody Henthorne 253cc5fec4 Updated language translations. 2022-02-24 12:55:47 -05:00
Greyson Parrelli c296a28a4a Update client-side max envelope size to 256KB to match server. 2022-02-24 12:52:06 -05:00
Greyson Parrelli ff95319559 Bump version to 5.32.13 2022-02-23 12:19:58 -05:00
Greyson Parrelli 3aa770ee08 Updated language translations. 2022-02-23 12:19:35 -05:00
Greyson Parrelli 653410cf27 Only generate a PNI key if necessary. 2022-02-23 12:15:18 -05:00
Greyson Parrelli ba08dbef5f Fix crash on conversation settings screen for longtime-unregistered users. 2022-02-23 12:05:44 -05:00
Cody Henthorne c1df628079 Bump version to 5.32.12 2022-02-22 12:25:02 -05:00
Cody Henthorne e72cac7db5 Updated language translations. 2022-02-22 12:14:50 -05:00
Greyson Parrelli cbfa573d3d Improve logging around profile uploads. 2022-02-22 11:37:29 -05:00
Greyson Parrelli 1b404cef34 Fix crash if you've been unregistered for couple months. 2022-02-22 11:36:23 -05:00
Greyson Parrelli cb66996407 Bump version to 5.32.11 2022-02-21 09:33:36 -05:00
Greyson Parrelli 96f908b068 Updated language translations. 2022-02-21 09:33:15 -05:00
Greyson Parrelli 472c8a441f Allow late initialization of some PNI keys. 2022-02-21 09:14:12 -05:00
Greyson Parrelli 1f0c56546e Improve robustness of PNI migration job. 2022-02-21 09:14:12 -05:00
Greyson Parrelli 97f8b5988d Refactor LiveRecipient fetch to be more clear. 2022-02-21 09:14:12 -05:00
Greyson Parrelli 19dc90b68b Allow group leave operations on blocked groups.
We should be leaving groups *before* they're blocked, but this helps
some other cases.
2022-02-20 22:56:23 -05:00
2495 changed files with 119845 additions and 30721 deletions
+23
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
+5 -4
View File
@@ -4,7 +4,7 @@ on:
pull_request: pull_request:
push: push:
branches: branches:
- 'master' - 'main'
- '4.**' - '4.**'
- '5.**' - '5.**'
@@ -14,11 +14,12 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: set up JDK 11 - name: set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v3
with: with:
distribution: temurin
java-version: 11 java-version: 11
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
@@ -32,7 +33,7 @@ jobs:
- name: Archive reports for failed build - name: Archive reports for failed build
if: ${{ failure() }} if: ${{ failure() }}
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: reports name: reports
path: '*/build/reports' path: '*/build/reports'
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- name: Build image - name: Build image
run: cd reproducible-builds && docker build -t signal-android . && cd .. run: cd reproducible-builds && docker build -t signal-android . && cd ..
- name: Test build - 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
+4 -4
View File
@@ -1,18 +1,18 @@
#if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME} #if (${PACKAGE_NAME} && ${PACKAGE_NAME} != "")package ${PACKAGE_NAME}
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.util.livedata.Store import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.disposables.CompositeDisposable
import org.thoughtcrime.securesms.util.rx.RxStore
#end #end
#parse("File Header.java") #parse("File Header.java")
class ${NAME}ViewModel : ViewModel() { class ${NAME}ViewModel : ViewModel() {
private val store = Store(${NAME}State()) private val store = RxStore(${NAME}State())
private val disposables = CompositeDisposable() private val disposables = CompositeDisposable()
val state: LiveData<${NAME}State> = store.stateLiveData val state: Flowable<${NAME}State> = store.stateFlowable
override fun onCleared() { override fun onCleared() {
disposables.clear() disposables.clear()
+2 -2
View File
@@ -28,7 +28,7 @@ https://www.transifex.com/projects/p/signal-android/
## Contributing Code ## 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. 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.
@@ -59,7 +59,7 @@ The form and manner of this distribution makes it eligible for export under the
## License ## License
Copyright 2013-2021 Signal Copyright 2013-2022 Signal
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
+1 -1
View File
@@ -96,7 +96,7 @@ try:
print("\nTo include this in the distribution, copy it to the project's assets/databases/ directory.") 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.") 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: if connection:
connection.rollback() connection.rollback()
print("Error: %s" % e.args[0]) print("Error: %s" % e.args[0])
+27 -30
View File
@@ -7,14 +7,9 @@ apply from: 'translations.gradle'
apply plugin: 'org.jetbrains.kotlin.android' apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'app.cash.exhaustive' apply plugin: 'app.cash.exhaustive'
apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-parcelize'
apply from: 'static-ips.gradle'
repositories { repositories {
maven {
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
content {
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
}
}
maven { maven {
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/" url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
content { content {
@@ -62,8 +57,8 @@ ktlint {
version = "0.43.2" version = "0.43.2"
} }
def canonicalVersionCode = 1013 def canonicalVersionCode = 1079
def canonicalVersionName = "5.32.10" def canonicalVersionName = "5.42.1"
def postFixSize = 100 def postFixSize = 100
def abiPostFix = ['universal' : 0, def abiPostFix = ['universal' : 0,
@@ -156,6 +151,8 @@ android {
exclude 'META-INF/LICENSE' exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE' exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro' exclude 'META-INF/proguard/androidx-annotations.pro'
exclude 'libsignal_jni.dylib'
exclude 'signal_jni.dll'
} }
@@ -178,7 +175,7 @@ android {
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"" buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.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_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_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"" buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
@@ -186,25 +183,28 @@ android {
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\"}" buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\"}"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443" buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String[]", "SIGNAL_SERVICE_IPS", service_ips
buildConfigField "String[]", "SIGNAL_STORAGE_IPS", storage_ips
buildConfigField "String[]", "SIGNAL_CDN_IPS", cdn_ips
buildConfigField "String[]", "SIGNAL_CDN2_IPS", cdn2_ips
buildConfigField "String[]", "SIGNAL_CDS_IPS", cds_ips
buildConfigField "String[]", "SIGNAL_KBS_IPS", kbs_ips
buildConfigField "String[]", "SIGNAL_SFU_IPS", sfu_ips
buildConfigField "String[]", "SIGNAL_CONTENT_PROXY_IPS", content_proxy_ips
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
buildConfigField "String", "CDSH_CODE_HASH", "\"ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\"" buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "String", "CDSI_MRENCLAVE", "\"42e36b74794abe612d698308b148ff8a7dc5fdc6ad28d99bc5024ed6ece18dfe\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " + buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"0cedba03535b41b67729ce9924185f831d7767928a1d1689acb689bc079c375f\", " +
"\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " + "\"187d2739d22be65e74b65f0055e74d31310e4267e5fac2b1246cc8beba81af39\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")" "\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] {" + buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
"new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" +
"}"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXQ==\"" buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXQ==\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\"" buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
buildConfigField "int[]", "MOBILE_COIN_BLACKLIST", "new int[]{98,963,53,850,7}"
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\"" buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\""
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"" buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"" buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
@@ -335,14 +335,11 @@ android {
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " + buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"dd6f66d397d9e8cf6ec6db238e59a7be078dd50e9715427b9c89b409ffe53f99\", " +
"\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " + "\"4200003414528c151e2dccafbc87aa6d3d66a5eb8f8c05979a6e97cb33cd493a\", " +
"\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")" "\"ee19f1965b1eefa3dc4204eb70c04f397755f771b8c1909d080c04dad2a6a9ba\")"
buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[] {" + buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
"new org.thoughtcrime.securesms.KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
"\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" +
"}"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXQ==\"" buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXQ==\""
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"" buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
buildConfigField "String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\""
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"" buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"" buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
@@ -445,8 +442,10 @@ dependencies {
implementation project(':device-transfer') implementation project(':device-transfer')
implementation project(':image-editor') implementation project(':image-editor')
implementation project(':donations') 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.google.protobuf.javalite
implementation(libs.mobilecoin) { implementation(libs.mobilecoin) {
@@ -471,7 +470,6 @@ dependencies {
implementation libs.materialish.progress implementation libs.materialish.progress
implementation libs.greenrobot.eventbus implementation libs.greenrobot.eventbus
implementation libs.waitingdots implementation libs.waitingdots
implementation libs.floatingactionbutton
implementation libs.google.zxing.android.integration implementation libs.google.zxing.android.integration
implementation libs.time.duration.picker implementation libs.time.duration.picker
implementation libs.google.zxing.core implementation libs.google.zxing.core
@@ -497,7 +495,6 @@ dependencies {
implementation libs.lottie implementation libs.lottie
implementation libs.stickyheadergrid implementation libs.stickyheadergrid
implementation libs.circular.progress.button
implementation libs.signal.android.database.sqlcipher implementation libs.signal.android.database.sqlcipher
implementation libs.androidx.sqlite implementation libs.androidx.sqlite
@@ -514,10 +511,7 @@ dependencies {
testImplementation testLibs.junit.junit testImplementation testLibs.junit.junit
testImplementation testLibs.assertj.core testImplementation testLibs.assertj.core
testImplementation testLibs.mockito.core testImplementation testLibs.mockito.core
testImplementation testLibs.powermock.api.mockito testImplementation testLibs.mockito.kotlin
testImplementation testLibs.powermock.module.junit4.core
testImplementation testLibs.powermock.module.junit4.rule
testImplementation testLibs.powermock.classloading.xstream
testImplementation testLibs.androidx.test.core testImplementation testLibs.androidx.test.core
testImplementation (testLibs.robolectric.robolectric) { testImplementation (testLibs.robolectric.robolectric) {
@@ -533,6 +527,9 @@ dependencies {
androidTestImplementation testLibs.androidx.test.ext.junit androidTestImplementation testLibs.androidx.test.ext.junit
androidTestImplementation testLibs.espresso.core 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 testImplementation testLibs.espresso.core
@@ -544,7 +541,7 @@ dependencies {
implementation libs.rxjava3.rxkotlin implementation libs.rxjava3.rxkotlin
implementation libs.rxdogtag implementation libs.rxdogtag
androidTestUtil 'androidx.test:orchestrator:1.4.0' androidTestUtil 'androidx.test:orchestrator:1.4.1'
} }
def getLastCommitTimestamp() { def getLastCommitTimestamp() {
+1
View File
@@ -2,6 +2,7 @@
-dontobfuscate -dontobfuscate
-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable
-keep class org.whispersystems.** { *; } -keep class org.whispersystems.** { *; }
-keep class org.signal.libsignal.protocol.** { *; }
-keep class org.thoughtcrime.securesms.** { *; } -keep class org.thoughtcrime.securesms.** { *; }
-keepclassmembers class ** { -keepclassmembers class ** {
public void onEvent*(**); public void onEvent*(**);
@@ -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)
}
}
@@ -0,0 +1,98 @@
package org.thoughtcrime.securesms.database
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import java.util.UUID
class DistributionListDatabaseTest {
private lateinit var distributionDatabase: DistributionListDatabase
@Before
fun setup() {
distributionDatabase = SignalDatabase.distributionLists
}
@Test
fun createList_whenNoConflict_insertSuccessfully() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
}
@Test
fun createList_whenNameConflict_failToInsert() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val id2: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNull(id2)
}
@Test
fun getList_returnCorrectList() {
createRecipients(3)
val members: List<RecipientId> = recipientList(1, 2, 3)
val id: DistributionListId? = distributionDatabase.createList("test", members)
Assert.assertNotNull(id)
val record: DistributionListRecord? = distributionDatabase.getList(id!!)
Assert.assertNotNull(record)
Assert.assertEquals(id, record!!.id)
Assert.assertEquals("test", record.name)
Assert.assertEquals(members, record.members)
}
@Test
fun getMembers_returnsCorrectMembers() {
createRecipients(3)
val members: List<RecipientId> = recipientList(1, 2, 3)
val id: DistributionListId? = distributionDatabase.createList("test", members)
Assert.assertNotNull(id)
val foundMembers: List<RecipientId> = distributionDatabase.getMembers(id!!)
Assert.assertEquals(members, foundMembers)
}
@Test
fun givenStoryExists_getStoryType_returnsStoryWithReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val storyType = distributionDatabase.getStoryType(id!!)
Assert.assertEquals(StoryType.STORY_WITH_REPLIES, storyType)
}
@Test
fun givenStoryExistsAndMarkedNoReplies_getStoryType_returnsStoryWithoutReplies() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
distributionDatabase.setAllowsReplies(id!!, false)
val storyType = distributionDatabase.getStoryType(id)
Assert.assertEquals(StoryType.STORY_WITHOUT_REPLIES, storyType)
}
@Test(expected = IllegalStateException::class)
fun givenStoryDoesNotExist_getStoryType_throwsIllegalStateException() {
distributionDatabase.getStoryType(DistributionListId.from(12))
Assert.fail("Expected an assertion error.")
}
private fun createRecipients(count: Int) {
for (i in 0 until count) {
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
}
}
private fun recipientList(vararg ids: Long): List<RecipientId> {
return ids.map { RecipientId.from(it) }
}
}
@@ -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())
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -1,6 +1,9 @@
package org.thoughtcrime.securesms.database package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4 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.assertEquals
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNotEquals
@@ -8,26 +11,55 @@ import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
import org.signal.core.util.ThreadUtil 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.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
import org.thoughtcrime.securesms.keyvalue.AccountValues import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
import org.thoughtcrime.securesms.keyvalue.KeyValueStore import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
import org.thoughtcrime.securesms.keyvalue.SignalStore 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.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.libsignal.util.guava.Optional import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.whispersystems.signalservice.api.push.ACI import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI import org.whispersystems.signalservice.api.push.PNI
import java.lang.IllegalArgumentException import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional
import java.util.UUID import java.util.UUID
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest { class RecipientDatabaseTest_getAndPossiblyMerge {
private lateinit var recipientDatabase: RecipientDatabase 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 localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID()) private val localPni = PNI.from(UUID.randomUUID())
@@ -35,6 +67,19 @@ class RecipientDatabaseTest {
@Before @Before
fun setup() { fun setup() {
recipientDatabase = SignalDatabase.recipients 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() ensureDbEmpty()
SignalStore.account().setAci(localAci) SignalStore.account().setAci(localAci)
@@ -479,6 +524,140 @@ class RecipientDatabaseTest {
assert(changeNumberListener.numberChangeWasEnqueued) 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 // Misc
// ============================================================== // ==============================================================
@@ -523,12 +702,59 @@ class RecipientDatabaseTest {
} }
private fun ensureDbEmpty() { 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()) assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0)) 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 { private class ChangeNumberListener {
var numberChangeWasEnqueued = false var numberChangeWasEnqueued = false
@@ -1,270 +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.Assert.assertTrue
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.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.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.util.UuidUtil
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 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
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
ensureDbEmpty()
}
/** 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)
// 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))
}
private val context: Application
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", 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.absent()): 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.absent()): IncomingMediaMessage {
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.absent())
}
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"
}
}
@@ -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"
}
}
@@ -0,0 +1,810 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.lang.AssertionError
import java.lang.IllegalArgumentException
import java.lang.IllegalStateException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_processPnpTupleToChangeSet {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
private lateinit var db: RecipientDatabase
@Before
fun setup() {
db = SignalDatabase.recipients
}
@Test
fun noMatch_e164Only() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, null, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, null, null)
),
changeSet
)
}
@Test
fun noMatch_e164AndPni() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, null)
),
changeSet
)
}
@Test
fun noMatch_aciOnly() {
val changeSet = db.processPnpTupleToChangeSet(null, null, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(null, null, ACI_A)
),
changeSet
)
}
@Test(expected = IllegalArgumentException::class)
fun noMatch_pniOnly() {
db.processPnpTupleToChangeSet(null, PNI_A, null, pniVerified = false)
}
@Test(expected = IllegalArgumentException::class)
fun noMatch_noData() {
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
}
@Test
fun noMatch_allFields() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, ACI_A)
),
changeSet
)
}
@Test
fun fullMatch() {
val result = applyAndAssert(
Input(E164_A, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id)
),
result.changeSet
)
}
@Test
fun onlyE164Matches() {
val result = applyAndAssert(
Input(E164_A, null, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null, pniSession = true),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun e164AndAciMatches() {
val result = applyAndAssert(
Input(E164_A, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(null, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(null, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
),
PnpOperation.SessionSwitchoverInsert(result.id)
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches() {
val result = applyAndAssert(
Input(null, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches() {
val result = applyAndAssert(
Input(null, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
Input(null, null, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.thirdId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.Merge(
primaryId = result.thirdId,
secondaryId = result.firstId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.SetAci(
recipientId = result.firstId,
aci = ACI_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniAndE164_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(E164_B, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(
recipientId = result.firstId,
pni = PNI_A
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(null, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(E164_B, PNI_A, null),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_sessionsExist() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null, pniSession = true),
Input(E164_B, PNI_A, null, pniSession = true),
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.secondId),
PnpOperation.SessionSwitchoverInsert(result.firstId)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(null, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
val result = applyAndAssert(
listOf(
Input(E164_B, PNI_A, null),
Input(null, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.Update(
recipientId = result.secondId,
e164 = E164_A,
pni = PNI_A,
aci = ACI_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, PNI_B, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164Aci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, null, ACI_A),
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
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 insertMockSessionFor(account: ServiceId, address: ServiceId) {
SignalDatabase.rawDatabase.insert(
SessionDatabase.TABLE_NAME, null,
contentValuesOf(
SessionDatabase.ACCOUNT_ID to account.toString(),
SessionDatabase.ADDRESS to address.toString(),
SessionDatabase.DEVICE to 1,
SessionDatabase.RECORD to Util.getSecretBytes(32)
)
)
}
data class Input(val e164: String?, val pni: PNI?, val aci: ACI?, val pniSession: Boolean = false, val aciSession: Boolean = false)
data class Update(val e164: String?, val pni: PNI?, val aci: ACI?, val pniVerified: Boolean = false)
data class Output(val e164: String?, val pni: PNI?, val aci: ACI?)
data class PnpMatchResult(val ids: List<RecipientId>, val changeSet: PnpChangeSet) {
val id
get() = if (ids.size == 1) {
ids[0]
} else {
throw IllegalStateException("There are multiple IDs, but you assumed 1!")
}
val firstId
get() = ids[0]
val secondId
get() = ids[1]
val thirdId
get() = ids[2]
}
private fun applyAndAssert(input: Input, update: Update, output: Output): PnpMatchResult {
return applyAndAssert(listOf(input), update, output)
}
/**
* Helper method that will call insert your recipients, call [RecipientDatabase.processPnpTupleToChangeSet] with your params,
* and then verify your output matches what you expect.
*
* It results the inserted ID's and changeset for additional verification.
*
* But basically this is here to make the tests more readable. It gives you a clear list of:
* - input
* - update
* - output
*
* that you can spot check easily.
*
* Important: The output will only include records that contain fields from the input. That means
* for:
*
* Input: E164_B, PNI_A, null
* Update: E164_A, PNI_A, null
*
* You will get:
* Output: E164_A, PNI_A, null
*
* Even though there was an update that will also result in the row (E164_B, null, null)
*/
private fun applyAndAssert(input: List<Input>, update: Update, output: Output): PnpMatchResult {
val ids = input.map { insert(it.e164, it.pni, it.aci) }
input
.filter { it.pniSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.pni!!) }
input
.filter { it.aciSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.aci!!) }
val byE164 = update.e164?.let { db.getByE164(it).orElse(null) }
val byPniSid = update.pni?.let { db.getByServiceId(it).orElse(null) }
val byAciSid = update.aci?.let { db.getByServiceId(it).orElse(null) }
val data = PnpDataSet(
e164 = update.e164,
pni = update.pni,
aci = update.aci,
byE164 = byE164,
byPniSid = byPniSid,
byPniOnly = update.pni?.let { db.getByPni(it).orElse(null) },
byAciSid = byAciSid,
e164Record = byE164?.let { db.getRecord(it) },
pniSidRecord = byPniSid?.let { db.getRecord(it) },
aciSidRecord = byAciSid?.let { db.getRecord(it) }
)
val changeSet = db.processPnpTupleToChangeSet(update.e164, update.pni, update.aci, pniVerified = update.pniVerified)
val finalData = data.perform(changeSet.operations)
val finalRecords = setOfNotNull(finalData.e164Record, finalData.pniSidRecord, finalData.aciSidRecord)
assertEquals("There's still multiple records in the resulting record set! $finalRecords", 1, finalRecords.size)
finalRecords.firstOrNull { record -> record.e164 == output.e164 && record.pni == output.pni && record.serviceId == (output.aci ?: output.pni) }
?: throw AssertionError("Expected output was not found in the result set! Expected: $output")
return PnpMatchResult(
ids = ids,
changeSet = changeSet
)
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}
@@ -122,4 +122,62 @@ class SQLiteDatabaseTest {
assertTrue(hasRun1.get()) assertTrue(hasRun1.get())
assertFalse(hasRun2.get()) assertFalse(hasRun2.get())
} }
@Test
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransaction() {
val hasRun = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction {
try {
db.beginTransaction()
hasRun.set(true)
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
assertFalse(hasRun.get())
db.setTransactionSuccessful()
db.endTransaction()
assertTrue(hasRun.get())
}
@Test
fun runPostSuccessfulTransaction_runsAndPerformsAnotherTransactionAndRunPostNested() {
val hasRun1 = AtomicBoolean(false)
val hasRun2 = AtomicBoolean(false)
db.beginTransaction()
db.runPostSuccessfulTransaction {
db.beginTransaction()
db.runPostSuccessfulTransaction {
assertTrue(hasRun1.get())
assertFalse(hasRun2.get())
hasRun2.set(true)
}
assertFalse(hasRun1.get())
hasRun1.set(true)
assertFalse(hasRun2.get())
db.setTransactionSuccessful()
db.endTransaction()
}
assertFalse(hasRun1.get())
assertFalse(hasRun2.get())
db.setTransactionSuccessful()
db.endTransaction()
assertTrue(hasRun1.get())
assertTrue(hasRun2.get())
}
} }
@@ -0,0 +1,292 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.notNullValue
import org.hamcrest.Matchers.nullValue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
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
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
import org.thoughtcrime.securesms.database.model.databaseprotos.groupChange
import org.thoughtcrime.securesms.database.model.databaseprotos.groupContext
import org.thoughtcrime.securesms.groups.GroupId
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.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.Optional
import java.util.UUID
@Suppress("ClassName", "TestFunctionName")
@RunWith(AndroidJUnit4::class)
class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
private lateinit var recipients: RecipientDatabase
private lateinit var sms: SmsDatabase
private val localAci = ACI.from(UUID.randomUUID())
private val localPni = PNI.from(UUID.randomUUID())
private var wallClock: Long = 1000
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
@Before
fun setUp() {
recipients = SignalDatabase.recipients
sms = SignalDatabase.sms
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
bob = recipients.getOrInsertFromServiceId(bobServiceId)
}
/**
* Do nothing if no previous messages.
*/
@Test
fun noPreviousMessage() {
val result = sms.collapseJoinRequestEventsIfPossible(
1,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
}
/**
* Do nothing if previous message is text.
*/
@Test
fun previousTextMesssage() {
val threadId = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get().threadId
val result = sms.collapseJoinRequestEventsIfPossible(
threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
}
/**
* Do nothing if previous is unrelated group change.
*/
@Test
fun previousUnrelatedGroupChange() {
val threadId = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addMember(bobServiceId)
}
}
)
).get().threadId
val result = sms.collapseJoinRequestEventsIfPossible(
threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
}
/**
* Do nothing if previous join request is from a different recipient.
*/
@Test
fun previousJoinRequestFromADifferentRecipient() {
val threadId = sms.insertMessageInbox(
groupUpdateMessage(
sender = bob,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = bobServiceId) {
deleteRequestingMember(bobServiceId)
}
}
)
).get().threadId
val result = sms.collapseJoinRequestEventsIfPossible(
threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is null when not collapsing", result.orElse(null), nullValue())
}
/**
* Collapse if previous is join request from same.
*/
@Test
fun previousJoinRequestCollapse() {
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addRequestingMember(aliceServiceId)
}
}
)
).get()
val result = sms.collapseJoinRequestEventsIfPossible(
latestMessage.threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
}
/**
* Collapse if previous is join request from same, and leave second previous alone if text.
*/
@Test
fun previousJoinThenTextCollapse() {
val secondLatestMessage = sms.insertMessageInbox(smsMessage(sender = alice, body = "What up")).get()
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addRequestingMember(aliceServiceId)
}
}
)
).get()
assert(secondLatestMessage.threadId == latestMessage.threadId)
val result = sms.collapseJoinRequestEventsIfPossible(
latestMessage.threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
assertThat("result message id should be same as latest message", result.get().messageId, `is`(latestMessage.messageId))
}
/**
* Collapse "twice" is previous is a join request and second previous is already collapsed join/delete from the same recipient.
*/
@Test
fun previousCollapseAndJoinRequestDoubleCollapse() {
val secondLatestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addRequestingMember(aliceServiceId)
deleteRequestingMember(aliceServiceId)
}
}
)
).get()
val latestMessage: MessageDatabase.InsertResult = sms.insertMessageInbox(
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
addRequestingMember(aliceServiceId)
}
}
)
).get()
assert(secondLatestMessage.threadId == latestMessage.threadId)
val result = sms.collapseJoinRequestEventsIfPossible(
latestMessage.threadId,
groupUpdateMessage(
sender = alice,
groupContext = groupContext(masterKey = masterKey) {
change = groupChange(editor = aliceServiceId) {
deleteRequestingMember(aliceServiceId)
}
}
)
)
assertThat("result is not null when collapsing", result.orElse(null), notNullValue())
assertThat("result message id should be same as second latest message", result.get().messageId, `is`(secondLatestMessage.messageId))
assertThat("latest message should be deleted", sms.getMessageRecordOrNull(latestMessage.messageId), nullValue())
}
private fun smsMessage(sender: RecipientId, body: String? = ""): IncomingTextMessage {
wallClock++
return IncomingTextMessage(sender, 1, wallClock, wallClock, wallClock, body, Optional.of(groupId), 0, true, null)
}
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingGroupUpdateMessage {
return IncomingGroupUpdateMessage(smsMessage(sender, null), groupContext)
}
companion object {
private val aliceServiceId: ServiceId = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
private val bobServiceId: ServiceId = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
private val masterKey = GroupMasterKey(Hex.fromStringCondensed("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
private val groupId = GroupId.v2(masterKey)
}
}
@@ -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()))
}
}
}
@@ -0,0 +1,74 @@
package org.thoughtcrime.securesms.database
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.whispersystems.signalservice.api.push.ServiceId
import java.util.UUID
@Suppress("ClassName")
class ThreadDatabaseTest_pinned {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule()
private lateinit var recipient: Recipient
@Before
fun setUp() {
recipient = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromServiceId(ServiceId.from(UUID.randomUUID())))
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIDoNotDeleteOrUnpinTheThread() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.mms.deleteMessage(messageId)
// THEN
val pinned = SignalDatabase.threads.pinnedThreadIds
assertTrue(threadId in pinned)
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectTheThreadInUnarchivedCount() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.mms.deleteMessage(messageId)
// THEN
val unarchivedCount = SignalDatabase.threads.unarchivedConversationListCount
assertEquals(1, unarchivedCount)
}
@Test
fun givenAPinnedThread_whenIDeleteTheLastMessage_thenIExpectPinnedThreadInUnarchivedList() {
// GIVEN
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val messageId = MmsHelper.insert(recipient = recipient, threadId = threadId)
SignalDatabase.threads.pinConversations(listOf(threadId))
// WHEN
SignalDatabase.mms.deleteMessage(messageId)
// THEN
SignalDatabase.threads.getUnarchivedConversationList(true, 0, 1).use {
it.moveToFirst()
assertEquals(threadId, CursorUtil.requireLong(it, ThreadDatabase.ID))
}
}
}
@@ -4,7 +4,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; 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.HashedPin;
import org.whispersystems.signalservice.api.kbs.KbsData; import org.whispersystems.signalservice.api.kbs.KbsData;
import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.kbs.MasterKey;
@@ -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)
}
}
@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.testing
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import java.util.UUID
/**
* Sets up bare-minimum to allow writing unit tests against the database,
* including setting up the local ACI and PNI pair.
*
* @param deleteAllThreadsOnEachRun Run deleteAllThreads between each unit test
*/
class SignalDatabaseRule(
private val deleteAllThreadsOnEachRun: Boolean = true
) : TestWatcher() {
val localAci: ACI = ACI.from(UUID.randomUUID())
val localPni: PNI = PNI.from(UUID.randomUUID())
override fun starting(description: Description?) {
deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
}
override fun finished(description: Description?) {
deleteAllThreads()
}
private fun deleteAllThreads() {
if (deleteAllThreadsOnEachRun) {
SignalDatabase.mms.deleteAllThreads()
}
}
}
+54 -7
View File
@@ -91,6 +91,8 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT"/>
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
<application android:name=".ApplicationContext" <application android:name=".ApplicationContext"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@@ -177,11 +179,12 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity android:name=".sharing.ShareActivity" <activity android:name=".sharing.v2.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:taskAffinity="" android:taskAffinity=""
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
@@ -221,7 +224,7 @@
<data android:scheme="sgnl" <data android:scheme="sgnl"
android:host="addstickers" /> android:host="addstickers" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
@@ -255,7 +258,7 @@
android:noHistory="true" android:noHistory="true"
android:theme="@style/Signal.Transparent"> android:theme="@style/Signal.Transparent">
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
@@ -272,7 +275,7 @@
android:host="signal.group"/> android:host="signal.group"/>
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
@@ -282,7 +285,7 @@
android:host="signal.tube" /> android:host="signal.tube" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
@@ -386,6 +389,32 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".stories.my.MyStoriesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name=".stories.settings.StorySettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
<activity
android:name=".stories.viewer.StoryViewerActivity"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
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" <activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
android:theme="@style/Signal.DayNight.NoActionBar" android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -396,6 +425,11 @@
android:windowSoftInputMode="stateAlwaysHidden"> android:windowSoftInputMode="stateAlwaysHidden">
</activity> </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" <activity android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -437,6 +471,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DeviceActivity" <activity android:name=".DeviceActivity"
android:screenOrientation="portrait"
android:label="@string/AndroidManifest__linked_devices" android:label="@string/AndroidManifest__linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -514,6 +549,7 @@
<activity android:name=".blocked.BlockedUsersActivity" <activity android:name=".blocked.BlockedUsersActivity"
android:theme="@style/TextSecure.LightTheme" android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".scribbles.ImageEditorStickerSelectActivity" <activity android:name=".scribbles.ImageEditorStickerSelectActivity"
@@ -624,10 +660,17 @@
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<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:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/> <service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
<service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/> <service android:enabled="true" android:name=".messages.IncomingMessageObserver$ForegroundService"/>
<service android:name=".service.webrtc.AndroidCallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<service android:name=".components.voice.VoiceNotePlaybackService"> <service android:name=".components.voice.VoiceNotePlaybackService">
<intent-filter> <intent-filter>
@@ -671,7 +714,9 @@
<service android:name=".service.GenericForegroundService"/> <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"> <service android:name=".gcm.FcmReceiveService">
<intent-filter> <intent-filter>
@@ -730,6 +775,8 @@
<receiver android:name=".service.ExpirationListener" /> <receiver android:name=".service.ExpirationListener" />
<receiver android:name=".service.ExpiringStoriesManager$ExpireStoriesAlarm" />
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" /> <receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" /> <receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
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
@@ -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)
}
}
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.account.AccountAttributes; import org.whispersystems.signalservice.api.account.AccountAttributes;
public final class AppCapabilities { public final class AppCapabilities {
@@ -19,6 +20,6 @@ public final class AppCapabilities {
* asking if the user has set a Signal PIN or not. * asking if the user has set a Signal PIN or not.
*/ */
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) { public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER); return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, CHANGE_NUMBER, FeatureFlags.stories(), FeatureFlags.giftBadges());
} }
} }
@@ -35,30 +35,32 @@ import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer; import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs; import org.signal.glide.SignalGlideCodecs;
import org.thoughtcrime.securesms.emoji.JumboEmoji; import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.thoughtcrime.securesms.jobs.RetrieveReleaseChannelJob;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.signal.ringrtc.CallManager; import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage; import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider; import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.LogDatabase; import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider; import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource; import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmJobService; import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob; import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob; import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob; import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob; import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob; import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob; import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
@@ -66,6 +68,7 @@ import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver; import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.SignalGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil; import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
@@ -78,6 +81,7 @@ import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener; import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener; import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper; import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver; import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.AppStartup;
@@ -89,9 +93,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker; import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import java.io.IOException;
import java.net.SocketException; import java.net.SocketException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.security.Security; import java.security.Security;
@@ -188,6 +190,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(EmojiSource::refresh) .addNonBlocking(EmojiSource::refresh)
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this)) .addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(this::ensureProfileUploaded) .addNonBlocking(this::ensureProfileUploaded)
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this)) .addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager) .addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this))) .addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
@@ -195,7 +198,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary) .addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge())) .addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this)) .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)
.execute(); .execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -211,7 +218,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
ApplicationDependencies.getFrameRateTracker().start(); ApplicationDependencies.getFrameRateTracker().start();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded(); ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
ApplicationDependencies.getDeadlockDetector().start(); ApplicationDependencies.getDeadlockDetector().start();
SubscriptionKeepAliveJob.launchSubscriberIdKeepAliveJobIfNecessary(); SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
SignalExecutors.BOUNDED.execute(() -> { SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary(); FeatureFlags.refreshIfNecessary();
@@ -14,6 +14,7 @@ import android.widget.ImageView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityOptionsCompat; import androidx.core.app.ActivityOptionsCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
@@ -58,6 +59,12 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, from, "avatar").toBundle(); return ActivityOptionsCompat.makeSceneTransitionAnimation(activity, from, "avatar").toBundle();
} }
@Override
protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
super.attachBaseContext(newBase);
}
@Override @Override
protected void onCreate(Bundle savedInstanceState, boolean ready) { protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready); super.onCreate(savedInstanceState, ready);
@@ -132,7 +139,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility()); 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)); fullscreenHelper.showAndHideWithSystemUI(getWindow(), findViewById(R.id.toolbar_layout));
} }
@@ -26,10 +26,10 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Optional;
import java.util.Set; import java.util.Set;
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable, Multiselectable { public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable, Multiselectable {
@@ -46,7 +46,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
boolean hasWallpaper, boolean hasWallpaper,
boolean isMessageRequestAccepted, boolean isMessageRequestAccepted,
boolean canPlayInline, boolean canPlayInline,
@NonNull Colorizer colorizer); @NonNull Colorizer colorizer,
boolean isCondensedMode);
@NonNull ConversationMessage getConversationMessage(); @NonNull ConversationMessage getConversationMessage();
@@ -60,9 +61,14 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
// Intentionally Blank. // Intentionally Blank.
} }
default void updateSelectedState() {
// Intentionally Blank.
}
interface EventListener { interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord); void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview); void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
void onQuotedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms); void onMoreTextClicked(@NonNull RecipientId conversationRecipientId, long messageId, boolean isMms);
void onStickerClicked(@NonNull StickerLocator stickerLocator); void onStickerClicked(@NonNull StickerLocator stickerLocator);
void onViewOnceMessageClicked(@NonNull MmsMessageRecord messageRecord); void onViewOnceMessageClicked(@NonNull MmsMessageRecord messageRecord);
@@ -94,8 +100,13 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onChangeNumberUpdateContact(@NonNull Recipient recipient); void onChangeNumberUpdateContact(@NonNull Recipient recipient);
void onCallToAction(@NonNull String action); void onCallToAction(@NonNull String action);
void onDonateClicked(); 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 */ /** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url); boolean onUrlClicked(@NonNull String url);
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
} }
} }
@@ -1,8 +1,8 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet; import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
@@ -12,7 +12,8 @@ import java.util.Set;
public interface BindableConversationListItem extends Unbindable { 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 GlideRequests glideRequests, @NonNull Locale locale,
@NonNull Set<Long> typingThreads, @NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations); @NonNull ConversationSet selectedConversations);
@@ -13,7 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.recipients.Recipient; 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. * 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); builder.setNegativeButton(android.R.string.cancel, null);
} else { } else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context))); 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) { if (onBlockAndReportSpam != null) {
builder.setNeutralButton(android.R.string.cancel, null); builder.setNeutralButton(android.R.string.cancel, null);
@@ -128,7 +129,8 @@ public final class BlockUnblockDialog {
builder.setNegativeButton(android.R.string.cancel, null); builder.setNegativeButton(android.R.string.cancel, null);
} else { } else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context))); 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.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
builder.setNegativeButton(android.R.string.cancel, null); builder.setNegativeButton(android.R.string.cancel, null);
} }
@@ -20,22 +20,23 @@ import android.content.Context;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView; import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode; 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.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@@ -123,12 +124,12 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
} }
@Override @Override
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) { public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
callback.accept(true); callback.accept(true);
} }
@Override @Override
public void onContactDeselected(Optional<RecipientId> recipientId, String number) {} public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {}
@Override @Override
public void onBeginScroll() { public void onBeginScroll() {
@@ -152,7 +153,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
@Override @Override
protected Void doInBackground(Context... params) { protected Void doInBackground(Context... params) {
try { try {
DirectoryHelper.refreshDirectory(params[0], true); ContactDiscovery.refreshAll(params[0], true);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
} }
@@ -18,11 +18,11 @@ package org.thoughtcrime.securesms;
import android.Manifest; import android.Manifest;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Rect;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
@@ -31,7 +31,6 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Button; import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@@ -42,6 +41,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.ConstraintSet;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
import androidx.loader.app.LoaderManager; import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader; import androidx.loader.content.Loader;
import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.DefaultItemAnimator;
@@ -53,58 +53,64 @@ import androidx.transition.TransitionManager;
import com.annimon.stream.Collectors; import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.pnikosis.materialishprogress.ProgressWheel; import com.pnikosis.materialishprogress.ProgressWheel;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper;
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader; import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactChip; import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.HeaderAction;
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration; import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
import org.thoughtcrime.securesms.contacts.SelectedContact; import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.contacts.SelectedContacts;
import org.thoughtcrime.securesms.contacts.selection.ContactSelectionArguments;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.groups.SelectionLimits; import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog; import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareContact;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter; import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader; import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer; import java.util.function.Consumer;
import io.reactivex.rxjava3.disposables.Disposable;
import kotlin.Unit;
/** /**
* Fragment for selecting a one or more contacts from a list. * Fragment for selecting a one or more contacts from a list.
* *
* @author Moxie Marlinspike * @author Moxie Marlinspike
*
*/ */
public final class ContactSelectionListFragment extends LoggingFragment public final class ContactSelectionListFragment extends LoggingFragment
implements LoaderManager.LoaderCallbacks<Cursor> implements LoaderManager.LoaderCallbacks<Cursor>
{ {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = Log.tag(ContactSelectionListFragment.class); private static final String TAG = Log.tag(ContactSelectionListFragment.class);
private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 1; private static final int CHIP_GROUP_EMPTY_CHILD_COUNT = 0;
private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150; private static final int CHIP_GROUP_REVEAL_DURATION_MS = 150;
public static final int NO_LIMIT = Integer.MAX_VALUE; public static final int NO_LIMIT = Integer.MAX_VALUE;
@@ -132,24 +138,26 @@ public final class ContactSelectionListFragment extends LoggingFragment
private RecyclerView recyclerView; private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller; private RecyclerViewFastScroller fastScroller;
private ContactSelectionListAdapter cursorRecyclerViewAdapter; private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup; private RecyclerView chipRecycler;
private HorizontalScrollView chipGroupScrollContainer;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener; private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider; private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
private View shadowView; private MappingAdapter contactChipAdapter;
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper; private ContactChipViewModel contactChipViewModel;
private LifecycleDisposable lifecycleDisposable;
private HeaderActionProvider headerActionProvider;
private TextView headerActionView;
@Nullable private FixedViewsAdapter headerAdapter; @Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter; @Nullable private FixedViewsAdapter footerAdapter;
@Nullable private ListCallback listCallback; @Nullable private ListCallback listCallback;
@Nullable private ScrollCallback scrollCallback; @Nullable private ScrollCallback scrollCallback;
private GlideRequests glideRequests; private GlideRequests glideRequests;
private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS; private SelectionLimits selectionLimit = SelectionLimits.NO_LIMITS;
private Set<RecipientId> currentSelection; private Set<RecipientId> currentSelection;
private boolean isMulti; private boolean isMulti;
private boolean hideCount; private boolean hideCount;
private boolean canSelectSelf; private boolean canSelectSelf;
@Override @Override
public void onAttach(@NonNull Context context) { public void onAttach(@NonNull Context context) {
@@ -190,6 +198,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) { if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) {
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment(); cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment();
} }
if (context instanceof HeaderActionProvider) {
headerActionProvider = (HeaderActionProvider) context;
}
if (getParentFragment() instanceof HeaderActionProvider) {
headerActionProvider = (HeaderActionProvider) getParentFragment();
}
} }
@Override @Override
@@ -239,15 +255,13 @@ public final class ContactSelectionListFragment extends LoggingFragment
showContactsButton = view.findViewById(R.id.show_contacts_button); showContactsButton = view.findViewById(R.id.show_contacts_button);
showContactsDescription = view.findViewById(R.id.show_contacts_description); showContactsDescription = view.findViewById(R.id.show_contacts_description);
showContactsProgress = view.findViewById(R.id.progress); showContactsProgress = view.findViewById(R.id.progress);
chipGroup = view.findViewById(R.id.chipGroup); chipRecycler = view.findViewById(R.id.chipRecycler);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
constraintLayout = view.findViewById(R.id.container); 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.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setItemAnimator(new DefaultItemAnimator() { recyclerView.setItemAnimator(new DefaultItemAnimator() {
@Override @Override
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) { public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
@@ -255,6 +269,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
}); });
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
contactChipAdapter = new MappingAdapter();
lifecycleDisposable = new LifecycleDisposable();
lifecycleDisposable.bindTo(getViewLifecycleOwner());
SelectedContacts.register(contactChipAdapter, this::onChipCloseIconClicked);
chipRecycler.setAdapter(contactChipAdapter);
Disposable disposable = contactChipViewModel.getState().subscribe(this::handleSelectedContactsChanged);
lifecycleDisposable.add(disposable);
Intent intent = requireActivity().getIntent(); Intent intent = requireActivity().getIntent();
Bundle arguments = safeArguments(); Bundle arguments = safeArguments();
@@ -285,6 +311,40 @@ public final class ContactSelectionListFragment extends LoggingFragment
currentSelection = getCurrentSelection(); currentSelection = getCurrentSelection();
final HeaderAction headerAction;
if (headerActionProvider != null) {
headerAction = headerActionProvider.getHeaderAction();
headerActionView.setEnabled(true);
headerActionView.setText(headerAction.getLabel());
headerActionView.setCompoundDrawablesRelativeWithIntrinsicBounds(headerAction.getIcon(), 0, 0, 0);
headerActionView.setOnClickListener(v -> headerAction.getAction().run());
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
private final Rect bounds = new Rect();
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (hideLetterHeaders()) {
return;
}
int firstPosition = layoutManager.findFirstVisibleItemPosition();
if (firstPosition == 0) {
View firstChild = recyclerView.getChildAt(0);
recyclerView.getDecoratedBoundsWithMargins(firstChild, bounds);
headerActionView.setTranslationY(bounds.top);
}
}
});
} else {
headerActionView.setEnabled(false);
}
return view; return view;
} }
@@ -343,7 +403,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
null, null,
new ListClickListener(), new ListClickListener(),
isMulti, isMulti,
currentSelection); currentSelection,
safeArguments().getInt(ContactSelectionArguments.CHECKBOX_RESOURCE, R.drawable.contact_selection_checkbox));
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader(); RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
@@ -491,12 +552,19 @@ public final class ContactSelectionListFragment extends LoggingFragment
fastScroller.setRecyclerView(null); fastScroller.setRecyclerView(null);
fastScroller.setVisibility(View.GONE); fastScroller.setVisibility(View.GONE);
} }
if (headerActionView.isEnabled() && !hasQueryFilter()) {
headerActionView.setVisibility(View.VISIBLE);
} else {
headerActionView.setVisibility(View.GONE);
}
} }
@Override @Override
public void onLoaderReset(@NonNull Loader<Cursor> loader) { public void onLoaderReset(@NonNull Loader<Cursor> loader) {
cursorRecyclerViewAdapter.changeCursor(null); cursorRecyclerViewAdapter.changeCursor(null);
fastScroller.setVisibility(View.GONE); fastScroller.setVisibility(View.GONE);
headerActionView.setVisibility(View.GONE);
} }
private boolean shouldDisplayRecents() { private boolean shouldDisplayRecents() {
@@ -521,7 +589,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
@Override @Override
protected Boolean doInBackground(Void... voids) { protected Boolean doInBackground(Void... voids) {
try { try {
DirectoryHelper.refreshDirectory(context, false); ContactDiscovery.refreshAll(context, false);
return true; return true;
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
@@ -546,11 +614,44 @@ public final class ContactSelectionListFragment extends LoggingFragment
}.execute(); }.execute();
} }
/**
* Allows the caller to submit a list of recipients to be marked selected. Useful for when a screen needs to load preselected
* entries in the background before setting them in the adapter.
*
* @param contacts List of the contacts to select. This will not overwrite the current selection, but append to it.
*/
public void markSelected(@NonNull Set<ShareContact> contacts) {
if (contacts.isEmpty()) {
return;
}
Set<SelectedContact> toMarkSelected = contacts.stream()
.map(contact -> {
if (contact.getRecipientId().isPresent()) {
return SelectedContact.forRecipientId(contact.getRecipientId().get());
} else {
return SelectedContact.forPhone(null, contact.getNumber());
}
})
.filter(c -> !cursorRecyclerViewAdapter.isSelectedContact(c))
.collect(java.util.stream.Collectors.toSet());
if (toMarkSelected.isEmpty()) {
return;
}
for (final SelectedContact selectedContact : toMarkSelected) {
markContactSelected(selectedContact);
}
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount());
}
private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener { private class ListClickListener implements ContactSelectionListAdapter.ItemClickListener {
@Override @Override
public void onItemClick(ContactSelectionListItem contact) { public void onItemClick(ContactSelectionListItem contact) {
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber()) SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orElse(null), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber()); : SelectedContact.forPhone(contact.getRecipientId().orElse(null), contact.getNumber());
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) { if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show(); Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
@@ -575,8 +676,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
}, uuid -> { }, uuid -> {
loadingDialog.dismiss(); loadingDialog.dismiss();
if (uuid.isPresent()) { if (uuid.isPresent()) {
Recipient recipient = Recipient.externalUsername(uuid.get(), contact.getNumber()); Recipient recipient = Recipient.externalUsername(uuid.get(), contact.getNumber());
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber()); SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
if (onContactSelectedListener != null) { if (onContactSelectedListener != null) {
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> { onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
@@ -646,75 +747,22 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void markContactUnselected(@NonNull SelectedContact selectedContact) { private void markContactUnselected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact); cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE); cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
removeChipForContact(selectedContact); contactChipViewModel.remove(selectedContact);
if (onContactSelectedListener != null) { if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged(); onContactSelectedListener.onSelectionChanged();
} }
} }
private void removeChipForContact(@NonNull SelectedContact contact) { private void handleSelectedContactsChanged(@NonNull List<SelectedContacts.Model> selectedContacts) {
for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) { contactChipAdapter.submitList(new MappingModelList(selectedContacts), this::smoothScrollChipsToEnd);
View v = chipGroup.getChildAt(i);
if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) {
chipGroup.removeView(v);
}
}
if (getChipCount() == 0) { if (selectedContacts.isEmpty()) {
setChipGroupVisibility(ConstraintSet.GONE); setChipGroupVisibility(ConstraintSet.GONE);
} } else {
}
private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
() -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
resolved -> addChipForRecipient(resolved, selectedContact));
}
private void addChipForRecipient(@NonNull Recipient recipient, @NonNull SelectedContact selectedContact) {
final ContactChip chip = new ContactChip(requireContext());
if (getChipCount() == 0) {
setChipGroupVisibility(ConstraintSet.VISIBLE); setChipGroupVisibility(ConstraintSet.VISIBLE);
} }
chip.setText(recipient.getShortDisplayName(requireContext()));
chip.setContact(selectedContact);
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(view -> {
markContactUnselected(selectedContact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(Optional.of(recipient.getId()), recipient.getE164().orNull());
}
});
chipGroup.getLayoutTransition().addTransitionListener(new LayoutTransition.TransitionListener() {
@Override
public void startTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
}
@Override
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
if (getView() == null || !requireView().isAttachedToWindow()) {
Log.w(TAG, "Fragment's view was detached before the animation completed.");
return;
}
if (view == chip && transitionType == LayoutTransition.APPEARING) {
chipGroup.getLayoutTransition().removeTransitionListener(this);
registerChipRecipientObserver(chip, recipient.live());
chipGroup.post(ContactSelectionListFragment.this::smoothScrollChipsToEnd);
}
}
});
chip.setAvatar(glideRequests, recipient, () -> addChip(chip));
}
private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip);
if (selectionWarningLimitReachedExactly()) { if (selectionWarningLimitReachedExactly()) {
if (onSelectionLimitReachedListener != null) { if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit()); onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
@@ -724,21 +772,25 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
} }
private int getChipCount() { private void addChipForSelectedContact(@NonNull SelectedContact selectedContact) {
int count = chipGroup.getChildCount() - CHIP_GROUP_EMPTY_CHILD_COUNT; SimpleTask.run(getViewLifecycleOwner().getLifecycle(),
if (count < 0) throw new AssertionError(); () -> Recipient.resolved(selectedContact.getOrCreateRecipientId(requireContext())),
return count; resolved -> contactChipViewModel.add(selectedContact));
} }
private void registerChipRecipientObserver(@NonNull ContactChip chip, @Nullable LiveRecipient recipient) { private Unit onChipCloseIconClicked(SelectedContacts.Model model) {
if (recipient != null) { markContactUnselected(model.getSelectedContact());
recipient.observe(getViewLifecycleOwner(), resolved -> { if (onContactSelectedListener != null) {
if (chip.isAttachedToWindow()) { onContactSelectedListener.onContactDeselected(Optional.of(model.getRecipient().getId()), model.getRecipient().getE164().orElse(null));
chip.setAvatar(glideRequests, resolved, null);
chip.setText(resolved.getShortDisplayName(chip.getContext()));
}
});
} }
return Unit.INSTANCE;
}
private int getChipCount() {
int count = contactChipViewModel.getCount() - CHIP_GROUP_EMPTY_CHILD_COUNT;
if (count < 0) throw new AssertionError();
return count;
} }
private void setChipGroupVisibility(int visibility) { private void setChipGroupVisibility(int visibility) {
@@ -754,7 +806,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
ConstraintSet constraintSet = new ConstraintSet(); ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(constraintLayout); constraintSet.clone(constraintLayout);
constraintSet.setVisibility(R.id.chipGroupScrollContainer, visibility); constraintSet.setVisibility(R.id.chipRecycler, visibility);
constraintSet.applyTo(constraintLayout); constraintSet.applyTo(constraintLayout);
} }
@@ -763,24 +815,30 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
private void smoothScrollChipsToEnd() { private void smoothScrollChipsToEnd() {
int x = ViewUtil.isLtr(chipGroupScrollContainer) ? chipGroup.getWidth() : 0; int x = ViewUtil.isLtr(chipRecycler) ? chipRecycler.getWidth() : 0;
chipGroupScrollContainer.smoothScrollTo(x, 0); chipRecycler.smoothScrollBy(x, 0);
} }
public interface OnContactSelectedListener { public interface OnContactSelectedListener {
/** Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it. */ /**
void onBeforeContactSelected(Optional<RecipientId> recipientId, @Nullable String number, Consumer<Boolean> callback); * Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it.
void onContactDeselected(Optional<RecipientId> recipientId, @Nullable String number); */
void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, @Nullable String number, @NonNull Consumer<Boolean> callback);
void onContactDeselected(@NonNull Optional<RecipientId> recipientId, @Nullable String number);
void onSelectionChanged(); void onSelectionChanged();
} }
public interface OnSelectionLimitReachedListener { public interface OnSelectionLimitReachedListener {
void onSuggestedLimitReached(int limit); void onSuggestedLimitReached(int limit);
void onHardLimitReached(int limit); void onHardLimitReached(int limit);
} }
public interface ListCallback { public interface ListCallback {
void onInvite(); void onInvite();
void onNewGroup(boolean forceV1); void onNewGroup(boolean forceV1);
} }
@@ -788,6 +846,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
void onBeginScroll(); void onBeginScroll();
} }
public interface HeaderActionProvider {
@NonNull HeaderAction getHeaderAction();
}
public interface AbstractContactsCursorLoaderFactoryProvider { public interface AbstractContactsCursorLoaderFactoryProvider {
@NonNull AbstractContactsCursorLoader.Factory get(); @NonNull AbstractContactsCursorLoader.Factory get();
} }
@@ -17,28 +17,25 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import org.signal.core.util.ThreadUtil; import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.protocol.IdentityKeyPair;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; 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.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; 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.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalServiceAccountManager; import org.whispersystems.signalservice.api.SignalServiceAccountManager;
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException; import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException; import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
@@ -14,36 +14,34 @@ import android.widget.ImageView;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import org.thoughtcrime.securesms.components.camera.CameraView; import org.signal.qr.QrScannerView;
import org.thoughtcrime.securesms.qr.ScanListener; import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.qr.ScanningThread; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleDisposable;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
public class DeviceAddFragment extends LoggingFragment { public class DeviceAddFragment extends LoggingFragment {
private ViewGroup container; private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
private LinearLayout overlay;
private ImageView devicesImage; private ImageView devicesImage;
private CameraView scannerView; private ScanListener scanListener;
private ScanningThread scanningThread;
private ScanListener scanListener;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment); ViewGroup 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);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { QrScannerView scannerView = container.findViewById(R.id.scanner);
this.overlay.setOrientation(LinearLayout.HORIZONTAL); this.devicesImage = container.findViewById(R.id.devices);
} else { ViewCompat.setTransitionName(devicesImage, "devices");
this.overlay.setOrientation(LinearLayout.VERTICAL);
}
if (Build.VERSION.SDK_INT >= 21) { if (Build.VERSION.SDK_INT >= 21) {
this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() { container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@TargetApi(21) @TargetApi(21)
@Override @Override
public void onLayoutChange(View v, int left, int top, int right, int bottom, 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() { public ImageView getDevicesImage() {
return devicesImage; return devicesImage;
} }
public void setScanListener(ScanListener scanListener) { public void setScanListener(ScanListener scanListener) {
this.scanListener = scanListener; this.scanListener = scanListener;
if (this.scanningThread != null) {
this.scanningThread.setScanListener(scanListener);
}
} }
} }
@@ -9,6 +9,7 @@ import android.view.ViewGroup;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
public class DeviceLinkFragment extends Fragment implements View.OnClickListener { 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) { public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = (LinearLayout) inflater.inflate(R.layout.device_link_fragment, container, false); this.container = (LinearLayout) inflater.inflate(R.layout.device_link_fragment, container, false);
this.container.findViewById(R.id.link_device).setOnClickListener(this); this.container.findViewById(R.id.link_device).setOnClickListener(this);
ViewCompat.setTransitionName(container.findViewById(R.id.devices), "devices");
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
container.setOrientation(LinearLayout.HORIZONTAL); container.setOrientation(LinearLayout.HORIZONTAL);
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@@ -22,7 +21,7 @@ import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader; import androidx.loader.content.Loader;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; 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.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader; import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
@@ -16,6 +16,7 @@ import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.AnimRes; import androidx.annotation.AnimRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
@@ -37,8 +38,8 @@ import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged; import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -134,13 +135,13 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
} }
@Override @Override
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) { public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1); updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
callback.accept(true); callback.accept(true);
} }
@Override @Override
public void onContactDeselected(Optional<RecipientId> recipientId, String number) { public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number) {
updateSmsButtonText(contactsFragment.getSelectedContacts().size()); updateSmsButtonText(contactsFragment.getSelectedContacts().size());
} }
@@ -250,7 +251,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
for (SelectedContact contact : contacts) { for (SelectedContact contact : contacts) {
RecipientId recipientId = contact.getOrCreateRecipientId(context); RecipientId recipientId = contact.getOrCreateRecipientId(context);
Recipient recipient = Recipient.resolved(recipientId); Recipient recipient = Recipient.resolved(recipientId);
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1); int subscriptionId = recipient.getDefaultSubscriptionId().orElse(-1);
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null); MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
@@ -5,19 +5,27 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController; import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner; import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog; import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog;
import org.thoughtcrime.securesms.keyvalue.SignalStore; 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.ConversationListTabsViewModel;
import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.CachedInflater; import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; 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 { public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
@@ -26,13 +34,14 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final MainNavigator navigator = new MainNavigator(this); private final MainNavigator navigator = new MainNavigator(this);
private VoiceNoteMediaController mediaController; private VoiceNoteMediaController mediaController;
private ConversationListTabsViewModel conversationListTabsViewModel;
public static @NonNull Intent clearTop(@NonNull Context context) { public static @NonNull Intent clearTop(@NonNull Context context) {
Intent intent = new Intent(context, MainActivity.class); Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_SINGLE_TOP); Intent.FLAG_ACTIVITY_SINGLE_TOP);
return intent; return intent;
@@ -42,22 +51,28 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
protected void onCreate(Bundle savedInstanceState, boolean ready) { protected void onCreate(Bundle savedInstanceState, boolean ready) {
AppStartup.getInstance().onCriticalRenderEventStart(); AppStartup.getInstance().onCriticalRenderEventStart();
super.onCreate(savedInstanceState, ready); super.onCreate(savedInstanceState, ready);
setContentView(R.layout.main_activity); setContentView(R.layout.main_activity);
mediaController = new VoiceNoteMediaController(this); mediaController = new VoiceNoteMediaController(this);
navigator.onCreate(savedInstanceState);
ConversationListTabRepository repository = new ConversationListTabRepository();
ConversationListTabsViewModel.Factory factory = new ConversationListTabsViewModel.Factory(repository);
handleGroupLinkInIntent(getIntent()); handleGroupLinkInIntent(getIntent());
handleProxyInIntent(getIntent()); handleProxyInIntent(getIntent());
handleSignalMeIntent(getIntent()); handleSignalMeIntent(getIntent());
CachedInflater.from(this).clear(); CachedInflater.from(this).clear();
conversationListTabsViewModel = new ViewModelProvider(this, factory).get(ConversationListTabsViewModel.class);
updateTabVisibility();
} }
@Override @Override
public Intent getIntent() { public Intent getIntent() {
return super.getIntent().setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | return super.getIntent().setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_SINGLE_TOP); Intent.FLAG_ACTIVITY_SINGLE_TOP);
} }
@@ -82,6 +97,14 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
if (SignalStore.misc().isOldDeviceTransferLocked()) { if (SignalStore.misc().isOldDeviceTransferLocked()) {
OldDeviceTransferLockedDialog.show(getSupportFragmentManager()); OldDeviceTransferLockedDialog.show(getSupportFragmentManager());
} }
updateTabVisibility();
}
@Override
protected void onStop() {
super.onStop();
SplashScreenUtil.setSplashScreenThemeIfNecessary(this, SignalStore.settings().getTheme());
} }
@Override @Override
@@ -99,6 +122,17 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
} }
} }
private void updateTabVisibility() {
if (Stories.isFeatureEnabled()) {
findViewById(R.id.conversation_list_tabs).setVisibility(View.VISIBLE);
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorSurface2));
} else {
findViewById(R.id.conversation_list_tabs).setVisibility(View.GONE);
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorBackground));
conversationListTabsViewModel.onChatsSelected();
}
}
public @NonNull MainNavigator getNavigator() { public @NonNull MainNavigator getNavigator() {
return navigator; return navigator;
} }
@@ -2,18 +2,13 @@ package org.thoughtcrime.securesms;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity; import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.conversation.ConversationIntents; 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.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.insights.InsightsLauncher; import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -36,16 +31,6 @@ public class MainNavigator {
return ((MainActivity) activity).getNavigator(); 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 * @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. * to the system to do the default behavior.
@@ -74,14 +59,6 @@ public class MainNavigator {
activity.startActivityForResult(AppSettingsActivity.home(activity), REQUEST_CONFIG_CHANGES); 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 goToGroupCreation() { public void goToGroupCreation() {
activity.startActivity(CreateGroupActivity.newIntent(activity)); activity.startActivity(CreateGroupActivity.newIntent(activity));
} }
@@ -55,6 +55,10 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.animation.DepthPageTransformer; import org.thoughtcrime.securesms.animation.DepthPageTransformer;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener; 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;
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord; import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader; 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.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FullscreenHelper; import org.thoughtcrime.securesms.util.FullscreenHelper;
@@ -87,7 +90,8 @@ import java.util.Objects;
public final class MediaPreviewActivity extends PassphraseRequiredActivity public final class MediaPreviewActivity extends PassphraseRequiredActivity
implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>, implements LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
MediaRailAdapter.RailItemListener, MediaRailAdapter.RailItemListener,
MediaPreviewFragment.Events MediaPreviewFragment.Events,
VoiceNoteMediaControllerOwner
{ {
private final static String TAG = Log.tag(MediaPreviewActivity.class); private final static String TAG = Log.tag(MediaPreviewActivity.class);
@@ -127,6 +131,8 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private MediaDatabase.Sorting sorting; private MediaDatabase.Sorting sorting;
private FullscreenHelper fullscreenHelper; private FullscreenHelper fullscreenHelper;
private VoiceNoteMediaController voiceNoteMediaController;
private @Nullable Cursor cursor = null; private @Nullable Cursor cursor = null;
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context, public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
@@ -159,6 +165,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
setSupportActionBar(findViewById(R.id.toolbar)); setSupportActionBar(findViewById(R.id.toolbar));
voiceNoteMediaController = new VoiceNoteMediaController(this);
viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class); viewModel = ViewModelProviders.of(this).get(MediaPreviewViewModel.class);
fullscreenHelper = new FullscreenHelper(this); fullscreenHelper = new FullscreenHelper(this);
@@ -203,23 +210,25 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
else from = ""; else from = "";
if (showThread) { if (showThread) {
String to = null; String titleText = null;
Recipient threadRecipient = mediaItem.threadRecipient; Recipient threadRecipient = mediaItem.threadRecipient;
if (threadRecipient != null) { if (threadRecipient != null) {
if (mediaItem.outgoing || threadRecipient.isGroup()) { if (mediaItem.outgoing) {
if (threadRecipient.isSelf()) { if (threadRecipient.isSelf()) {
from = getString(R.string.note_to_self); titleText = getString(R.string.note_to_self);
} else { } else {
to = threadRecipient.getDisplayName(this); titleText = getString(R.string.MediaPreviewActivity_you_to_s, threadRecipient.getDisplayName(this));
} }
} else { } 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 titleText != null ? titleText : from;
return to != null ? getString(R.string.MediaPreviewActivity_s_to_s, from, to)
: from;
} else { } else {
return from; return from;
} }
@@ -286,7 +295,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
anchorMarginsToBottomInsets(detailsContainer); 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); fullscreenHelper.showAndHideWithSystemUI(getWindow(), detailsContainer, toolbarLayout);
} }
@@ -386,10 +395,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
MediaItem mediaItem = getCurrentMediaItem(); MediaItem mediaItem = getCurrentMediaItem();
if (mediaItem != null) { if (mediaItem != null) {
Intent composeIntent = new Intent(this, ShareActivity.class); MultiselectForwardFragmentArgs.create(
composeIntent.putExtra(Intent.EXTRA_STREAM, mediaItem.uri); this,
composeIntent.setType(mediaItem.type); mediaItem.uri,
startActivity(composeIntent); mediaItem.type,
args -> MultiselectForwardFragment.showBottomSheet(getSupportFragmentManager(), args)
);
} }
} }
@@ -553,6 +564,8 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
} }
cursor = Objects.requireNonNull(data.first); cursor = Objects.requireNonNull(data.first);
viewModel.setCursor(this, cursor, leftIsRecent);
int mediaPosition = Objects.requireNonNull(data.second); int mediaPosition = Objects.requireNonNull(data.second);
CursorPagerAdapter oldAdapter = (CursorPagerAdapter) mediaPager.getAdapter(); CursorPagerAdapter oldAdapter = (CursorPagerAdapter) mediaPager.getAdapter();
@@ -565,13 +578,13 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
oldAdapter.setActive(true); oldAdapter.setActive(true);
} }
viewModel.setCursor(this, cursor, leftIsRecent); if (oldAdapter == null || restartItem >= 0) {
int item = restartItem >= 0 ? restartItem : mediaPosition;
mediaPager.setCurrentItem(item);
int item = restartItem >= 0 ? restartItem : mediaPosition; if (item == 0) {
mediaPager.setCurrentItem(item); viewPagerListener.onPageSelected(0);
}
if (item == 0) {
viewPagerListener.onPageSelected(0);
} }
} else { } else {
mediaNotAvailable(); mediaNotAvailable();
@@ -595,6 +608,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
finish(); finish();
} }
@Override
public void onMediaReady() {
}
@Override
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
return voiceNoteMediaController;
}
private class ViewPagerListener extends ExtendedOnPageChangedListener { private class ViewPagerListener extends ExtendedOnPageChangedListener {
@Override @Override
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@@ -21,10 +21,11 @@ import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import org.signal.core.util.logging.Log; 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.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity; import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
@@ -32,11 +33,11 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.signal.core.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.util.Optional;
import java.util.function.Consumer; import java.util.function.Consumer;
/** /**
@@ -61,7 +62,7 @@ public class NewConversationActivity extends ContactSelectionActivity
} }
@Override @Override
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) { public void onBeforeContactSelected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
if (recipientId.isPresent()) { if (recipientId.isPresent()) {
launch(Recipient.resolved(recipientId.get())); launch(Recipient.resolved(recipientId.get()));
} else { } else {
@@ -78,7 +79,7 @@ public class NewConversationActivity extends ContactSelectionActivity
if (!resolved.isRegistered() || !resolved.hasServiceId()) { if (!resolved.isRegistered() || !resolved.hasServiceId()) {
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh."); Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
try { try {
DirectoryHelper.refreshDirectoryFor(this, resolved, false); ContactDiscovery.refresh(this, resolved, false);
resolved = Recipient.resolved(resolved.getId()); resolved = Recipient.resolved(resolved.getId());
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact."); Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
@@ -19,7 +19,6 @@ package org.thoughtcrime.securesms;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -62,8 +61,8 @@ public class PassphraseCreateActivity extends PassphraseActivity {
passphrase); passphrase);
MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret); MasterSecretUtil.generateAsymmetricMasterSecret(PassphraseCreateActivity.this, masterSecret);
SignalStore.account().generateAciIdentityKey(); SignalStore.account().generateAciIdentityKeyIfNecessary();
SignalStore.account().generatePniIdentityKey(); SignalStore.account().generatePniIdentityKeyIfNecessary();
VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this); VersionTracker.updateLastSeenVersion(PassphraseCreateActivity.this);
return null; return null;
@@ -171,6 +171,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_WELCOME_PUSH_SCREEN; return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.storageService().needsAccountRestore()) { } else if (SignalStore.storageService().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN; return STATE_ENTER_SIGNAL_PIN;
} else if (userHasSkippedOrForgottenPin()) {
return STATE_CREATE_SIGNAL_PIN;
} else if (userMustSetProfileName()) { } else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME; return STATE_CREATE_PROFILE_NAME;
} else if (userMustCreateSignalPin()) { } 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(); 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() { private boolean userMustSetProfileName() {
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty(); return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
} }
@@ -1,147 +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 org.whispersystems.libsignal.util.guava.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.<CharSequence>absent(), Optional.<Integer>absent());
}
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.fromNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.absent());
}
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.orNull(), 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];
}
};
}
@@ -1,225 +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.libsignal.util.guava.Optional;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
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.absent();
private Optional<TransportOption> selectedOption = Optional.absent();
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.absent();
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.fromNullable(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(defaultSubscriptionId.or(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().or(-1)) {
return transportOption;
}
}
}
return null;
}
public void disableTransport(Type type) {
TransportOption selected = selectedOption.orNull();
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);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -39,16 +39,19 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer; import androidx.core.util.Consumer;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProvider;
import androidx.window.DisplayFeature; import androidx.window.DisplayFeature;
import androidx.window.FoldingFeature; import androidx.window.FoldingFeature;
import androidx.window.WindowLayoutInfo; import androidx.window.WindowLayoutInfo;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor; import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow; 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.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThrottledDebouncer; import org.thoughtcrime.securesms.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VibrateUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState; import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit; 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; import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback { 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 String TAG = Log.tag(WebRtcCallActivity.class);
private static final int STANDARD_DELAY_FINISH = 1000; 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 ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_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 TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel; private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable; private boolean enableVideoIfAvailable;
private boolean hasWarnedAboutBluetooth;
private androidx.window.WindowManager windowManager; private androidx.window.WindowManager windowManager;
private WindowLayoutInfoConsumer windowLayoutInfoConsumer; private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
private ThrottledDebouncer requestNewSizesThrottle; private ThrottledDebouncer requestNewSizesThrottle;
private Disposable ephemeralStateDisposable = Disposable.empty();
@Override @Override
protected void attachBaseContext(@NonNull Context newBase) { protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES); getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
@@ -153,6 +163,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1)); requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
} }
@Override
protected void onStart() {
super.onStart();
ephemeralStateDisposable = ApplicationDependencies.getSignalCallManager()
.ephemeralStates()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(viewModel::updateFromEphemeralState);
}
@Override @Override
public void onResume() { public void onResume() {
Log.i(TAG, "onResume()"); Log.i(TAG, "onResume()");
@@ -193,6 +213,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
Log.i(TAG, "onStop"); Log.i(TAG, "onStop");
super.onStop(); super.onStop();
ephemeralStateDisposable.dispose();
if (!isInPipMode() || isFinishing()) { if (!isInPipMode() || isFinishing()) {
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
requestNewSizesThrottle.clear(); requestNewSizesThrottle.clear();
@@ -285,7 +307,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor); 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.setIsLandscapeEnabled(isLandscapeEnabled);
viewModel.setIsInPipMode(isInPipMode()); viewModel.setIsInPipMode(isInPipMode());
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled); viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
@@ -295,7 +317,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(), LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
viewModel.getOrientationAndLandscapeEnabled(), 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)); .observe(this, p -> callScreen.updateCallParticipants(p));
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate); viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent); 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() { private void handleRecipientUnavailable() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class); EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable)); callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
@@ -623,6 +651,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
handleCallPreJoin(event); break; handleCallPreJoin(event); break;
case CALL_CONNECTED: case CALL_CONNECTED:
handleCallConnected(event); break; handleCallConnected(event); break;
case CALL_RECONNECTING:
handleCallReconnecting(); break;
case NETWORK_FAILURE: case NETWORK_FAILURE:
handleServerFailure(); break; handleServerFailure(); break;
case CALL_RINGING: case CALL_RINGING:
@@ -659,6 +689,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
enableVideoIfAvailable = false; enableVideoIfAvailable = false;
handleSetMuteVideo(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) { private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
@@ -807,7 +848,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
if (feature.isPresent()) { if (feature.isPresent()) {
FoldingFeature foldingFeature = (FoldingFeature) feature.get(); FoldingFeature foldingFeature = (FoldingFeature) feature.get();
Rect bounds = foldingFeature.getBounds(); 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"); Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top display mode");
viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top)); viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top));
} else { } else {
@@ -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)
}
}
@@ -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
}
}
}
}
@@ -9,12 +9,12 @@ import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.Base64;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Optional;
public class PointerAttachment extends Attachment { public class PointerAttachment extends Attachment {
@@ -93,7 +93,7 @@ public class PointerAttachment extends Attachment {
} }
public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> pointer, @Nullable StickerLocator stickerLocator, @Nullable String fastPreflightId) { public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> pointer, @Nullable StickerLocator stickerLocator, @Nullable String fastPreflightId) {
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.absent(); if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.empty();
String encodedKey = null; String encodedKey = null;
@@ -103,12 +103,12 @@ public class PointerAttachment extends Attachment {
return Optional.of(new PointerAttachment(pointer.get().getContentType(), return Optional.of(new PointerAttachment(pointer.get().getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING, AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().or(0), pointer.get().asPointer().getSize().orElse(0),
pointer.get().asPointer().getFileName().orNull(), pointer.get().asPointer().getFileName().orElse(null),
pointer.get().asPointer().getCdnNumber(), pointer.get().asPointer().getCdnNumber(),
pointer.get().asPointer().getRemoteId().toString(), pointer.get().asPointer().getRemoteId().toString(),
encodedKey, null, encodedKey, null,
pointer.get().asPointer().getDigest().orNull(), pointer.get().asPointer().getDigest().orElse(null),
fastPreflightId, fastPreflightId,
pointer.get().asPointer().getVoiceNote(), pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().isBorderless(), pointer.get().asPointer().isBorderless(),
@@ -116,9 +116,9 @@ public class PointerAttachment extends Attachment {
pointer.get().asPointer().getWidth(), pointer.get().asPointer().getWidth(),
pointer.get().asPointer().getHeight(), pointer.get().asPointer().getHeight(),
pointer.get().asPointer().getUploadTimestamp(), pointer.get().asPointer().getUploadTimestamp(),
pointer.get().asPointer().getCaption().orNull(), pointer.get().asPointer().getCaption().orElse(null),
stickerLocator, stickerLocator,
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull()))); BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orElse(null))));
} }
@@ -127,13 +127,13 @@ public class PointerAttachment extends Attachment {
return Optional.of(new PointerAttachment(pointer.getContentType(), return Optional.of(new PointerAttachment(pointer.getContentType(),
AttachmentDatabase.TRANSFER_PROGRESS_PENDING, AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0, thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
pointer.getFileName(), pointer.getFileName(),
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0, thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0", thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null, thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
null, null,
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null, thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
null, null,
false, false,
false, false,
@@ -141,7 +141,7 @@ public class PointerAttachment extends Attachment {
thumbnail != null ? thumbnail.asPointer().getWidth() : 0, thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0, thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0, thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null, thumbnail != null ? thumbnail.asPointer().getCaption().orElse(null) : null,
null, null,
null)); null));
} }
@@ -12,7 +12,6 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft; import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
@@ -14,10 +14,10 @@ import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.profiles.AvatarHelper import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.MediaUtil import org.thoughtcrime.securesms.util.MediaUtil
import org.whispersystems.libsignal.util.guava.Optional
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
import java.util.Optional
import javax.annotation.meta.Exhaustive import javax.annotation.meta.Exhaustive
/** /**
@@ -128,6 +128,6 @@ object AvatarRenderer {
} }
private fun createMedia(uri: Uri, size: Long): Media { private fun createMedia(uri: Uri, size: Long): Media {
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent()) return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty())
} }
} }
@@ -31,8 +31,8 @@ class TextAvatarDrawable(
} }
override fun draw(canvas: Canvas) { override fun draw(canvas: Canvas) {
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
val width = bounds.width() val width = bounds.width()
val textSize = Avatars.getTextSizeForLength(context, avatar.text, width * 0.8f, width * 0.45f)
val candidates = EmojiProvider.getCandidates(avatar.text) val candidates = EmojiProvider.getCandidates(avatar.text)
textPaint.textSize = textSize textPaint.textSize = textSize
@@ -81,14 +81,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
} }
clearButton.visible = state.canClear clearButton.visible = state.canClear
saveButton.isClickable = state.canSave
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)
}
val items = state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) } val items = state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) }
val selectedPosition = items.indexOfFirst { it.isSelected } val selectedPosition = items.indexOfFirst { it.isSelected }
@@ -104,6 +97,11 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
photoButton.setOnIconClickedListener { openGallery() } photoButton.setOnIconClickedListener { openGallery() }
textButton.setOnIconClickedListener { openTextEditor(null) } textButton.setOnIconClickedListener { openTextEditor(null) }
saveButton.setOnClickListener { v -> saveButton.setOnClickListener { v ->
if (!saveButton.isEnabled) {
return@setOnClickListener
}
saveButton.isEnabled = false
viewModel.save( viewModel.save(
{ {
setFragmentResult( setFragmentResult(
@@ -183,7 +183,7 @@ sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepositor
private val isNewGroup: Boolean, private val isNewGroup: Boolean,
private val groupAvatarMedia: Media? private val groupAvatarMedia: Media?
) : ViewModelProvider.Factory { ) : 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) { val viewModel = if (groupId == null && !isNewGroup) {
SelfAvatarPickerViewModel(repository) SelfAvatarPickerViewModel(repository)
} else if (groupId == null) { } else if (groupId == null) {
@@ -33,7 +33,7 @@ class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
} }
class Factory(private val initialText: Avatar.Text) : ViewModelProvider.Factory { 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))) return requireNotNull(modelClass.cast(TextAvatarCreationViewModel(initialText)))
} }
} }
@@ -20,7 +20,7 @@ class VectorAvatarCreationViewModel(initialAvatar: Avatar.Vector) : ViewModel()
fun getCurrentAvatar() = store.state.currentAvatar fun getCurrentAvatar() = store.state.currentAvatar
class Factory(private val initialAvatar: Avatar.Vector) : ViewModelProvider.Factory { 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))) return requireNotNull(modelClass.cast(VectorAvatarCreationViewModel(initialAvatar)))
} }
} }
@@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.avatar.view
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.visible
/**
* AvatarView encapsulating the AvatarImageView and decorations.
*/
class AvatarView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {
init {
inflate(context, R.layout.avatar_view, this)
isClickable = false
}
private val avatar: AvatarImageView = findViewById<AvatarImageView>(R.id.avatar_image_view).apply {
initialize(context, attrs)
}
private val storyRing: View = findViewById(R.id.avatar_story_ring)
private fun showStoryRing(hasUnreadStory: Boolean) {
if (!Stories.isFeatureEnabled()) {
return
}
storyRing.visible = true
storyRing.isActivated = hasUnreadStory
avatar.scaleX = 0.8f
avatar.scaleY = 0.8f
}
private fun hideStoryRing() {
storyRing.visible = false
avatar.scaleX = 1f
avatar.scaleY = 1f
}
fun hasStory(): Boolean {
return storyRing.visible
}
fun setStoryRingFromState(storyViewState: StoryViewState) {
when (storyViewState) {
StoryViewState.NONE -> hideStoryRing()
StoryViewState.UNVIEWED -> showStoryRing(true)
StoryViewState.VIEWED -> showStoryRing(false)
}
}
/**
* Displays Note-to-Self
*/
fun displayChatAvatar(recipient: Recipient) {
avatar.setAvatar(recipient)
}
/**
* Displays Note-to-Self
*/
fun displayChatAvatar(requestManager: GlideRequests, recipient: Recipient, isQuickContactEnabled: Boolean) {
avatar.setAvatar(requestManager, recipient, isQuickContactEnabled)
}
/**
* Displays Profile image
*/
fun displayProfileAvatar(recipient: Recipient) {
avatar.setRecipient(recipient)
}
fun setFallbackPhotoProvider(fallbackPhotoProvider: Recipient.FallbackPhotoProvider) {
avatar.setFallbackPhotoProvider(fallbackPhotoProvider)
}
fun disableQuickContact() {
avatar.disableQuickContact()
}
}
@@ -23,6 +23,8 @@ import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -110,7 +112,7 @@ public class BackupDialog {
@RequiresApi(29) @RequiresApi(29)
public static void showChooseBackupLocationDialog(@NonNull Fragment fragment, int requestCode) { 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) .setView(R.layout.backup_choose_location_dialog)
.setCancelable(true) .setCancelable(true)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> { .setNegativeButton(android.R.string.cancel, (dialog, which) -> {
@@ -141,7 +143,7 @@ public class BackupDialog {
} }
public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) { public static void showDisableBackupDialog(@NonNull Context context, @NonNull Runnable onBackupsDisabled) {
new AlertDialog.Builder(context) new MaterialAlertDialogBuilder(context)
.setTitle(R.string.BackupDialog_delete_backups) .setTitle(R.string.BackupDialog_delete_backups)
.setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups) .setMessage(R.string.BackupDialog_disable_and_delete_all_local_backups)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
@@ -157,12 +159,12 @@ public class BackupDialog {
public static void showVerifyBackupPassphraseDialog(@NonNull Context context) { public static void showVerifyBackupPassphraseDialog(@NonNull Context context) {
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null); View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
EditText prompt = view.findViewById(R.id.restore_passphrase_input); EditText prompt = view.findViewById(R.id.restore_passphrase_input);
AlertDialog dialog = new AlertDialog.Builder(context) AlertDialog dialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify) .setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify)
.setView(view) .setView(view)
.setPositiveButton(R.string.BackupDialog_verify, null) .setPositiveButton(R.string.BackupDialog_verify, null)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show(); .show();
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setEnabled(false); positiveButton.setEnabled(false);
@@ -5,8 +5,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.util.ByteUtil;
import org.whispersystems.libsignal.util.ByteUtil;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
@@ -18,10 +18,11 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions; import org.signal.core.util.Conversions;
import org.signal.core.util.logging.Log; 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.attachments.AttachmentId;
import org.thoughtcrime.securesms.crypto.AttachmentSecret; import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase; import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
@@ -47,13 +48,11 @@ import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil; import org.signal.core.util.CursorUtil;
import org.thoughtcrime.securesms.util.SetUtil; import org.signal.core.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; 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.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
@@ -198,7 +197,9 @@ public class FullBackupExporter extends FullBackupBase {
throwIfCanceled(cancellationSignal); throwIfCanceled(cancellationSignal);
if (avatar != null) { if (avatar != null) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); 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());
}
} }
} }
@@ -378,6 +379,7 @@ public class FullBackupExporter extends FullBackupBase {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size); outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
inputStream.close();
} }
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
@@ -396,8 +398,9 @@ public class FullBackupExporter extends FullBackupBase {
if (!TextUtils.isEmpty(data) && size > 0) { if (!TextUtils.isEmpty(data) && size > 0) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); try (InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0)) {
outputStream.writeSticker(rowId, inputStream, size); outputStream.writeSticker(rowId, inputStream, size);
}
} }
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
@@ -17,6 +17,8 @@ import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions; import org.signal.core.util.Conversions;
import org.signal.core.util.StreamUtil; import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log; 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.Attachment;
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame; import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion; 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.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil; import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.SqlUtil; import org.signal.core.util.SqlUtil;
import org.whispersystems.libsignal.kdf.HKDFv3;
import org.whispersystems.libsignal.util.ByteUtil;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
@@ -69,6 +69,18 @@ public class FullBackupImporter extends FullBackupBase {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = Log.tag(FullBackupImporter.class); private static final String TAG = Log.tag(FullBackupImporter.class);
private static final String[] TABLES_TO_DROP_FIRST = {
"distribution_list_member",
"distribution_list",
"message_send_log_recipients",
"msl_recipient",
"msl_message",
"reaction",
"notification_profile_schedule",
"notification_profile_allowed_members",
"story_sends"
};
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase) @NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
throws IOException throws IOException
@@ -85,12 +97,12 @@ public class FullBackupImporter extends FullBackupBase {
int count = 0; int count = 0;
SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).getSqlCipherDatabase(); SQLiteDatabase keyValueDatabase = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()).getSqlCipherDatabase();
db.beginTransaction();
keyValueDatabase.beginTransaction();
try { try {
BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase); BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase);
db.beginTransaction();
keyValueDatabase.beginTransaction();
dropAllTables(db); dropAllTables(db);
BackupFrame frame; BackupFrame frame;
@@ -272,12 +284,17 @@ public class FullBackupImporter extends FullBackupBase {
} }
private static void dropAllTables(@NonNull SQLiteDatabase db) { private static void dropAllTables(@NonNull SQLiteDatabase db) {
for (String name : TABLES_TO_DROP_FIRST) {
db.execSQL("DROP TABLE IF EXISTS " + name);
}
try (Cursor cursor = db.rawQuery("SELECT name, type FROM sqlite_master", null)) { try (Cursor cursor = db.rawQuery("SELECT name, type FROM sqlite_master", null)) {
while (cursor != null && cursor.moveToNext()) { while (cursor != null && cursor.moveToNext()) {
String name = cursor.getString(0); String name = cursor.getString(0);
String type = cursor.getString(1); String type = cursor.getString(1);
if ("table".equals(type) && !name.startsWith("sqlite_")) { if ("table".equals(type) && !name.startsWith("sqlite_")) {
Log.i(TAG, "Dropping table: " + name);
db.execSQL("DROP TABLE IF EXISTS " + name); db.execSQL("DROP TABLE IF EXISTS " + name);
} }
} }
@@ -8,11 +8,13 @@ import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
import org.thoughtcrime.securesms.badges.models.Badge 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.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ScreenDensity
import org.thoughtcrime.securesms.util.ThemeUtil import org.thoughtcrime.securesms.util.ThemeUtil
import java.lang.IllegalArgumentException
class BadgeImageView @JvmOverloads constructor( class BadgeImageView @JvmOverloads constructor(
context: Context, context: Context,
@@ -78,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() { private fun clearDrawable() {
setImageDrawable(null) setImageDrawable(null)
isClickable = false isClickable = false
@@ -1,41 +1,83 @@
package org.thoughtcrime.securesms.badges package org.thoughtcrime.securesms.badges
import android.content.Context import android.content.Context
import androidx.annotation.WorkerThread
import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.RecipientDatabase import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.SignalDatabase 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.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ProfileUtil import org.thoughtcrime.securesms.util.ProfileUtil
import java.io.IOException
class BadgeRepository(context: Context) { class BadgeRepository(context: Context) {
companion object {
private val TAG = Log.tag(BadgeRepository::class.java)
}
private val context = context.applicationContext 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( fun setVisibilityForAllBadges(
displayBadgesOnProfile: Boolean, displayBadgesOnProfile: Boolean,
selfBadges: List<Badge> = Recipient.self().badges selfBadges: List<Badge> = Recipient.self().badges
): Completable = Completable.fromAction { ): Completable = Completable.fromAction {
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients setVisibilityForAllBadgesSync(displayBadgesOnProfile, selfBadges)
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
ProfileUtil.uploadProfileWithBadges(context, badges) Log.d(TAG, "[setVisibilityForAllBadges] Enqueueing profile refresh...", true)
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile) ApplicationDependencies.getJobManager()
recipientDatabase.markNeedsSync(Recipient.self().id) .startChain(RefreshOwnProfileJob())
StorageSyncHelper.scheduleSyncForDataChange() .then(MultiDeviceProfileContentUpdateJob())
.enqueue()
recipientDatabase.setBadges(Recipient.self().id, badges)
}.subscribeOn(Schedulers.io()) }.subscribeOn(Schedulers.io())
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction { fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
val badges = Recipient.self().badges val badges = Recipient.self().badges
val reOrderedBadges = listOf(featuredBadge.copy(visible = true)) + (badges.filterNot { it.id == featuredBadge.id }) 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) ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients Log.d(TAG, "[setFeaturedBadge] Enqueueing profile refresh...", true)
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges) ApplicationDependencies.getJobManager()
.startChain(RefreshOwnProfileJob())
.then(MultiDeviceProfileContentUpdateJob())
.enqueue()
}.subscribeOn(Schedulers.io()) }.subscribeOn(Schedulers.io())
} }
@@ -9,16 +9,17 @@ import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.flexbox.JustifyContent import com.google.android.flexbox.JustifyContent
import org.signal.core.util.DimensionUnit import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.util.Pair
import org.thoughtcrime.securesms.BuildConfig import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.badges.models.Badge import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
import org.thoughtcrime.securesms.components.settings.DSLConfiguration import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
import org.thoughtcrime.securesms.util.ScreenDensity import org.thoughtcrime.securesms.util.ScreenDensity
import org.whispersystems.libsignal.util.Pair
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import java.math.BigDecimal import java.math.BigDecimal
import java.sql.Timestamp import java.sql.Timestamp
import java.util.concurrent.TimeUnit
object Badges { object Badges {
@@ -93,7 +94,8 @@ object Badges {
Uri.parse(badge.imageUrl), Uri.parse(badge.imageUrl),
badge.imageDensity, badge.imageDensity,
badge.expiration, badge.expiration,
badge.visible badge.visible,
0L
) )
} }
@@ -122,7 +124,8 @@ object Badges {
uriAndDensity.first(), uriAndDensity.first(),
uriAndDensity.second(), uriAndDensity.second(),
serviceBadge.expiration?.let { getTimestamp(it) } ?: 0, serviceBadge.expiration?.let { getTimestamp(it) } ?: 0,
serviceBadge.isVisible serviceBadge.isVisible,
TimeUnit.SECONDS.toMillis(serviceBadge.duration)
) )
} }
} }
@@ -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()
}
}
@@ -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()
}
)
}
}
}
@@ -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()
}
}
@@ -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)
}
}
@@ -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()
}
@@ -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
}
}
@@ -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)
@@ -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()
}
}
}
}
@@ -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))
}
}
}
@@ -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
}
@@ -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) }
}
}
}
@@ -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
)
)
}
}
}
}
}

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