Compare commits

..

250 Commits

Author SHA1 Message Date
Greyson Parrelli
9d8501cd64 Bump version to 6.25.5 2023-07-11 14:21:30 -04:00
Greyson Parrelli
88827e94f5 Updated language translations. 2023-07-11 14:21:30 -04:00
Alex Hart
99d2a0c0b6 Wrap dialog content in a scroll view. 2023-07-11 14:21:30 -04:00
Alex Hart
1d0582867b Fix crashing when deleting a custom story. 2023-07-11 14:21:30 -04:00
Alex Hart
0ea6d9205d Fix rotation metrics for DeliveryStatusView. 2023-07-11 14:21:30 -04:00
Greyson Parrelli
f438ef543b Fix prekey generation during registration. 2023-07-10 23:05:36 -04:00
Alex Hart
61cd9767c8 Ensure bitmaps are not recycled when getting them from the cache. 2023-07-10 17:08:27 -03:00
Alex Hart
5fbf0a98b9 Bump version to 6.25.4 2023-07-07 15:08:04 -03:00
Alex Hart
1671518ded Updated language translations. 2023-07-07 15:04:13 -03:00
Alex Hart
f628ffca06 Fix avatar blurring during calls. 2023-07-07 14:53:23 -03:00
Alex Hart
1d6f4fd4e7 Bump version to 6.25.3 2023-07-06 16:27:55 -03:00
Alex Hart
9eedc0a36b Updated language translations. 2023-07-06 16:19:42 -03:00
Alex Hart
a870fe9e1a Do not throw an ISE when we cannot start a foreground service from calling. 2023-07-06 16:12:09 -03:00
Alex Hart
d3f779cea9 Fix crash when toggling stories. 2023-07-05 12:16:46 -03:00
Cody Henthorne
fb4f41b996 Extend style to inserted text via paste or auto-correct regardless of content. 2023-06-30 14:05:35 -04:00
Cody Henthorne
11aac76fb6 Fix spoiler rendering in quote views. 2023-06-30 13:46:17 -04:00
Nicholas
98424f6cbb Bump version to 6.25.2 2023-06-30 13:36:03 -04:00
Nicholas
1cf7c59af9 Updated language translations. 2023-06-30 13:33:51 -04:00
Clark
13470fb0c3 Increase FCM push websocket timeout. 2023-06-30 12:41:00 -04:00
Nicholas Tinsley
3aa0fd1937 Reset continue button state when dismissing registration number confirmation dialog. 2023-06-30 12:23:47 -04:00
Clark
9e6f2336d1 Add push websocket fetch stats. 2023-06-30 11:07:05 -04:00
Nicholas Tinsley
8b8d62f598 Only close AttachmentCipher streams if using incremental MAC. 2023-06-30 11:06:51 -04:00
Nicholas
4572ae5886 Bump version to 6.25.1 2023-06-29 18:42:29 -04:00
Nicholas
0802d4beb4 Updated language translations. 2023-06-29 18:40:18 -04:00
Nicholas
9361aa700a Transcode video files in a streamable format. 2023-06-29 18:30:29 -04:00
Greyson Parrelli
be5cad1cec Add support for Emoji v15.0 2023-06-29 15:55:49 -04:00
Greyson Parrelli
fe20de2995 Improve logging around edit and sync messages. 2023-06-29 15:55:12 -04:00
Clark
8714e4298e Specify scale type for glide thumbnails. 2023-06-29 15:32:04 -04:00
Alex Hart
1baebe7475 Remove read log line from AvatarProvider. 2023-06-29 13:17:05 -03:00
Nicholas
6686ae43f3 Bump version to 6.25.0 2023-06-28 17:24:23 -04:00
Nicholas
cc2c0e9561 Updated language translations. 2023-06-28 17:21:07 -04:00
Nicholas
34d252a4bd Add incremental digests to attachment sending. 2023-06-28 17:13:15 -04:00
Cody Henthorne
025411c9fb Attempt to fix crash on call hangup. 2023-06-28 17:13:15 -04:00
Nicholas Tinsley
4edb66d2b9 Fix SimpleProgressDialog. 2023-06-28 17:13:15 -04:00
Alex Hart
a17033dff4 Add ContentProvider for user avatars. 2023-06-28 17:13:15 -04:00
Cody Henthorne
04a5e56da7 Add mentions support to CFv2. 2023-06-28 17:13:15 -04:00
Bernie Dolan
0e6a3dd408 Fix MobileCoin test net config. 2023-06-28 17:13:15 -04:00
Nicholas Tinsley
47f48a6a8c Put backup enable dialog into ScrollView for smaller screens.
Addresses #13033.
2023-06-28 17:13:15 -04:00
Cody Henthorne
2ef7fabade Add inline emoji search to CFv2. 2023-06-28 17:13:15 -04:00
Alex Hart
5c2b475c01 Add randomized testing for ConversationItem. 2023-06-28 17:13:15 -04:00
Clark
559f4bc0d3 Rotate edit message feature flag. 2023-06-28 17:13:15 -04:00
Clark
1357a4816b Update edit history dialog to new style. 2023-06-28 17:13:15 -04:00
Nicholas Tinsley
d1b8a56c0f Revert "Add more logging around Bubble eligibility."
This purely logs the state without affecting the return logic.
2023-06-28 17:13:15 -04:00
Cody Henthorne
7ea38298ea Fix gif playback in CFv2. 2023-06-28 17:13:15 -04:00
Clark
c08f1355db Refresh isConnectionNecessary on network block changes. 2023-06-28 17:13:15 -04:00
Cody Henthorne
b6589637fa Add emoji search to CFv2. 2023-06-28 17:13:15 -04:00
Alex Hart
2ee2d2883a Disallow reacting to pending or failed messages. 2023-06-28 17:13:15 -04:00
Alex Hart
029c8ba917 Fix text story keyboard in text stories. 2023-06-28 17:13:15 -04:00
Alex Hart
60e1ee21ed Prevent ANR from large call logs. 2023-06-28 17:13:15 -04:00
Alex Hart
a96e9158c4 Change call notification flag to UPDATE_CURRENT and change request code. 2023-06-28 17:13:15 -04:00
Alex Hart
e47765d7d5 Add logging to WebRtcActivity Intent. 2023-06-28 17:13:15 -04:00
Clark Chen
e807435c8b Capitalize edited string. 2023-06-28 17:13:15 -04:00
Sgn-32
14b41a93e2 Fix Do not animate spoilers if system animations are disabled.
Closes #13016
2023-06-28 17:13:15 -04:00
Yuichi Araki
f51fb9da29 Report shortcut usage when a message is sent to a group.
Fixes #13029
Close #13030
2023-06-28 17:13:15 -04:00
Sebastian Scheibner
df3ca3d3cc Fix duplicate kyber pre key id in registration
The `PreKeyUtil.generateKyberPreKey` method doesn't update the `nextKyberPreKeyId` in the metadataStore,
so the two `metadataStore.getNextKyberPreKeyId()` calls in this method return the same id.
The first oneTimeKyberPreKey will have the same id as the lastResortKyberPreKey and overwrite it in the database.

Closes #13021
2023-06-28 17:13:15 -04:00
Clark
7786956b11 Fix conversation date header weirdness with edited messages. 2023-06-28 17:13:15 -04:00
Cody Henthorne
c17d62aeab Update ktlint and gradle plugin. 2023-06-28 17:13:15 -04:00
Cody Henthorne
65255121de Add media keyboard support in CFv2. 2023-06-28 17:13:15 -04:00
Greyson Parrelli
b042945fef Add check for internal users around group lock ordering. 2023-06-28 17:13:15 -04:00
Greyson Parrelli
9d979217fa Include 'dont keep activities' setting in debuglog. 2023-06-28 17:13:15 -04:00
Greyson Parrelli
c177de2ec3 Add CDSI and SVR2 to static IP list. 2023-06-26 15:09:39 -04:00
Greyson Parrelli
b4fd57d900 Fix possible NPEs with megaphone container. 2023-06-26 15:09:36 -04:00
Nicholas Tinsley
92339dfdcf Add one more logging statement to the change number flow. 2023-06-26 15:09:36 -04:00
Nicholas
8ae115028e Update PIN switch keyboard button to be more straightforward.
Addresses #12866.
2023-06-26 15:09:36 -04:00
Greyson Parrelli
2dd0221680 Fix issue where FTS recovery path didn't work during migrations.
There was a recursive getDatabase() call because it was happening during
a migration. Solution was to re-use the DB instance we already had.
2023-06-26 15:09:36 -04:00
Greyson Parrelli
1a1923c6c0 Fix docker build by using our own mirror.
snapshot.debian.org is super flaky and unreliable. At this point it's
easier to host our own mirror snapshot.
2023-06-26 15:09:36 -04:00
Cody Henthorne
5801ad4bdb Update FCM to 23.1.2 2023-06-26 15:09:36 -04:00
Cody Henthorne
388f2971e9 Allow libsignal-service to build with JDK17. 2023-06-26 15:09:36 -04:00
Greyson Parrelli
14c3a36ec0 Improve logging for GV2 validation error. 2023-06-26 15:09:36 -04:00
Alex Hart
1ad338ce31 Turn DelveryStatusView into a custom AppCompatImageView. 2023-06-26 15:09:36 -04:00
Greyson Parrelli
71981e8a27 Fix dockerfile for reproducible builds. 2023-06-26 15:09:36 -04:00
Nicholas
5cb10cd054 Improve voice note Bluetooth state handling. 2023-06-26 15:09:36 -04:00
Rashad Sookram
ecf576e9b9 Don't localize audio output in logs. 2023-06-26 15:09:36 -04:00
Alex Hart
09d17659b9 Add media send support to CFV2. 2023-06-26 15:09:36 -04:00
Cody Henthorne
53673be5cb Update AGP to 8.0
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2023-06-26 15:09:36 -04:00
Alex Hart
ed4a1d6ddd Use delete instead of delete for me in call log dialog. 2023-06-26 15:09:36 -04:00
Alex Hart
e3044b8b85 Add preview treatment for call links. 2023-06-26 15:09:36 -04:00
Alex Hart
4ce05a064c Add CFV2 Sticker Suggestions. 2023-06-26 15:09:36 -04:00
Alex Hart
2fbcc23451 Add LinkPreview support to CFV2. 2023-06-26 15:09:36 -04:00
Alex Hart
3bdffed8c9 Add call link scrubbing for logs. 2023-06-26 15:09:36 -04:00
Nicholas
6cc8e87d46 Bump version to 6.24.4 2023-06-26 14:33:40 -04:00
Nicholas
7d8f549d97 Updated baseline profile. 2023-06-26 14:31:28 -04:00
Nicholas
fb2ef265bd Updated language translations. 2023-06-26 14:16:45 -04:00
Nicholas
96e8256781 Fix TestUser constructor. 2023-06-26 14:10:13 -04:00
Cody Henthorne
dd40517f12 Fix crash after hanging up call. 2023-06-26 14:10:13 -04:00
Cody Henthorne
6c95b766d6 Fix answer audio call and video starting bug. 2023-06-26 14:10:13 -04:00
Greyson Parrelli
feffdcb71e Bump version to 6.24.3 2023-06-22 18:53:58 -04:00
Greyson Parrelli
f33e2c49ca Updated language translations. 2023-06-22 18:53:29 -04:00
Clark
1037acd4a2 Load original message and use it for timestamps in Conversation. 2023-06-22 18:42:56 -04:00
Clark
68042fc755 No longer call startForeground in onCreate. 2023-06-22 14:09:23 -04:00
Alex Hart
1bdc77affe Resolve issue with incoming video call state. 2023-06-22 14:52:10 -03:00
Nicholas Tinsley
88f50da4fb Discard voice note recording on error. 2023-06-22 13:32:27 -04:00
Cody Henthorne
bc1fbd9b6c Fix spoilers not animating after leaving and returning to conversation.
Fixes #13015
2023-06-22 11:57:59 -04:00
Cody Henthorne
c1b7b7c95e Fix styling lost in draft state bug. 2023-06-22 11:16:27 -04:00
Greyson Parrelli
b0688eed5c Bump version to 6.24.2 2023-06-21 20:34:12 -04:00
Greyson Parrelli
e06ed03d33 Updated language translations. 2023-06-21 20:34:12 -04:00
Clark
e2a79394ab Always background legacy FCM fallback. 2023-06-21 20:34:12 -04:00
Clark
5db770ca44 Use original message instead of edit message when checking if we can edit. 2023-06-21 17:52:33 -04:00
Cody Henthorne
df8aaa2005 Provide better 'why' when an attachment is not auto-downloaded. 2023-06-21 16:18:16 -04:00
Cody Henthorne
882748f080 Allow formatting text from overflow menu. 2023-06-21 16:16:16 -04:00
Cody Henthorne
15035f4eb3 Fix rendering when editing a message with spoilers. 2023-06-21 13:08:00 -04:00
Cody Henthorne
1d0a87f52a Add ability to clear or toggle formatting. 2023-06-21 13:05:46 -04:00
Clark
59b2cc5f79 Fix edit message date header showing wrong date. 2023-06-21 12:44:22 -04:00
Nicholas Tinsley
d6758fc264 Store PIN keyboard type in file backup. 2023-06-21 11:53:01 -04:00
Greyson Parrelli
36418bec59 Ensure SqlCipherDeletingErrorHandler runs delete. 2023-06-21 10:24:12 -04:00
Cody Henthorne
07bd8b2fa3 Fix NPE in spoiler renderer. 2023-06-21 10:10:01 -04:00
Greyson Parrelli
d989d02af9 Bump version to 6.24.1 2023-06-20 14:26:35 -04:00
Greyson Parrelli
503ce13122 Updated language translations. 2023-06-20 14:24:16 -04:00
Alex Hart
8f77321adb Dispose notification disposable when stopping service. 2023-06-20 12:07:11 -03:00
Clark
60cdcea791 Revert "Call startForeground onCreate for generic foreground service. " 2023-06-20 10:53:28 -04:00
Clark
86cd4c5c30 Fix remote delete for edit messages. 2023-06-20 10:29:47 -04:00
Nicholas Tinsley
62d5f61a0b Allow verification with PIN rather than SMS when restoring from backup. 2023-06-16 16:31:46 -04:00
Clark
25860867bb Use correct receive timestamp for edit message handling. 2023-06-16 16:30:49 -04:00
Greyson Parrelli
272860f071 Do not animate spoilers if system animations are disabled. 2023-06-16 15:51:24 -04:00
Nicholas
767cfbc717 Fix atomic registrations when not using session ID. 2023-06-16 15:38:16 -04:00
Cody Henthorne
55af6ca84e Bump version to 6.24.0 2023-06-15 15:44:35 -04:00
Cody Henthorne
88933ae051 Updated baseline profile. 2023-06-15 15:36:09 -04:00
Cody Henthorne
4781beebee Updated language translations. 2023-06-15 15:36:09 -04:00
Clark Chen
0049c74323 Fix baseline profile generation benchmark. 2023-06-15 15:36:09 -04:00
Alex Hart
361727cec6 Add resend handler to CFV2. 2023-06-15 15:36:09 -04:00
Alex Hart
1b95177e0e Add handleViewPaymentDetails to CFV2. 2023-06-15 15:36:09 -04:00
Alex Hart
8d34c54de2 Add multiselect action support to CFV2. 2023-06-15 15:36:09 -04:00
Alex Hart
2fa0eba3db Add search call-through in CFV2 override. 2023-06-15 15:36:09 -04:00
Cody Henthorne
6cc41e95c6 Remove edit message receive feature flag. 2023-06-15 15:36:09 -04:00
Cody Henthorne
b1523f5b91 Update text formatting feature flag. 2023-06-15 15:36:09 -04:00
Nicholas
d16002546d Create account in single network request. 2023-06-15 15:36:09 -04:00
Clark
186a93f5d1 Use separate PNI key distribution endpoint instead of change number. 2023-06-15 15:36:09 -04:00
Alex Hart
3d4875bcfe Add sticker suggestion send support to CFV2. 2023-06-15 15:36:09 -04:00
Nicholas
441e30971a Add more logging around Bubble eligibility.
To help diagnose #12036.
2023-06-15 15:36:08 -04:00
Alex Hart
ff115c2349 Add voice recording to CFV2. 2023-06-15 15:36:08 -04:00
Clark
f23e5bdb44 Prepare edit message for beta run. 2023-06-15 15:36:08 -04:00
Jim Gustafson
d0a232d86a Update to RingRTC v2.28.1 2023-06-15 15:36:08 -04:00
Alex Hart
9fef8386e6 Fix initial call state when starting from action. 2023-06-15 15:36:08 -04:00
Alex Hart
b1680ba5c6 Add new call notification strings. 2023-06-15 15:36:08 -04:00
Alex Hart
6cd59daf0a Fix several issues with call notifications. 2023-06-15 15:36:08 -04:00
Greyson Parrelli
38f2b39ac4 Add common interface over SVR implementations. 2023-06-15 15:36:08 -04:00
Alex Hart
51222738df Remove avatar color from CallLink table. 2023-06-15 15:36:08 -04:00
Clark
b9835584d8 Add sharing for PNP usernames badge. 2023-06-15 13:32:00 -04:00
Alex Hart
cff01021c2 Ensure call links only ever have one call event associated with them. 2023-06-15 13:32:00 -04:00
Alex Hart
d19b8a125c Do not enable ringing for call links. 2023-06-15 13:32:00 -04:00
Alex Hart
4caaa0033b Re-enable call delete sync events. 2023-06-15 13:32:00 -04:00
Alex Hart
f3a0a059ea Add search and arbitrary jump support to CFV2. 2023-06-15 13:32:00 -04:00
Alex Hart
290b0fe46f Ensure owned call links are revoked on delete. 2023-06-15 13:32:00 -04:00
g1a55er
03a212eee4 Reschedule job if background web socket message retrieval fails.
Closes #12971
2023-06-15 13:31:59 -04:00
Alex Hart
f3b629bc06 Add reaction support to CFV2. 2023-06-15 13:31:59 -04:00
Alex Hart
b9e002f7b1 Update call links parsing. 2023-06-15 13:31:59 -04:00
Alex Hart
c90779beea Fix jump position for quotes. 2023-06-15 13:31:59 -04:00
Alex Hart
bc8c8a049f Add onViewsRevealed implementation. 2023-06-15 13:31:59 -04:00
Alex Hart
1d9dc66265 Update several androidx dependencies.
Navigation to 1.6.0
Fragment to 1.6.0
Compose BOM to 2023.05.01
Lifecycle to 2.6.1
Activity to 1.7.2
2023-06-15 13:31:59 -04:00
Alex Hart
886c149c3f Add in-call info sheet for call links. 2023-06-15 13:31:59 -04:00
Clark
369ca189d3 Disable stickers and gifs when editing message. 2023-06-15 13:31:59 -04:00
Cody Henthorne
02c4bbe816 Add date headers to CFv2. 2023-06-15 13:31:59 -04:00
Clark
523c9f6576 Add workflow for monitoring APK size changes. 2023-06-15 13:31:59 -04:00
Cody Henthorne
c259430b09 Bump version to 6.23.5 2023-06-15 13:04:42 -04:00
Cody Henthorne
fc94b90a03 Revert "Adopt new APIs for network connectivity check."
This reverts commit de4c6ab7b7.
2023-06-15 12:57:35 -04:00
Cody Henthorne
6af521130d Revert "Fix connectivity over VPN on older API versions."
This reverts commit 7e24252447.
2023-06-15 12:57:14 -04:00
Cody Henthorne
9c001e4f35 Bump version to 6.23.4 2023-06-15 11:30:05 -04:00
Cody Henthorne
9e8dee36a6 Updated baseline profile. 2023-06-15 11:25:25 -04:00
Cody Henthorne
26b17d8a3c Updated language translations. 2023-06-15 11:23:11 -04:00
Cody Henthorne
1c5e2e3359 Fix linkify for valid URLs with ... in the path. 2023-06-15 11:20:22 -04:00
Clark
0437d37f23 Fix pending retry receipt cache deadlock. 2023-06-15 11:20:22 -04:00
Cody Henthorne
ec6b1a44de Add text formatting support to release notes channel. 2023-06-15 11:20:22 -04:00
Clark
08c661bb14 Fix foreground service start/stop race. 2023-06-14 14:42:08 -04:00
Cody Henthorne
62f62d89c5 Bump version to 6.23.3 2023-06-14 12:40:51 -04:00
Cody Henthorne
37dd8b40b2 Updated baseline profile. 2023-06-14 12:28:43 -04:00
Cody Henthorne
15825f6c3f Updated language translations. 2023-06-14 12:26:09 -04:00
Clark
ad196bf03c Fix stopForegroundTask crash. 2023-06-14 10:54:21 -04:00
Cody Henthorne
332c4ca26e Improve spoiler performance by reducing number of particles and frame rate. 2023-06-14 10:36:43 -04:00
Cody Henthorne
305edf1928 Fix SQL crash in backup restore by preventing job from running until restore complete. 2023-06-14 10:28:34 -04:00
Nicholas Tinsley
9c0c25ef99 Detect URL patterns that will crash OkHttp.
Addresses #12998.
2023-06-13 15:48:24 -04:00
Cody Henthorne
458dae227f Bump version to 6.23.2 2023-06-13 11:10:20 -04:00
Cody Henthorne
b1ba9fd54f Updated baseline profile. 2023-06-13 10:55:53 -04:00
Cody Henthorne
dd42b5b851 Updated language translations. 2023-06-13 10:51:44 -04:00
Alex Hart
d8e3edc729 Fix incoming call intent. 2023-06-13 10:47:40 -04:00
Cody Henthorne
36aa8623da Improve spoiler performance by stopping animation when backgrounded. 2023-06-12 13:11:15 -04:00
Cody Henthorne
7e24252447 Fix connectivity over VPN on older API versions. 2023-06-12 12:39:18 -04:00
Cody Henthorne
164ce06177 Fix style growing when applying other styles. 2023-06-12 09:01:21 -04:00
Cody Henthorne
c9d298c447 Bump version to 6.23.1 2023-06-09 16:28:20 -04:00
Cody Henthorne
5129613ce8 Updated baseline profile. 2023-06-09 16:22:01 -04:00
Cody Henthorne
b985ace7ed Updated language translations. 2023-06-09 16:19:06 -04:00
Cody Henthorne
d28afac973 Fix baseline benchmark. 2023-06-09 16:15:00 -04:00
Clark
fd9b5ff7c4 Drop failed processed incoming messages. 2023-06-09 15:48:01 -04:00
Cody Henthorne
75580bea27 Update SQLCipher to 4.5.4-S2 2023-06-09 15:40:12 -04:00
Cody Henthorne
db7056c53b Fix bug when processing duplicate text messages within the same batch. 2023-06-09 15:39:13 -04:00
Cody Henthorne
b55181ffe6 Fix localization issue with group call start strings. 2023-06-09 12:30:30 -04:00
Nicholas Tinsley
81149e5aa8 Hide call audio devices that are not of known type. 2023-06-09 10:18:47 -04:00
Cody Henthorne
3a341eee19 Fix revealing spoilers in text stories. 2023-06-09 10:06:35 -04:00
Cody Henthorne
e19c7efbfe Bump version to 6.23.0 2023-06-07 16:01:13 -04:00
Cody Henthorne
7e7c68321b Updated baseline profile. 2023-06-07 15:17:03 -04:00
Cody Henthorne
9fa3f54c7c Updated language translations. 2023-06-07 15:14:47 -04:00
Cody Henthorne
3ff273f1f2 Ignore tests that fail when run on JDK17. 2023-06-07 15:11:26 -04:00
Cody Henthorne
e607b1962c Cycle text formatting send remote config. 2023-06-07 14:54:52 -04:00
Nicholas
2c4c6bf87c Allow registration with landlines. 2023-06-07 14:46:01 -04:00
Clark
bf048e2a75 Dont block main thread when we try to stop FCM fetch service. 2023-06-07 14:12:35 -04:00
Alex Hart
b0c4bb04e7 Hide clear history action menu item when total count is zero. 2023-06-07 14:25:54 -03:00
Cody Henthorne
7e0e6c2786 Fix pool limits and y-translation issues with CFv2 recycler view. 2023-06-07 12:51:08 -04:00
Alex Hart
d6a03df087 Fix db contention when inserting group ring updates. 2023-06-07 13:28:37 -03:00
Nicholas Tinsley
cfc89d2a74 Gracefully handle invalid audio device selection during calls. 2023-06-07 11:24:22 -04:00
Clark
c491c9dc8c Fix chosen location not being sent sometimes. 2023-06-06 12:47:16 -04:00
Cody Henthorne
eae066b3a2 Fix styling issues when covering mentions. 2023-06-06 12:47:16 -04:00
Clark
71aa17bad6 Verify number of backup frames written is read back. 2023-06-06 12:47:16 -04:00
Alex Hart
93df01e266 CallLink treatment for ConversationItem. 2023-06-06 12:47:16 -04:00
Alex Hart
8f96abb41e Update TAG usage throughout call link processors. 2023-06-06 12:47:16 -04:00
Nicholas Tinsley
1457a6fe16 Deduplicate audio output choices. 2023-06-06 12:47:16 -04:00
Alex Hart
290c107698 Implement simple avatar color picking algorithm to align with iOS. 2023-06-06 12:47:16 -04:00
Alex Hart
bf7aaddbf9 Hook in Call Links integration via factory. 2023-06-06 12:47:16 -04:00
Clark
59435e49c8 Call startForeground onCreate for generic foreground service. 2023-06-06 12:47:16 -04:00
Clark
c3499e538e Fix wallpaper scaling on orientation change. 2023-06-06 12:47:16 -04:00
Cody Henthorne
1d41b1c5a3 Remove a couple old dependencies. 2023-06-06 12:47:16 -04:00
Cody Henthorne
e303570b2f Update to libsignal 0.26.0 2023-06-06 12:47:16 -04:00
Alex Hart
62940893f0 Add peek and join capabilities to call links implementation. 2023-06-06 12:47:16 -04:00
Alex Hart
f8434bede5 Add clear all menu action to calls tab. 2023-06-06 12:47:16 -04:00
Jim Gustafson
c08e108fc3 Update to RingRTC v2.28.0
Co-authored-by: Jordan Rose <jrose@signal.org>
2023-06-06 12:47:16 -04:00
Alex Hart
cd9a160cae Fix pip offset. 2023-06-06 12:47:16 -04:00
Cody Henthorne
bba8b8be56 Fix external share when it contains an image and text. 2023-06-06 12:47:16 -04:00
Bernie Dolan
f56b5d58c6 Update MobileCoin payments and enclave values. 2023-06-06 12:47:16 -04:00
Cody Henthorne
ac4b0ed606 Improve auto-leave group behavior. 2023-06-06 12:47:16 -04:00
Clark Chen
d3e71185e6 Fix story ring not updating on recipient screen. 2023-06-06 12:47:16 -04:00
Alex Hart
b4f2cd9ff4 Add updated phone svg icon. 2023-06-06 12:47:16 -04:00
Alex Hart
fd8d305899 Add voice note draft playback impl to cfv2. 2023-06-06 12:47:16 -04:00
Alex Hart
bde7ae944a Add draft handling in toggle button update method. 2023-06-06 12:47:16 -04:00
Alex Hart
99f83e5dc9 Add handleDial implementation in CFV2. 2023-06-06 12:47:16 -04:00
Cody Henthorne
693aef5c04 Add partial share and draft support to CFv2. 2023-06-06 12:47:16 -04:00
Alex Hart
b9ae537706 Add onItemClick handling in CFV2. 2023-06-06 12:47:16 -04:00
Alex Hart
e41dd6d39d CFV2 Add edit message support. 2023-06-06 12:47:16 -04:00
Alex Hart
5d546f46e4 CFV2 Add reply to message support. 2023-06-06 12:47:16 -04:00
Clark
2bef5653b4 Lower priority for apk update notification. 2023-06-06 12:47:16 -04:00
Clark
63d8549865 Light refactor to thread update. 2023-06-06 12:47:16 -04:00
Clark Chen
f6bac2f476 Fix trash can not appearing when editing group photo. 2023-06-06 12:47:16 -04:00
Alex Hart
0dd51856d3 CFV2 - Implement add to home screen. 2023-06-06 12:47:16 -04:00
Alex Hart
be01f2b511 CFV2 -- Add to Contacts / Mute Conversation. 2023-06-06 12:47:16 -04:00
Alex Hart
045d2cf42f Copy over several more action handlers for CFV2. 2023-06-06 12:47:16 -04:00
Cody Henthorne
64ddd982fe Add review banner to CFv2. 2023-06-06 12:47:16 -04:00
Alex Hart
b785b3f887 CFV2 Save to Disk / Copy Text Content. 2023-06-06 12:47:16 -04:00
Alex Hart
399421e20e CFV2 Implement delete, forward, view once handling. 2023-06-06 12:47:16 -04:00
Cody Henthorne
a656d65d1d Move render split to match CFv2 for fairer comparisons. 2023-06-06 12:47:16 -04:00
Nicholas Tinsley
291a5d57c4 Replace em dash in javadoc with ASCII-safe hyphen. 2023-06-06 12:47:16 -04:00
Nicholas Tinsley
a9a91e3162 Address a bunch of compiler warnings.
None of these should change any behavior, they're all annotations and stuff.
2023-06-06 12:47:16 -04:00
g1a55er
de4c6ab7b7 Adopt new APIs for network connectivity check.
Addresses #12941
2023-06-06 12:47:16 -04:00
Nicholas Tinsley
7ea9fc0c3b Update AlertDialogs to MaterialAlertDialogs.
Addresses #12949.
2023-06-06 12:47:16 -04:00
Greyson Parrelli
1965d5879f Log message procesing speed at 2 decimal places. 2023-06-06 12:47:16 -04:00
Greyson Parrelli
b2b907a86a Add additional validation for group messages. 2023-06-06 12:47:15 -04:00
Cody Henthorne
e565de0724 Add verified updates and unverified banner. 2023-06-06 12:47:15 -04:00
Greyson Parrelli
7318e676f7 Add an internal feature to search your contacts by ID/ACI/PNI. 2023-06-06 12:47:15 -04:00
Greyson Parrelli
7c28d8ad51 Fix possible NPE when opening a story. 2023-06-06 12:47:15 -04:00
Greyson Parrelli
3e21fb77c7 Skip sends to users with prekey failures. 2023-06-06 12:47:15 -04:00
Cody Henthorne
6b91e525db Add Reminders and Conversation Banner to CFv2. 2023-06-06 12:47:15 -04:00
Greyson Parrelli
0aca03a919 Add kyber support for change number. 2023-06-06 12:47:15 -04:00
Greyson Parrelli
e2ef8e2ef9 Add support for kyber prekeys. 2023-06-06 12:47:15 -04:00
Greyson Parrelli
15c248184f Add two-phase commit support for SVR2. 2023-06-06 12:47:15 -04:00
681 changed files with 34310 additions and 23947 deletions

View File

@@ -1,5 +1,8 @@
root = true
[*.kt]
[*.{kt,kts}]
indent_size = 2
ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_allow_trailing_comma = false
ktlint_code_style = intellij_idea
twitter_compose_allowed_composition_locals=LocalExtendedColors

View File

@@ -19,11 +19,11 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: set up JDK 11
- name: set up JDK 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 11
java-version: 17
cache: gradle
- name: Validate Gradle Wrapper

82
.github/workflows/diffuse.yml vendored Normal file
View File

@@ -0,0 +1,82 @@
name: APK Diff
on:
pull_request:
permissions:
contents: read # to fetch code (actions/checkout)
pull-requests: write # to comment on PR
jobs:
assemble-base:
if: ${{ github.repository != 'signalapp/Signal-Android' }}
runs-on: ubuntu-latest-8-cores
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: set up JDK 17
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17
cache: gradle
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Cache base apk
id: cache-base
uses: actions/cache@v3
with:
path: diffuse-base.apk
key: diffuse-${{ github.event.pull_request.base.sha }}
- name: Build with Gradle
if: steps.cache-base.outputs.cache-hit != 'true'
run: ./gradlew assemblePlayProdRelease --parallel
- name: Copy base apk
if: steps.cache-base.outputs.cache-hit != 'true'
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-base.apk
- uses: actions/checkout@v3
with:
clean: 'false'
- name: Build with Gradle
run: ./gradlew assemblePlayProdRelease --parallel
- name: Copy PR apk
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk
- id: diffuse
uses: usefulness/diffuse-action@v1
with:
old-file-path: diffuse-base.apk
new-file-path: diffuse-new.apk
- uses: peter-evans/find-comment@v2
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: Diffuse output
- uses: peter-evans/create-or-update-comment@v3
with:
body: |
Diffuse output:
${{ steps.diffuse.outputs.diff-gh-comment }}
edit-mode: replace
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/upload-artifact@v3
with:
name: diffuse-output
path: ${{ steps.diffuse.outputs.diff-file }}

View File

@@ -212,5 +212,12 @@
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="2" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>

View File

@@ -42,19 +42,18 @@ wire {
}
ktlint {
// Use a newer version to resolve https://github.com/JLLeitschuh/ktlint-gradle/issues/507
version = "0.47.1"
version = "0.49.1"
}
def canonicalVersionCode = 1272
def canonicalVersionName = "6.22.7"
def canonicalVersionCode = 1289
def canonicalVersionName = "6.25.5"
def postFixSize = 100
def abiPostFix = ['universal' : 10,
'armeabi-v7a' : 11,
'arm64-v8a' : 12,
'x86' : 13,
'x86_64' : 14]
def abiPostFix = ['universal' : 0,
'armeabi-v7a' : 1,
'arm64-v8a' : 2,
'x86' : 3,
'x86_64' : 4]
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
@@ -159,7 +158,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = '1.3.2'
kotlinCompilerExtensionVersion = '1.4.4'
}
defaultConfig {
@@ -185,7 +184,7 @@ android {
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\""
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\""
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
buildConfigField "String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\""
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\", \"Development\"}"
@@ -199,9 +198,11 @@ android {
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_CDSI_IPS", cdsi_ips
buildConfigField "String[]", "SIGNAL_SVR2_IPS", svr2_ips
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\""
buildConfigField "String", "SVR2_MRENCLAVE", "\"dc9fd472a5a9c871a3c7f76f1af60aa9c1f314abf2e8d1e0c4ba25c8aaa2848c\""
buildConfigField "String", "SVR2_MRENCLAVE", "\"6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"e18376436159cda3ad7a45d9320e382e4a497f26b0dca34d8eab0bd0139483b5\", " +
"\"3a485adb56e2058ef7737764c738c4069dd62bc457637eafb6bbce1ce29ddb89\", " +
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
@@ -378,6 +379,8 @@ android {
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\""
buildConfigField "String", "SVR2_MRENCLAVE", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\""
buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"39963b736823d5780be96ab174869a9499d56d66497aa8f9b2244f777ebc366b\", " +
"\"9dbc6855c198e04f21b5cc35df839fdcd51b53658454dfa3f817afefaffc95ef\", " +
"\"45627094b2ea4a66f4cf0b182858a8dcf4b8479122c3820fe7fd0551a6d4cf5c\")"
@@ -445,6 +448,14 @@ android {
variant.setIgnore(true)
}
}
android.buildTypes.each {
if (it.name != 'release') {
sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/debug/java"
} else {
sourceSets.findByName(it.name).java.srcDirs += "$projectDir/src/release/java"
}
}
}
dependencies {
@@ -468,6 +479,7 @@ dependencies {
implementation libs.androidx.gridlayout
implementation libs.androidx.exifinterface
implementation libs.androidx.compose.rxjava3
implementation libs.androidx.compose.runtime.livedata
implementation libs.androidx.constraintlayout
implementation libs.androidx.multidex
implementation libs.androidx.navigation.fragment.ktx
@@ -529,13 +541,11 @@ dependencies {
implementation libs.leolin.shortcutbadger
implementation libs.emilsjolander.stickylistheaders
implementation libs.jpardogo.materialtabstrip
implementation libs.apache.httpclient.android
implementation libs.glide.glide
implementation libs.roundedimageview
implementation libs.materialish.progress
implementation libs.greenrobot.eventbus
implementation libs.waitingdots
implementation libs.google.zxing.android.integration
implementation libs.google.zxing.core
implementation libs.google.flexbox
@@ -578,9 +588,8 @@ dependencies {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
testImplementation testLibs.robolectric.shadows.multidex
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) {
force = true
}
testImplementation (testLibs.bouncycastle.bcprov.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
testImplementation (testLibs.bouncycastle.bcpkix.jdk15on) { version { strictly "1.70" } } // Used by roboelectric
testImplementation testLibs.hamcrest.hamcrest
testImplementation testLibs.mockk

View File

@@ -11,4 +11,11 @@
# Protobuf lite
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
-keep class androidx.window.** { *; }
-keep class androidx.window.** { *; }
# AGP generated dont warns
-dontwarn com.android.org.conscrypt.SSLParametersImpl
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn sun.net.spi.nameservice.NameService
-dontwarn sun.net.spi.nameservice.NameServiceDescriptor

View File

@@ -142,6 +142,7 @@ class ConversationItemPreviewer {
1024,
1024,
Optional.empty(),
Optional.empty(),
Optional.of("/not-there.jpg"),
false,
false,

View File

@@ -47,6 +47,7 @@ import org.whispersystems.signalservice.api.push.ServiceId
import java.util.Optional
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class RecipientTableTest_getAndPossiblyMerge {

View File

@@ -21,8 +21,8 @@ import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.testing.parsedRequestBody
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
import org.whispersystems.signalservice.internal.push.OneTimePreKeyCounts
import org.whispersystems.signalservice.internal.push.PreKeyState
import org.whispersystems.signalservice.internal.push.PreKeyStatus
@RunWith(AndroidJUnit4::class)
class PreKeysSyncJobTest {
@@ -106,8 +106,8 @@ class PreKeysSyncJobTest {
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(100)) }
Get("/v2/keys?identity=aci") { MockResponse().success(OneTimePreKeyCounts(100, 100)) },
Get("/v2/keys?identity=pni") { MockResponse().success(OneTimePreKeyCounts(100, 100)) }
)
// WHEN
@@ -133,7 +133,7 @@ class PreKeysSyncJobTest {
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(100)) },
Get("/v2/keys?identity=aci") { MockResponse().success(OneTimePreKeyCounts(100, 100)) },
Put("/v2/keys/signed?identity=pni") { MockResponse().success() }
)
@@ -157,15 +157,15 @@ class PreKeysSyncJobTest {
val currentAciKeyId = aciPreKeyMeta.activeSignedPreKeyId
val currentPniKeyId = pniPreKeyMeta.activeSignedPreKeyId
val currentNextAciPreKeyId = aciPreKeyMeta.nextOneTimePreKeyId
val currentNextPniPreKeyId = pniPreKeyMeta.nextOneTimePreKeyId
val currentNextAciPreKeyId = aciPreKeyMeta.nextEcOneTimePreKeyId
val currentNextPniPreKeyId = pniPreKeyMeta.nextEcOneTimePreKeyId
lateinit var aciPreKeyStateRequest: PreKeyState
lateinit var pniPreKeyStateRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v2/keys?identity=aci") { MockResponse().success(PreKeyStatus(5)) },
Get("/v2/keys?identity=pni") { MockResponse().success(PreKeyStatus(5)) },
Get("/v2/keys?identity=aci") { MockResponse().success(OneTimePreKeyCounts(5, 5)) },
Get("/v2/keys?identity=pni") { MockResponse().success(OneTimePreKeyCounts(5, 5)) },
Put("/v2/keys/?identity=aci") { r ->
aciPreKeyStateRequest = r.parsedRequestBody()
MockResponse().success()
@@ -184,8 +184,8 @@ class PreKeysSyncJobTest {
aciPreKeyMeta.activeSignedPreKeyId assertIsNot currentAciKeyId
pniPreKeyMeta.activeSignedPreKeyId assertIsNot currentPniKeyId
aciPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextAciPreKeyId
pniPreKeyMeta.nextOneTimePreKeyId assertIsNot currentNextPniPreKeyId
aciPreKeyMeta.nextEcOneTimePreKeyId assertIsNot currentNextAciPreKeyId
pniPreKeyMeta.nextEcOneTimePreKeyId assertIsNot currentNextPniPreKeyId
ApplicationDependencies.getProtocolStore().aci().identityKeyPair.publicKey.let { aciIdentityKey ->
aciPreKeyStateRequest.identityKey assertIs aciIdentityKey

View File

@@ -32,6 +32,7 @@ import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
import java.lang.UnsupportedOperationException
import java.util.Optional
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
@@ -168,6 +169,10 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
override fun getSenderKeySharedWith(distributionId: DistributionId?): MutableSet<SignalProtocolAddress> = throw UnsupportedOperationException()
override fun markSenderKeySharedWith(distributionId: DistributionId?, addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
override fun clearSenderKeySharedWith(addresses: MutableCollection<SignalProtocolAddress>?) = throw UnsupportedOperationException()
override fun storeLastResortKyberPreKey(kyberPreKeyId: Int, kyberPreKeyRecord: KyberPreKeyRecord) = throw UnsupportedOperationException()
override fun removeKyberPreKey(kyberPreKeyId: Int) = throw UnsupportedOperationException()
override fun loadLastResortKyberPreKeys(): List<KyberPreKeyRecord> = throw UnsupportedOperationException()
override fun isMultiDevice(): Boolean = throw UnsupportedOperationException()
}
}

View File

@@ -32,11 +32,11 @@ import org.thoughtcrime.securesms.registration.VerifyResponse
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.ServiceIdType
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.lang.IllegalArgumentException
import java.util.UUID
/**
@@ -88,6 +88,8 @@ class SignalActivityRule(private val othersCount: Int = 4) : ExternalResource()
password = Util.getSecret(18),
registrationId = registrationRepository.registrationId,
profileKey = registrationRepository.getProfileKey("+15555550101"),
aciPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.ACI),
pniPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.PNI),
fcmToken = null,
pniRegistrationId = registrationRepository.pniRegistrationId,
recoveryPassword = "asdfasdfasdfasdf"

View File

@@ -1,17 +1,14 @@
package org.signal.benchmark
import android.content.Context
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.state.PreKeyRecord
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.account.PreKeyUpload
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceIdType
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import java.io.IOException
import java.util.Optional
@@ -37,7 +34,7 @@ class DummyAccountManagerFactory : AccountManagerFactory() {
}
@Throws(IOException::class)
override fun setPreKeys(serviceIdType: ServiceIdType, identityKey: IdentityKey, signedPreKey: SignedPreKeyRecord, oneTimePreKeys: List<PreKeyRecord>) {
override fun setPreKeys(preKeyUpload: PreKeyUpload) {
}
}
}

View File

@@ -23,12 +23,12 @@ import org.thoughtcrime.securesms.registration.VerifyResponse
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.ServiceIdType
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.util.UUID
object TestUsers {
private var generatedOthers: Int = 0
@@ -50,6 +50,8 @@ object TestUsers {
password = Util.getSecret(18),
registrationId = registrationRepository.registrationId,
profileKey = registrationRepository.getProfileKey("+15555550101"),
aciPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.ACI),
pniPreKeyCollection = RegistrationRepository.generatePreKeysForType(ServiceIdType.PNI),
fcmToken = "fcm-token",
pniRegistrationId = registrationRepository.pniRegistrationId,
recoveryPassword = "asdfasdfasdfasdf"

View File

@@ -0,0 +1,132 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.internal.conversation
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import java.security.SecureRandom
import kotlin.time.Duration.Companion.milliseconds
/**
* Generates random conversation messages via the given set of parameters.
*/
class ConversationElementGenerator {
private val mappingModelCache = mutableMapOf<ConversationElementKey, MappingModel<*>>()
private val random = SecureRandom()
private val wordBank = listOf(
"A",
"Test",
"Message",
"To",
"Display",
"Content",
"In",
"Bubbles",
"User",
"Signal",
"The"
)
fun getMappingModel(key: ConversationElementKey): MappingModel<*> {
val cached = mappingModelCache[key]
if (cached != null) {
return cached
}
val messageModel = generateMessage(key)
mappingModelCache[key] = messageModel
return messageModel
}
private fun getIncomingType(): Long {
return MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT
}
private fun getSentOutgoingType(): Long {
return MessageTypes.BASE_SENT_TYPE or MessageTypes.SECURE_MESSAGE_BIT
}
private fun generateMessage(key: ConversationElementKey): MappingModel<*> {
val messageId = key.requireMessageId()
val now = getNow()
val testMessageWordLength = random.nextInt(40) + 1
val testMessage = (0 until testMessageWordLength).map {
wordBank.random()
}.joinToString(" ")
val isIncoming = random.nextBoolean()
val record = MediaMmsMessageRecord(
messageId,
if (isIncoming) Recipient.UNKNOWN else Recipient.self(),
0,
if (isIncoming) Recipient.self() else Recipient.UNKNOWN,
now,
now,
now,
1,
1,
testMessage,
SlideDeck(),
if (isIncoming) getIncomingType() else getSentOutgoingType(),
emptySet(),
emptySet(),
0,
0,
0,
false,
1,
null,
emptyList(),
emptyList(),
false,
emptyList(),
false,
false,
now,
1,
now,
null,
StoryType.NONE,
null,
null,
null,
null,
-1,
null,
null,
0
)
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
ApplicationDependencies.getApplication(),
record,
Recipient.UNKNOWN
)
return if (isIncoming) {
IncomingTextOnly(conversationMessage)
} else {
OutgoingTextOnly(conversationMessage)
}
}
private fun getNow(): Long {
val now = System.currentTimeMillis()
return now - random.nextInt(20.milliseconds.inWholeMilliseconds.toInt()).toLong()
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.internal.conversation
import org.signal.paging.PagedDataSource
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
import org.thoughtcrime.securesms.conversation.v2.data.ConversationMessageElement
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import kotlin.math.min
class InternalConversationTestDataSource(
private val size: Int,
private val generator: ConversationElementGenerator
) : PagedDataSource<ConversationElementKey, MappingModel<*>> {
override fun size(): Int = size
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): MutableList<MappingModel<*>> {
val end = min(start + length, totalSize)
return (start until end).map {
load(ConversationElementKey.forMessage(it.toLong()))!!
}.toMutableList()
}
override fun getKey(data: MappingModel<*>): ConversationElementKey {
check(data is ConversationMessageElement)
return ConversationElementKey.forMessage(data.conversationMessage.messageRecord.id)
}
override fun load(key: ConversationElementKey?): MappingModel<*>? {
return key?.let { generator.getMappingModel(it) }
}
}

View File

@@ -0,0 +1,292 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.internal.conversation
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener
import org.thoughtcrime.securesms.conversation.ConversationItem
import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.databinding.ConversationTestFragmentBinding
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.doAfterNextLayout
class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fragment) {
companion object {
private val TAG = Log.tag(InternalConversationTestFragment::class.java)
}
private val binding by ViewBinderDelegate(ConversationTestFragmentBinding::bind)
private val viewModel: InternalConversationTestViewModel by viewModels()
private val lifecycleDisposable = LifecycleDisposable()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = ConversationAdapterV2(
lifecycleOwner = viewLifecycleOwner,
glideRequests = GlideApp.with(this),
clickListener = ClickListener(),
hasWallpaper = false,
colorizer = Colorizer()
)
var startTime = 0L
var firstRender = true
lifecycleDisposable.bindTo(viewLifecycleOwner)
adapter.setPagingController(viewModel.controller)
lifecycleDisposable += viewModel.data.observeOn(AndroidSchedulers.mainThread()).subscribeBy {
if (firstRender) {
startTime = System.currentTimeMillis()
}
adapter.submitList(it) {
if (firstRender) {
firstRender = false
binding.root.doAfterNextLayout {
val endTime = System.currentTimeMillis()
Log.d(TAG, "First render in ${endTime - startTime} millis")
}
}
}
}
binding.root.layoutManager = SmoothScrollingLinearLayoutManager(requireContext(), true)
binding.root.adapter = adapter
RecyclerViewColorizer(binding.root).apply {
setChatColors(ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto))
}
}
private inner class ClickListener : ItemClickListener {
override fun onQuoteClicked(messageRecord: MmsMessageRecord?) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onLinkPreviewClicked(linkPreview: LinkPreview) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onMoreTextClicked(conversationRecipientId: RecipientId, messageId: Long, isMms: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onStickerClicked(stickerLocator: StickerLocator) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onViewOnceMessageClicked(messageRecord: MmsMessageRecord) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onSharedContactDetailsClicked(contact: Contact, avatarTransitionView: View) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onAddToContactsClicked(contact: Contact) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onMessageSharedContactClicked(choices: MutableList<Recipient>) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onInviteSharedContactClicked(choices: MutableList<Recipient>) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onMessageWithErrorClicked(messageRecord: MessageRecord) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onIncomingIdentityMismatchClicked(recipientId: RecipientId) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onRegisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onUnregisterVoiceNoteCallbacks(onPlaybackStartObserver: Observer<VoiceNotePlaybackState>) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onVoiceNotePause(uri: Uri) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onVoiceNotePlay(uri: Uri, messageId: Long, position: Double) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onVoiceNoteSeekTo(uri: Uri, position: Double) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onVoiceNotePlaybackSpeedChanged(uri: Uri, speed: Float) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onChatSessionRefreshLearnMoreClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onBadDecryptLearnMoreClicked(author: RecipientId) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onJoinGroupCallClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onEnableCallNotificationsClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onPlayInlineContent(conversationMessage: ConversationMessage?) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onInMemoryMessageClicked(messageRecord: InMemoryMessageRecord) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onViewGroupDescriptionChange(groupId: GroupId?, description: String, isMessageRequestAccepted: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onChangeNumberUpdateContact(recipient: Recipient) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onCallToAction(action: String) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onDonateClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onBlockJoinRequest(recipient: Recipient) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onRecipientNameClicked(target: RecipientId) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onInviteToSignalClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onActivatePaymentsClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onSendPaymentClicked(recipientId: RecipientId) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onScheduledIndicatorClicked(view: View, conversationMessage: ConversationMessage) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onUrlClicked(url: String): Boolean {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
return true
}
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onGiftBadgeRevealed(messageRecord: MessageRecord) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onJoinCallLink(callLinkRootKey: CallLinkRootKey) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onItemClick(item: MultiselectPart?) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -0,0 +1,27 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.internal.conversation
import androidx.lifecycle.ViewModel
import org.signal.paging.PagedData
import org.signal.paging.PagingConfig
class InternalConversationTestViewModel : ViewModel() {
private val generator = ConversationElementGenerator()
private val dataSource = InternalConversationTestDataSource(
500,
generator
)
private val config = PagingConfig.Builder().setPageSize(25)
.setBufferPages(2)
.build()
private val pagedData = PagedData.createForObservable(dataSource, config)
val controller = pagedData.controller
val data = pagedData.data
}

View File

@@ -17,6 +17,7 @@
<uses-feature android:name="android.hardware.wifi" android:required="false"/>
<uses-feature android:name="android.hardware.portrait" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-feature android:name="android.hardware.telephony" android:required="false" />
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
@@ -602,10 +603,13 @@
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="signal.link" />
<data android:scheme="sgnl" />
<data android:scheme="https" />
<data android:host="signal.link" />
</intent-filter>
</activity>
@@ -1154,6 +1158,11 @@
<receiver android:name=".payments.backup.phrase.ClearClipboardAlarmReceiver" />
<provider android:name=".providers.AvatarProvider"
android:authorities="${applicationId}.avatar"
android:exported="false"
android:grantUriPermissions="true" />
<provider android:name=".providers.PartProvider"
android:grantUriPermissions="true"
android:exported="false"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 150 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: 148 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.glide.transforms
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import kotlin.math.max
import kotlin.math.min
object SignalDownsampleStrategy {
/**
* Center outside, but don't up-scale, only downscale. You should be setting centerOutside
* on the target image view to still maintain center outside behavior.
*/
@JvmField
val CENTER_OUTSIDE_NO_UPSCALE: DownsampleStrategy = CenterOutsideNoUpscale()
private class CenterOutsideNoUpscale : DownsampleStrategy() {
override fun getScaleFactor(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
): Float {
val widthPercentage = requestedWidth / sourceWidth.toFloat()
val heightPercentage = requestedHeight / sourceHeight.toFloat()
return min(MAX_SCALE_FACTOR, max(widthPercentage, heightPercentage))
}
override fun getSampleSizeRounding(
sourceWidth: Int,
sourceHeight: Int,
requestedWidth: Int,
requestedHeight: Int
): SampleSizeRounding {
return SampleSizeRounding.QUALITY
}
companion object {
private const val MAX_SCALE_FACTOR = 1f
}
}
}

View File

@@ -8,6 +8,7 @@ import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationItem;
@@ -116,5 +117,6 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
}
}

View File

@@ -6,6 +6,8 @@ import android.view.Window;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
@@ -20,7 +22,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
@Override
protected void onCreate(Bundle bundle, boolean ready) {
AlertDialog dialog = new AlertDialog.Builder(this)
AlertDialog dialog = new MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
.setPositiveButton(R.string.DeviceProvisioningActivity_continue, (dialog1, which) -> {

View File

@@ -5,6 +5,8 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
@@ -26,7 +28,7 @@ public final class GroupMembersDialog {
}
public void display() {
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
AlertDialog dialog = new MaterialAlertDialogBuilder(fragmentActivity)
.setTitle(R.string.ConversationActivity_group_members)
.setIcon(R.drawable.ic_group_24)
.setCancelable(true)

View File

@@ -21,6 +21,8 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
@@ -217,7 +219,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
private class SmsSendClickListener implements OnClickListener {
@Override
public void onClick(View v) {
new AlertDialog.Builder(InviteActivity.this)
new MaterialAlertDialogBuilder(InviteActivity.this)
.setTitle(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_invites,
contactsFragment.getSelectedContacts().size(),
contactsFragment.getSelectedContacts().size()))

View File

@@ -101,6 +101,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
handleGroupLinkInIntent(intent);
handleProxyInIntent(intent);
handleSignalMeIntent(intent);
handleCallLinkInIntent(intent);
}
@Override

View File

@@ -28,6 +28,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.google.android.gms.common.GoogleApiAvailability;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
public class PlayServicesProblemFragment extends DialogFragment {
@@ -37,7 +38,7 @@ public class PlayServicesProblemFragment extends DialogFragment {
Dialog dialog = GoogleApiAvailability.getInstance().getErrorDialog(getActivity(), code, 9111);
if (dialog == null) {
return new AlertDialog.Builder(requireActivity())
return new MaterialAlertDialogBuilder(requireActivity())
.setNegativeButton(android.R.string.ok, null)
.setMessage(R.string.PlayServicesProblemFragment_the_version_of_google_play_services_you_have_installed_is_not_functioning)
.create();

View File

@@ -55,6 +55,7 @@ import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.webrtc.CallLinkInfoSheet;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
@@ -71,6 +72,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
@@ -104,9 +106,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private static final int STANDARD_DELAY_FINISH = 1000;
private static final int VIBRATE_DURATION = 50;
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION";
/**
* ANSWER the call via voice-only.
*/
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
/**
* ANSWER the call via video.
*/
public static final String ANSWER_VIDEO_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_VIDEO_ACTION";
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION";
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN";
@@ -159,10 +169,18 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
initializeViewModel(isLandscapeEnabled);
initializePictureInPictureParams();
processIntent(getIntent());
logIntent(getIntent());
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
if (ANSWER_VIDEO_ACTION.equals(getIntent().getAction())) {
enableVideoIfAvailable = true;
} else if (ANSWER_ACTION.equals(getIntent().getAction()) || getIntent().getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false)) {
enableVideoIfAvailable = false;
} else {
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
}
processIntent(getIntent());
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
@@ -211,6 +229,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public void onNewIntent(Intent intent) {
Log.i(TAG, "onNewIntent(" + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")");
super.onNewIntent(intent);
logIntent(intent);
processIntent(intent);
}
@@ -297,9 +316,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode();
}
private void logIntent(@NonNull Intent intent) {
Log.d(TAG, "Intent: Action: " + intent.getAction());
Log.d(TAG, "Intent: EXTRA_STARTED_FROM_FULLSCREEN: " + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false));
Log.d(TAG, "Intent: EXTRA_ENABLE_VIDEO_IF_AVAILABLE: " + intent.getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false));
}
private void processIntent(@NonNull Intent intent) {
if (ANSWER_ACTION.equals(intent.getAction())) {
handleAnswerWithAudio();
} else if (ANSWER_VIDEO_ACTION.equals(intent.getAction())) {
handleAnswerWithVideo();
} else if (DENY_ACTION.equals(intent.getAction())) {
handleDenyCall();
} else if (END_CALL_ACTION.equals(intent.getAction())) {
@@ -498,26 +525,20 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void handleAnswerWithVideo() {
Recipient recipient = viewModel.getRecipient().get();
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone_and_camera), R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
if (!recipient.equals(Recipient.UNKNOWN)) {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_answering));
ApplicationDependencies.getSignalCallManager().acceptCall(true);
ApplicationDependencies.getSignalCallManager().acceptCall(true);
handleSetMuteVideo(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
}
handleSetMuteVideo(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
}
private void handleDenyCall() {
@@ -901,7 +922,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onCallInfoClicked() {
CallParticipantsListDialog.show(getSupportFragmentManager());
LiveRecipient liveRecipient = viewModel.getRecipient();
if (liveRecipient.get().isCallLink()) {
CallLinkInfoSheet.show(getSupportFragmentManager(), liveRecipient.get().requireCallLinkRoomId());
} else {
CallParticipantsListDialog.show(getSupportFragmentManager());
}
}
@Override

View File

@@ -35,6 +35,9 @@ public abstract class Attachment {
@Nullable
private final byte[] digest;
@Nullable
private final byte[] incrementalDigest;
@Nullable
private final String fastPreflightId;
@@ -70,6 +73,7 @@ public abstract class Attachment {
@Nullable String key,
@Nullable String relay,
@Nullable byte[] digest,
@Nullable byte[] incrementalDigest,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
@@ -93,6 +97,7 @@ public abstract class Attachment {
this.key = key;
this.relay = relay;
this.digest = digest;
this.incrementalDigest = incrementalDigest;
this.fastPreflightId = fastPreflightId;
this.voiceNote = voiceNote;
this.borderless = borderless;
@@ -165,6 +170,11 @@ public abstract class Attachment {
return digest;
}
@Nullable
public byte[] getIncrementalDigest() {
return incrementalDigest;
}
@Nullable
public String getFastPreflightId() {
return fastPreflightId;

View File

@@ -33,6 +33,7 @@ public class DatabaseAttachment extends Attachment {
String key,
String relay,
byte[] digest,
byte[] incrementalDigest,
String fastPreflightId,
boolean voiceNote,
boolean borderless,
@@ -48,7 +49,7 @@ public class DatabaseAttachment extends Attachment {
int displayOrder,
long uploadTimestamp)
{
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.attachmentId = attachmentId;
this.hasData = hasData;
this.hasThumbnail = hasThumbnail;

View File

@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.MessageTable;
public class MmsNotificationAttachment extends Attachment {
public MmsNotificationAttachment(int status, long size) {
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null);
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null);
}
@Nullable

View File

@@ -30,6 +30,7 @@ public class PointerAttachment extends Attachment {
@Nullable String key,
@Nullable String relay,
@Nullable byte[] digest,
@Nullable byte[] incrementalDigest,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
@@ -41,7 +42,7 @@ public class PointerAttachment extends Attachment {
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash)
{
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
}
@Nullable
@@ -112,6 +113,7 @@ public class PointerAttachment extends Attachment {
pointer.get().asPointer().getRemoteId().toString(),
encodedKey, null,
pointer.get().asPointer().getDigest().orElse(null),
pointer.get().asPointer().getincrementalDigest().orElse(null),
fastPreflightId,
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().isBorderless(),
@@ -137,6 +139,7 @@ public class PointerAttachment extends Attachment {
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
null,
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null,
null,
false,
false,
@@ -166,6 +169,7 @@ public class PointerAttachment extends Attachment {
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
null,
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
thumbnail != null ? thumbnail.asPointer().getincrementalDigest().orElse(null) : null,
null,
false,
false,

View File

@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentTable;
public class TombstoneAttachment extends Attachment {
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
}
@Override

View File

@@ -52,7 +52,7 @@ public class UriAttachment extends Attachment {
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
super(contentType, transferState, size, fileName, 0, null, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.dataUri = Objects.requireNonNull(dataUri);
}

View File

@@ -48,14 +48,14 @@ public class AudioRecorder {
if (this.uiHandler != null) {
onAudioFocusChangeListener = focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording");
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording via UI handler.");
this.uiHandler.onRecordCanceled(false);
}
};
} else {
onAudioFocusChangeListener = focusChange -> {
if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording");
Log.i(TAG, "Audio focus change " + focusChange + " stopping recording.");
stopRecording();
}
};

View File

@@ -25,14 +25,14 @@ sealed interface BluetoothVoiceNoteUtil {
fun destroy()
companion object {
fun create(context: Context, listener: () -> Unit, bluetoothPermissionDeniedHandler: () -> Unit): BluetoothVoiceNoteUtil {
fun create(context: Context, listener: (Boolean) -> Unit, bluetoothPermissionDeniedHandler: () -> Unit): BluetoothVoiceNoteUtil {
return if (Build.VERSION.SDK_INT >= 31) BluetoothVoiceNoteUtil31(listener) else BluetoothVoiceNoteUtilLegacy(context, listener, bluetoothPermissionDeniedHandler)
}
}
}
@RequiresApi(31)
private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoiceNoteUtil {
private class BluetoothVoiceNoteUtil31(val listener: (Boolean) -> Unit) : BluetoothVoiceNoteUtil {
override fun connectBluetoothScoConnection() {
val audioManager = ApplicationDependencies.getAndroidCallAudioManager()
val device: AudioDeviceInfo? = audioManager.connectedBluetoothDevice
@@ -40,13 +40,15 @@ private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoic
val result: Boolean = audioManager.setCommunicationDevice(device)
if (result) {
Log.d(TAG, "Successfully set Bluetooth device as active communication device.")
listener(true)
} else {
Log.d(TAG, "Found Bluetooth device but failed to set it as active communication device.")
listener(false)
}
} else {
Log.d(TAG, "Could not find Bluetooth device in list of communications devices, falling back to current input.")
listener(false)
}
listener()
}
override fun disconnectBluetoothScoConnection() {
@@ -64,15 +66,23 @@ private class BluetoothVoiceNoteUtil31(val listener: () -> Unit) : BluetoothVoic
* @param listener This will be executed on the main thread after the Bluetooth connection connects, or if it doesn't.
* @param bluetoothPermissionDeniedHandler called when we detect the Bluetooth permission has been denied to our app.
*/
private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: () -> Unit, val bluetoothPermissionDeniedHandler: () -> Unit) : BluetoothVoiceNoteUtil {
private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: (Boolean) -> Unit, val bluetoothPermissionDeniedHandler: () -> Unit) : BluetoothVoiceNoteUtil {
private val commandAndControlThread: HandlerThread = SignalExecutors.getAndStartHandlerThread("voice-note-audio", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD)
private val uiThreadHandler = Handler(context.mainLooper)
private val audioHandler: SignalAudioHandler = SignalAudioHandler(commandAndControlThread.looper)
private val deviceUpdatedListener: AudioDeviceUpdatedListener = object : AudioDeviceUpdatedListener {
override fun onAudioDeviceUpdated() {
if (signalBluetoothManager.state == SignalBluetoothManager.State.CONNECTED) {
Log.d(TAG, "Bluetooth SCO connected. Starting voice note recording on UI thread.")
uiThreadHandler.post { listener() }
when (signalBluetoothManager.state) {
SignalBluetoothManager.State.CONNECTED -> {
Log.d(TAG, "Bluetooth SCO connected. Starting voice note recording on UI thread.")
uiThreadHandler.post { listener(true) }
}
SignalBluetoothManager.State.ERROR,
SignalBluetoothManager.State.PERMISSION_DENIED -> {
Log.w(TAG, "Unable to complete Bluetooth connection due to ${signalBluetoothManager.state}. Starting voice note recording anyway on UI thread.")
uiThreadHandler.post { listener(false) }
}
else -> Log.d(TAG, "Current Bluetooth connection state: ${signalBluetoothManager.state}.")
}
}
}
@@ -105,7 +115,7 @@ private class BluetoothVoiceNoteUtilLegacy(val context: Context, val listener: (
bluetoothPermissionDeniedHandler()
hasWarnedAboutBluetooth = true
}
listener()
listener(false)
}
}
}

View File

@@ -140,7 +140,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
viewModel.onAvatarEditCompleted(vector)
}
setFragmentResultListener(PhotoEditorFragment.REQUEST_KEY_EDIT) { _, bundle ->
setFragmentResultListener(PhotoEditorFragment.REQUEST_KEY_EDIT) { _, _ ->
}
photoEditorLauncher = registerForActivityResult(PhotoEditorActivity.Contract()) { photo ->
@@ -155,6 +155,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
ViewUtil.hideKeyboard(requireContext(), requireView())
}
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
val media: Media = requireNotNull(data.getParcelableExtraCompat(AvatarSelectionActivity.EXTRA_MEDIA, Media::class.java))
@@ -194,7 +195,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
return true
}
fun openEditor(avatar: Avatar) {
private fun openEditor(avatar: Avatar) {
when (avatar) {
is Avatar.Photo -> openPhotoEditor(avatar)
is Avatar.Resource -> throw UnsupportedOperationException()
@@ -250,6 +251,7 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
.execute()
}
@Deprecated("Deprecated in Java")
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}

View File

@@ -1,9 +1,9 @@
package org.thoughtcrime.securesms.avatar.text
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.distinctUntilChanged
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.util.livedata.Store
@@ -12,7 +12,7 @@ class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
private val store = Store(TextAvatarCreationState(initialText))
val state: LiveData<TextAvatarCreationState> = Transformations.distinctUntilChanged(store.stateLiveData)
val state: LiveData<TextAvatarCreationState> = store.stateLiveData.distinctUntilChanged()
fun setColor(colorPair: Avatars.ColorPair) {
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }

View File

@@ -45,7 +45,7 @@ public class BackupDialog {
@NonNull Runnable onBackupsEnabled)
{
String[] password = BackupUtil.generateBackupPassphrase();
AlertDialog dialog = new AlertDialog.Builder(context)
AlertDialog dialog = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.BackupDialog_enable_local_backups)
.setView(backupDirectorySelectionIntent != null ? R.layout.backup_enable_dialog_v29 : R.layout.backup_enable_dialog)
.setPositiveButton(R.string.BackupDialog_enable_backups, null)

View File

@@ -42,12 +42,20 @@ object BackupVerifier {
frame = inputStream.readFrame()
}
if (frame.end == true) {
count++
}
}
if (cancellationSignal.isCanceled) {
throw FullBackupExporter.BackupCanceledException()
}
if (count != expectedCount) {
Log.e(TAG, "Incorrect number of frames expected $expectedCount but only $count")
return false
}
return true
}

View File

@@ -228,7 +228,7 @@ public class FullBackupExporter extends FullBackupBase {
outputStream.close();
}
}
return new BackupEvent(BackupEvent.Type.FINISHED, ++count, estimatedCountOutside);
return new BackupEvent(BackupEvent.Type.FINISHED, outputStream.frames, estimatedCountOutside);
}
private static long calculateCount(@NonNull Context context, @NonNull SQLiteDatabase input, List<String> tables) {
@@ -637,6 +637,8 @@ public class FullBackupExporter extends FullBackupBase {
private final byte[] iv;
private int counter;
private int frames;
private BackupFrameOutputStream(@NonNull OutputStream output, @NonNull String passphrase) throws IOException {
try {
byte[] salt = Util.getSecretBytes(32);
@@ -796,6 +798,7 @@ public class FullBackupExporter extends FullBackupBase {
out.write(length);
out.write(frameCiphertext);
out.write(frameMac, 0, 10);
frames++;
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
}

View File

@@ -23,6 +23,6 @@ data class GiftFlowState(
RECIPIENT_VERIFICATION,
TOKEN_REQUEST,
PAYMENT_PIPELINE,
FAILURE;
FAILURE
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.calls.links
import android.content.Context
import android.util.AttributeSet
import androidx.annotation.ColorRes
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import com.google.android.material.button.MaterialButton
import org.thoughtcrime.securesms.R
class CallLinkJoinButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : LinearLayoutCompat(context, attrs) {
init {
orientation = VERTICAL
inflate(context, R.layout.call_link_join_button, this)
}
private val joinButton: MaterialButton = findViewById(R.id.join_button)
fun setTextColor(@ColorRes textColorResId: Int) {
joinButton.setTextColor(ContextCompat.getColor(context, textColorResId))
}
fun setJoinClickListener(onClickListener: OnClickListener) {
joinButton.setOnClickListener(onClickListener)
}
}

View File

@@ -6,8 +6,8 @@
package org.thoughtcrime.securesms.calls.links
import io.reactivex.rxjava3.core.Observable
import org.signal.core.util.Hex
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallException
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.DatabaseObserver
@@ -21,10 +21,12 @@ import java.net.URLDecoder
*/
object CallLinks {
private const val ROOT_KEY = "key"
private const val HTTPS_LINK_PREFIX = "https://signal.link/call/#key="
private const val SNGL_LINK_PREFIX = "sgnl://signal.link/#key="
private val TAG = Log.tag(CallLinks::class.java)
fun url(linkKeyBytes: ByteArray) = "https://signal.link/call/#key=${Hex.dump(linkKeyBytes)}"
fun url(linkKeyBytes: ByteArray) = "$HTTPS_LINK_PREFIX${CallLinkRootKey(linkKeyBytes)}"
fun watchCallLink(roomId: CallLinkRoomId): Observable<CallLinkTable.CallLink> {
return Observable.create { emitter ->
@@ -49,8 +51,23 @@ object CallLinks {
}
}
@JvmStatic
fun isCallLink(url: String): Boolean {
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
Log.w(TAG, "Invalid url prefix.")
return false
}
return url.split("#").last().startsWith("key=")
}
@JvmStatic
fun parseUrl(url: String): CallLinkRootKey? {
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
Log.w(TAG, "Invalid url prefix.")
return null
}
val parts = url.split("#")
if (parts.size != 2) {
Log.w(TAG, "Invalid fragment delimiter count in url.")
@@ -77,7 +94,11 @@ object CallLinks {
return null
}
// TODO Parse the key into a byte array
return null
return try {
CallLinkRootKey(key)
} catch (e: CallException) {
Log.w(TAG, "Invalid root key found in fragment query string.")
null
}
}
}

View File

@@ -37,7 +37,6 @@ import org.signal.core.ui.Buttons
import org.signal.core.ui.theme.SignalTheme
import org.signal.ringrtc.CallLinkRootKey
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -49,7 +48,6 @@ import java.time.Instant
@Preview
@Composable
private fun SignalCallRowPreview() {
val avatarColor = remember { AvatarColor.random() }
val callLink = remember {
val credentials = CallLinkCredentials.generate()
CallLinkTable.CallLink(
@@ -61,8 +59,7 @@ private fun SignalCallRowPreview() {
restrictions = org.signal.ringrtc.CallLinkState.Restrictions.NONE,
expiration = Instant.MAX,
revoked = false
),
avatarColor = avatarColor
)
)
}
SignalTheme(false) {
@@ -76,7 +73,7 @@ private fun SignalCallRowPreview() {
@Composable
fun SignalCallRow(
callLink: CallLinkTable.CallLink,
onJoinClicked: () -> Unit,
onJoinClicked: (() -> Unit)?,
modifier: Modifier = Modifier
) {
Row(
@@ -122,13 +119,15 @@ fun SignalCallRow(
)
}
Spacer(modifier = Modifier.width(10.dp))
if (onJoinClicked != null) {
Spacer(modifier = Modifier.width(10.dp))
Buttons.Small(
onClick = onJoinClicked,
modifier = Modifier.align(CenterVertically)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
Buttons.Small(
onClick = onJoinClicked,
modifier = Modifier.align(CenterVertically)
) {
Text(text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__join))
}
}
}
}

View File

@@ -34,6 +34,7 @@ import androidx.core.app.ShareCompat
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dividers
import org.signal.core.ui.Rows
@@ -86,7 +87,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
) {
val callLink: CallLinkTable.CallLink by viewModel.callLink
Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
BottomSheets.Handle(modifier = Modifier.align(Alignment.CenterHorizontally))
Spacer(modifier = Modifier.height(20.dp))
@@ -108,7 +109,7 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__add_call_name),
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked)
onClick = this@CreateCallLinkBottomSheetDialogFragment::onAddACallNameClicked
)
Rows.ToggleRow(
@@ -123,19 +124,19 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal),
icon = ImageVector.vectorResource(id = R.drawable.symbol_forward_24),
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked)
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_copy_android_24),
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked)
onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_share_android_24),
modifier = Modifier.clickable(onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked)
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked
)
Buttons.MediumTonal(

View File

@@ -7,7 +7,6 @@ package org.thoughtcrime.securesms.calls.links.create
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
@@ -24,7 +23,7 @@ import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
class CreateCallLinkRepository(
private val callLinkManager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager
) {
fun ensureCallLinkCreated(credentials: CallLinkCredentials, avatarColor: AvatarColor): Single<EnsureCallLinkCreatedResult> {
fun ensureCallLinkCreated(credentials: CallLinkCredentials): Single<EnsureCallLinkCreatedResult> {
val callLinkRecipientId = Single.fromCallable {
SignalDatabase.recipients.getByCallLinkRoomId(credentials.roomId)
}
@@ -41,8 +40,7 @@ class CreateCallLinkRepository(
recipientId = RecipientId.UNKNOWN,
roomId = credentials.roomId,
credentials = credentials,
state = it.state,
avatarColor = avatarColor
state = it.state
)
)

View File

@@ -16,7 +16,6 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.ringrtc.CallLinkState.Restrictions
import org.thoughtcrime.securesms.calls.links.CallLinks
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials
@@ -29,7 +28,6 @@ class CreateCallLinkViewModel(
private val mutationRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
) : ViewModel() {
private val credentials = CallLinkCredentials.generate()
private val avatarColor = AvatarColor.random()
private val _callLink: MutableState<CallLinkTable.CallLink> = mutableStateOf(
CallLinkTable.CallLink(
recipientId = RecipientId.UNKNOWN,
@@ -40,8 +38,7 @@ class CreateCallLinkViewModel(
restrictions = Restrictions.NONE,
revoked = false,
expiration = Instant.MAX
),
avatarColor = avatarColor
)
)
)
@@ -63,7 +60,7 @@ class CreateCallLinkViewModel(
}
fun commitCallLink(): Single<EnsureCallLinkCreatedResult> {
return repository.ensureCallLinkCreated(credentials, avatarColor)
return repository.ensureCallLinkCreated(credentials)
}
fun setApproveAllMembers(approveAllMembers: Boolean): Single<UpdateCallLinkResult> {

View File

@@ -10,7 +10,6 @@ import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
@@ -200,8 +199,7 @@ private fun CallLinkDetailsPreview() {
revoked = false,
restrictions = Restrictions.NONE,
expiration = Instant.MAX
),
avatarColor = avatarColor
)
)
}
@@ -248,7 +246,7 @@ private fun CallLinkDetails(
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__add_call_name),
modifier = Modifier.clickable(onClick = callback::onEditNameClicked)
onClick = callback::onEditNameClicked
)
Rows.ToggleRow(
@@ -262,14 +260,14 @@ private fun CallLinkDetails(
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_link_24),
modifier = Modifier.clickable(onClick = callback::onShareClicked)
onClick = callback::onShareClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__delete_call_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_trash_24),
foregroundTint = MaterialTheme.colorScheme.error,
modifier = Modifier.clickable(onClick = callback::onDeleteClicked)
onClick = callback::onDeleteClicked
)
}

View File

@@ -5,17 +5,15 @@
package org.thoughtcrime.securesms.calls.links.details
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.MaybeCompat
import org.signal.core.util.orNull
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.ReadCallLinkResult
import org.thoughtcrime.securesms.service.webrtc.links.SignalCallLinkManager
@@ -24,7 +22,7 @@ class CallLinkDetailsRepository(
private val callLinkManager: SignalCallLinkManager = ApplicationDependencies.getSignalCallManager().callLinkManager
) {
fun refreshCallLinkState(callLinkRoomId: CallLinkRoomId): Disposable {
return Maybe.fromCallable<CallLinkTable.CallLink> { SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId) }
return MaybeCompat.fromCallable { SignalDatabase.callLinks.getCallLinkByRoomId(callLinkRoomId) }
.flatMapSingle { callLinkManager.readCallLink(it.credentials!!) }
.subscribeOn(Schedulers.io())
.subscribeBy { result ->
@@ -36,7 +34,7 @@ class CallLinkDetailsRepository(
}
fun watchCallLinkRecipient(callLinkRoomId: CallLinkRoomId): Observable<Recipient> {
return Maybe.fromCallable<RecipientId> { SignalDatabase.recipients.getByCallLinkRoomId(callLinkRoomId).orNull() }
return MaybeCompat.fromCallable { SignalDatabase.recipients.getByCallLinkRoomId(callLinkRoomId).orNull() }
.flatMapObservable { Recipient.observable(it) }
.distinctUntilChanged { a, b -> a.hasSameContent(b) }
.subscribeOn(Schedulers.io())

View File

@@ -75,12 +75,10 @@ class CallLogAdapter(
fun submitCallRows(
rows: List<CallLogRow?>,
selectionState: CallLogSelectionState,
stagedDeletion: CallLogStagedDeletion?,
onCommit: () -> Unit
): Int {
val filteredRows = rows
.filterNotNull()
.filterNot { stagedDeletion?.isStagedForDeletion(it.id) == true }
.map {
when (it) {
is CallLogRow.Call -> CallModel(it, selectionState, itemCount)
@@ -355,6 +353,7 @@ class CallLogAdapter(
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_compact_16
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16
MessageTypes.GROUP_CALL_TYPE -> when {
call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16
call.event == CallTable.Event.MISSED -> R.drawable.symbol_missed_incoming_24
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16
@@ -376,6 +375,7 @@ class CallLogAdapter(
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
MessageTypes.GROUP_CALL_TYPE -> when {
call.type == CallTable.Type.AD_HOC_CALL -> R.string.CallLogAdapter__call_link
call.event == CallTable.Event.MISSED -> R.string.CallLogAdapter__missed
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
call.direction == CallTable.Direction.INCOMING -> R.string.CallLogAdapter__incoming

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.calls.log
sealed interface CallLogDeletionResult {
object Success : CallLogDeletionResult
object Empty : CallLogDeletionResult
data class FailedToRevoke(val failedRevocations: Int) : CallLogDeletionResult
data class UnknownFailure(val reason: Throwable) : CallLogDeletionResult
}

View File

@@ -12,5 +12,10 @@ enum class CallLogFilter {
/**
* Only missed calls will be displayed
*/
MISSED
MISSED,
/**
* Only ad-hoc calls will be returned
*/
AD_HOC
}

View File

@@ -7,7 +7,9 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.coordinatorlayout.widget.CoordinatorLayout
@@ -27,10 +29,13 @@ import io.reactivex.rxjava3.kotlin.Flowables
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.DimensionUnit
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.concurrent.addTo
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
import org.thoughtcrime.securesms.calls.new.NewCallActivity
import org.thoughtcrime.securesms.components.Material3SearchToolbar
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.menu.ActionItem
@@ -65,6 +70,10 @@ import java.util.concurrent.TimeUnit
@SuppressLint("DiscouragedApi")
class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Callbacks, CallLogContextMenu.Callbacks {
companion object {
private val TAG = Log.tag(CallLogFragment::class.java)
}
private val viewModel: CallLogViewModel by viewModels()
private val binding: CallLogFragmentBinding by ViewBinderDelegate(CallLogFragmentBinding::bind)
private val disposables = LifecycleDisposable()
@@ -84,10 +93,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
val isFiltered = viewModel.filterSnapshot == CallLogFilter.MISSED
menu.findItem(R.id.action_clear_missed_call_filter).isVisible = isFiltered
menu.findItem(R.id.action_filter_missed_calls).isVisible = !isFiltered
menu.findItem(R.id.action_clear_call_history).isVisible = !viewModel.isEmpty
}
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
when (menuItem.itemId) {
R.id.action_clear_call_history -> clearCallHistory()
R.id.action_settings -> startActivity(AppSettingsActivity.home(requireContext()))
R.id.action_notification_profile -> NotificationProfileSelectionFragment.show(parentFragmentManager)
R.id.action_filter_missed_calls -> filterMissedCalls()
@@ -112,24 +123,23 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
)
disposables += scrollToPositionDelegate
disposables += Flowables.combineLatest(viewModel.data, viewModel.selectedAndStagedDeletion)
disposables += Flowables.combineLatest(viewModel.data, viewModel.selected)
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (data, selected) ->
val filteredCount = adapter.submitCallRows(
data,
selected.first,
selected.second,
selected,
scrollToPositionDelegate::notifyListCommitted
)
binding.emptyState.visible = filteredCount == 0
}
disposables += Flowables.combineLatest(viewModel.selectedAndStagedDeletion, viewModel.totalCount)
disposables += Flowables.combineLatest(viewModel.selected, viewModel.totalCount)
.distinctUntilChanged()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (selected, totalCount) ->
if (selected.first.isNotEmpty(totalCount)) {
callLogActionMode.setCount(selected.first.count(totalCount))
if (selected.isNotEmpty(totalCount)) {
callLogActionMode.setCount(selected.count(totalCount))
} else {
callLogActionMode.end()
}
@@ -220,20 +230,9 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
val count = callLogActionMode.getCount()
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, count, count))
.setPositiveButton(R.string.CallLogFragment__delete_for_me) { _, _ ->
viewModel.stageSelectionDeletion()
.setPositiveButton(R.string.CallLogFragment__delete) { _, _ ->
performDeletion(count, viewModel.stageSelectionDeletion())
callLogActionMode.end()
Snackbar
.make(
binding.root,
resources.getQuantityString(R.plurals.CallLogFragment__d_calls_deleted, count, count),
Snackbar.LENGTH_SHORT
)
.addCallback(SnackbarDeletionCallback())
.setAction(R.string.CallLogFragment__undo) {
viewModel.cancelStagedDeletion()
}
.show()
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
@@ -270,6 +269,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
scrollToPositionDelegate.resetScrollPosition()
}
}
FilterPullState.OPENING -> {
ViewUtil.setMinimumHeight(collapsingToolbarLayout, openHeight)
viewModel.setFilter(CallLogFilter.MISSED)
@@ -363,21 +363,9 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
override fun deleteCall(call: CallLogRow) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(resources.getQuantityString(R.plurals.CallLogFragment__delete_d_calls, 1, 1))
.setPositiveButton(R.string.CallLogFragment__delete_for_me) { _, _ ->
viewModel.stageCallDeletion(call)
Snackbar
.make(
binding.root,
resources.getQuantityString(R.plurals.CallLogFragment__d_calls_deleted, 1, 1),
Snackbar.LENGTH_SHORT
)
.addCallback(SnackbarDeletionCallback())
.setAction(R.string.CallLogFragment__undo) {
viewModel.cancelStagedDeletion()
}
.show()
.setPositiveButton(R.string.CallLogFragment__delete) { _, _ ->
performDeletion(1, viewModel.stageCallDeletion(call))
}
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.show()
}
@@ -386,6 +374,18 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
binding.recyclerCoordinatorAppBar.setExpanded(false, true)
}
private fun clearCallHistory() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.CallLogFragment__clear_call_history_question)
.setMessage(R.string.CallLogFragment__this_will_permanently_delete_all_call_history)
.setPositiveButton(android.R.string.ok) { _, _ ->
callLogActionMode.end()
performDeletion(-1, viewModel.stageDeleteAll())
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun isSearchOpen(): Boolean {
return isSearchVisible() || viewModel.hasSearchQuery
}
@@ -401,7 +401,59 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
private fun isSearchVisible(): Boolean {
return requireListener<SearchBinder>().getSearchToolbar().resolved() &&
requireListener<SearchBinder>().getSearchToolbar().get().getVisibility() == View.VISIBLE
requireListener<SearchBinder>().getSearchToolbar().get().visibility == View.VISIBLE
}
private fun performDeletion(count: Int, callLogStagedDeletion: CallLogStagedDeletion) {
var progressDialog: ProgressCardDialogFragment? = null
var errorDialog: AlertDialog? = null
fun cleanUp() {
progressDialog?.dismissAllowingStateLoss()
progressDialog = null
errorDialog?.dismiss()
errorDialog = null
}
val snackbarMessage = if (count == -1) {
getString(R.string.CallLogFragment__cleared_call_history)
} else {
resources.getQuantityString(R.plurals.CallLogFragment__d_calls_deleted, count, count)
}
viewModel.delete(callLogStagedDeletion)
.observeOn(AndroidSchedulers.mainThread())
.doOnSubscribe {
progressDialog = ProgressCardDialogFragment.create(getString(R.string.CallLogFragment__deleting))
progressDialog?.show(parentFragmentManager, null)
}
.doOnDispose { cleanUp() }
.subscribeBy {
cleanUp()
when (it) {
CallLogDeletionResult.Empty -> Unit
is CallLogDeletionResult.FailedToRevoke -> {
errorDialog = MaterialAlertDialogBuilder(requireContext())
.setMessage(resources.getQuantityString(R.plurals.CallLogFragment__cant_delete_call_link, it.failedRevocations))
.setPositiveButton(R.string.ok, null)
.show()
}
CallLogDeletionResult.Success -> {
Snackbar
.make(
binding.root,
snackbarMessage,
Snackbar.LENGTH_SHORT
)
.show()
}
is CallLogDeletionResult.UnknownFailure -> {
Log.w(TAG, "Deletion failed.", it.reason)
Toast.makeText(requireContext(), R.string.CallLogFragment__deletion_failed, Toast.LENGTH_SHORT).show()
}
}
}
.addTo(disposables)
}
private inner class BottomActionBarControllerCallback : SignalBottomActionBarController.Callback {
@@ -429,12 +481,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
}
}
private inner class SnackbarDeletionCallback : Snackbar.Callback() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
viewModel.commitStagedDeletion()
}
}
interface Callback {
fun onMultiSelectStarted()
fun onMultiSelectFinished()

View File

@@ -2,13 +2,20 @@ package org.thoughtcrime.securesms.calls.log
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.calls.links.UpdateCallLinkRepository
import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.CallLinkPeekJob
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
import org.thoughtcrime.securesms.service.webrtc.links.UpdateCallLinkResult
class CallLogRepository : CallLogPagedDataSource.CallRepository {
class CallLogRepository(
private val updateCallLinkRepository: UpdateCallLinkRepository = UpdateCallLinkRepository()
) : CallLogPagedDataSource.CallRepository {
override fun getCallsCount(query: String?, filter: CallLogFilter): Int {
return SignalDatabase.calls.getCallsCount(query, filter)
}
@@ -20,14 +27,14 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
override fun getCallLinksCount(query: String?, filter: CallLogFilter): Int {
return when (filter) {
CallLogFilter.MISSED -> 0
CallLogFilter.ALL -> SignalDatabase.callLinks.getCallLinksCount(query)
CallLogFilter.ALL, CallLogFilter.AD_HOC -> SignalDatabase.callLinks.getCallLinksCount(query)
}
}
override fun getCallLinks(query: String?, filter: CallLogFilter, start: Int, length: Int): List<CallLogRow> {
return when (filter) {
CallLogFilter.MISSED -> emptyList()
CallLogFilter.ALL -> SignalDatabase.callLinks.getCallLinks(query, start, length)
CallLogFilter.ALL, CallLogFilter.AD_HOC -> SignalDatabase.callLinks.getCallLinks(query, start, length)
}
}
@@ -60,8 +67,8 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
selectedCallRowIds: Set<Long>
): Completable {
return Completable.fromAction {
SignalDatabase.calls.deleteCallEvents(selectedCallRowIds)
}.observeOn(Schedulers.io())
SignalDatabase.calls.deleteNonAdHocCallEvents(selectedCallRowIds)
}.subscribeOn(Schedulers.io())
}
fun deleteAllCallLogsExcept(
@@ -69,7 +76,88 @@ class CallLogRepository : CallLogPagedDataSource.CallRepository {
missedOnly: Boolean
): Completable {
return Completable.fromAction {
SignalDatabase.calls.deleteAllCallEventsExcept(selectedCallRowIds, missedOnly)
}.observeOn(Schedulers.io())
SignalDatabase.calls.deleteAllNonAdHocCallEventsExcept(selectedCallRowIds, missedOnly)
}.subscribeOn(Schedulers.io())
}
/**
* Deletes the selected call links. We DELETE those links we don't have admin keys for,
* and revoke the ones we *do* have admin keys for. We then perform a cleanup step on
* terminate to clean up call events.
*/
fun deleteSelectedCallLinks(
selectedCallRowIds: Set<Long>,
selectedRoomIds: Set<CallLinkRoomId>
): Single<Int> {
return Single.fromCallable {
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
SignalDatabase.callLinks.deleteNonAdminCallLinks(allCallLinkIds)
SignalDatabase.callLinks.getAdminCallLinks(allCallLinkIds)
}.flatMap { callLinksToRevoke ->
Single.merge(
callLinksToRevoke.map {
updateCallLinkRepository.revokeCallLink(it.credentials!!)
}
).reduce(0) { acc, current ->
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
}
}.doOnTerminate {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.doOnDispose {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.subscribeOn(Schedulers.io())
}
/**
* Deletes all but the selected call links. We DELETE those links we don't have admin keys for,
* and revoke the ones we *do* have admin keys for. We then perform a cleanup step on
* terminate to clean up call events.
*/
fun deleteAllCallLinksExcept(
selectedCallRowIds: Set<Long>,
selectedRoomIds: Set<CallLinkRoomId>
): Single<Int> {
return Single.fromCallable {
val allCallLinkIds = SignalDatabase.calls.getCallLinkRoomIdsFromCallRowIds(selectedCallRowIds) + selectedRoomIds
SignalDatabase.callLinks.deleteAllNonAdminCallLinksExcept(allCallLinkIds)
SignalDatabase.callLinks.getAllAdminCallLinksExcept(allCallLinkIds)
}.flatMap { callLinksToRevoke ->
Single.merge(
callLinksToRevoke.map {
updateCallLinkRepository.revokeCallLink(it.credentials!!)
}
).reduce(0) { acc, current ->
acc + (if (current is UpdateCallLinkResult.Success) 0 else 1)
}
}.doOnTerminate {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.doOnDispose {
SignalDatabase.calls.updateAdHocCallEventDeletionTimestamps()
}.subscribeOn(Schedulers.io())
}
fun peekCallLinks(): Completable {
return Completable.fromAction {
val callLinks: List<CallLogRow.CallLink> = SignalDatabase.callLinks.getCallLinks(
query = null,
offset = 0,
limit = 10
)
val callEvents: List<CallLogRow.Call> = SignalDatabase.calls.getCalls(
offset = 0,
limit = 10,
searchTerm = null,
filter = CallLogFilter.AD_HOC
)
val recipients = (callLinks.map { it.recipient } + callEvents.map { it.peer }).toSet()
val jobs = recipients.take(10).map {
CallLinkPeekJob(it.id)
}
ApplicationDependencies.getJobManager().addAll(jobs)
}.subscribeOn(Schedulers.io())
}
}

View File

@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.service.webrtc.CallLinkPeekInfo
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId
/**
@@ -25,6 +26,7 @@ sealed class CallLogRow {
val record: CallLinkTable.CallLink,
val recipient: Recipient,
val searchQuery: String?,
val callLinkPeekInfo: CallLinkPeekInfo?,
override val id: Id = Id.CallLink(record.roomId)
) : CallLogRow()
@@ -38,6 +40,7 @@ sealed class CallLogRow {
val groupCallState: GroupCallState,
val children: Set<Long>,
val searchQuery: String?,
val callLinkPeekInfo: CallLinkPeekInfo?,
override val id: Id = Id.Call(children)
) : CallLogRow()

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.calls.log
import androidx.annotation.MainThread
import io.reactivex.rxjava3.core.Single
/**
* Encapsulates a single deletion action
@@ -13,19 +14,13 @@ class CallLogStagedDeletion(
private var isCommitted = false
fun isStagedForDeletion(id: CallLogRow.Id): Boolean {
return stateSnapshot.contains(id)
}
/**
* Returns a Single<Int> which contains the number of failed call-link revocations.
*/
@MainThread
fun cancel() {
isCommitted = true
}
@MainThread
fun commit() {
fun commit(): Single<Int> {
if (isCommitted) {
return
return Single.just(0)
}
isCommitted = true
@@ -35,10 +30,19 @@ class CallLogStagedDeletion(
.flatten()
.toSet()
if (stateSnapshot.isExclusionary()) {
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).subscribe()
val callLinkIds = stateSnapshot.selected()
.filterIsInstance<CallLogRow.Id.CallLink>()
.map { it.roomId }
.toSet()
return if (stateSnapshot.isExclusionary()) {
repository.deleteAllCallLogsExcept(callRowIds, filter == CallLogFilter.MISSED).andThen(
repository.deleteAllCallLinksExcept(callRowIds, callLinkIds)
)
} else {
repository.deleteSelectedCallLogs(callRowIds).subscribe()
repository.deleteSelectedCallLogs(callRowIds).andThen(
repository.deleteSelectedCallLinks(callRowIds, callLinkIds)
)
}
}
}

View File

@@ -1,17 +1,24 @@
package org.thoughtcrime.securesms.calls.log
import android.annotation.SuppressLint
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.core.BackpressureStrategy
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Maybe
import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.paging.ObservablePagedData
import org.signal.paging.PagedData
import org.signal.paging.PagingConfig
import org.signal.paging.ProxyPagingController
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.rx.RxStore
import java.util.concurrent.TimeUnit
/**
* ViewModel for call log management.
@@ -31,12 +38,14 @@ class CallLogViewModel(
val controller = ProxyPagingController<CallLogRow.Id>()
val data: Flowable<MutableList<CallLogRow?>> = pagedData.switchMap { it.data.toFlowable(BackpressureStrategy.LATEST) }
val selectedAndStagedDeletion: Flowable<Pair<CallLogSelectionState, CallLogStagedDeletion?>> = callLogStore
.stateFlowable
.map { it.selectionState to it.stagedDeletion }
val selected: Flowable<CallLogSelectionState> = callLogStore.stateFlowable.map { it.selectionState }
private val _isEmpty: BehaviorProcessor<Boolean> = BehaviorProcessor.createDefault(false)
val isEmpty: Boolean get() = _isEmpty.value ?: false
val totalCount: Flowable<Int> = Flowable.combineLatest(distinctQueryFilterPairs, data) { a, _ -> a }
.map { (query, filter) -> callLogRepository.getCallsCount(query, filter) }
.doOnNext { _isEmpty.onNext(it <= 0) }
val selectionStateSnapshot: CallLogSelectionState
get() = callLogStore.state.selectionState
@@ -70,10 +79,25 @@ class CallLogViewModel(
disposables += callLogRepository.listenForChanges().subscribe {
controller.onDataInvalidated()
}
if (FeatureFlags.adHocCalling()) {
disposables += Observable
.interval(30, TimeUnit.SECONDS, Schedulers.computation())
.flatMapCompletable { callLogRepository.peekCallLinks() }
.subscribe()
disposables += ApplicationDependencies
.getSignalCallManager()
.peekInfoCache
.observeOn(Schedulers.computation())
.distinctUntilChanged()
.subscribe {
controller.onDataInvalidated()
}
}
}
override fun onCleared() {
commitStagedDeletion()
disposables.dispose()
}
@@ -96,49 +120,52 @@ class CallLogViewModel(
}
@MainThread
fun stageCallDeletion(call: CallLogRow) {
callLogStore.state.stagedDeletion?.commit()
callLogStore.update {
it.copy(
stagedDeletion = CallLogStagedDeletion(
it.filter,
CallLogSelectionState.empty().toggle(call.id),
callLogRepository
)
)
}
fun stageCallDeletion(call: CallLogRow): CallLogStagedDeletion {
return CallLogStagedDeletion(
callLogStore.state.filter,
CallLogSelectionState.empty().toggle(call.id),
callLogRepository
)
}
@MainThread
fun stageSelectionDeletion() {
callLogStore.state.stagedDeletion?.commit()
callLogStore.update {
it.copy(
stagedDeletion = CallLogStagedDeletion(
it.filter,
it.selectionState,
callLogRepository
)
)
}
fun stageSelectionDeletion(): CallLogStagedDeletion {
return CallLogStagedDeletion(
callLogStore.state.filter,
callLogStore.state.selectionState,
callLogRepository
)
}
fun commitStagedDeletion() {
callLogStore.state.stagedDeletion?.commit()
fun stageDeleteAll(): CallLogStagedDeletion {
callLogStore.update {
it.copy(
stagedDeletion = null
selectionState = CallLogSelectionState.empty()
)
}
return CallLogStagedDeletion(
callLogStore.state.filter,
CallLogSelectionState.selectAll(),
callLogRepository
)
}
fun cancelStagedDeletion() {
callLogStore.state.stagedDeletion?.cancel()
callLogStore.update {
it.copy(
stagedDeletion = null
)
}
@SuppressLint("CheckResult")
fun delete(stagedDeletion: CallLogStagedDeletion): Maybe<CallLogDeletionResult> {
return stagedDeletion.commit()
.doOnSubscribe {
clearSelected()
}
.map { failedRevocations ->
if (failedRevocations == 0) {
CallLogDeletionResult.Success
} else {
CallLogDeletionResult.FailedToRevoke(failedRevocations)
}
}
.onErrorReturn { CallLogDeletionResult.UnknownFailure(it) }
.toMaybe()
}
fun clearSelected() {
@@ -158,7 +185,6 @@ class CallLogViewModel(
private data class CallLogState(
val query: String? = null,
val filter: CallLogFilter = CallLogFilter.ALL,
val selectionState: CallLogSelectionState = CallLogSelectionState.empty(),
val stagedDeletion: CallLogStagedDeletion? = null
val selectionState: CallLogSelectionState = CallLogSelectionState.empty()
)
}

View File

@@ -8,6 +8,7 @@ import android.os.Bundle;
import android.text.Annotation;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
@@ -22,6 +23,7 @@ import android.view.MenuItem;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
@@ -71,6 +73,7 @@ public class ComposeText extends EmojiEditText {
@Nullable private InputPanel.MediaListener mediaListener;
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
@Nullable private InlineQueryChangedListener inlineQueryChangedListener;
@Nullable private StylingChangedListener stylingChangedListener;
public ComposeText(Context context) {
super(context);
@@ -191,6 +194,11 @@ public class ComposeText extends EmojiEditText {
setHintWithChecks(hint);
}
public void setDraftText(@Nullable CharSequence draftText) {
setText("");
append(draftText);
}
public void appendInvite(String invite) {
if (getText() == null) {
return;
@@ -216,6 +224,10 @@ public class ComposeText extends EmojiEditText {
mentionValidatorWatcher.setMentionValidator(mentionValidator);
}
public void setStylingChangedListener(@Nullable StylingChangedListener listener) {
stylingChangedListener = listener;
}
private boolean isLandscape() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
@@ -279,15 +291,11 @@ public class ComposeText extends EmojiEditText {
public boolean hasStyling() {
CharSequence trimmed = getTextTrimmed();
return FeatureFlags.textFormatting() && (trimmed instanceof Spanned) && MessageStyler.hasStyling((Spanned) trimmed);
return (trimmed instanceof Spanned) && MessageStyler.hasStyling((Spanned) trimmed);
}
public @Nullable BodyRangeList getStyling() {
if (FeatureFlags.textFormatting()) {
return MessageStyler.getStyling(getTextTrimmed());
} else {
return null;
}
return MessageStyler.getStyling(getTextTrimmed());
}
private void initialize() {
@@ -301,87 +309,57 @@ public class ComposeText extends EmojiEditText {
mentionValidatorWatcher = new MentionValidatorWatcher();
addTextChangedListener(mentionValidatorWatcher);
if (FeatureFlags.textFormatting()) {
spoilerRendererDelegate = new SpoilerRendererDelegate(this, true);
spoilerRendererDelegate = new SpoilerRendererDelegate(this, true);
addTextChangedListener(new ComposeTextStyleWatcher());
addTextChangedListener(new ComposeTextStyleWatcher());
setCustomSelectionActionModeCallback(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuItem copy = menu.findItem(android.R.id.copy);
MenuItem cut = menu.findItem(android.R.id.cut);
MenuItem paste = menu.findItem(android.R.id.paste);
int copyOrder = copy != null ? copy.getOrder() : 0;
int cutOrder = cut != null ? cut.getOrder() : 0;
int pasteOrder = paste != null ? paste.getOrder() : 0;
int largestOrder = Math.max(copyOrder, Math.max(cutOrder, pasteOrder));
setCustomSelectionActionModeCallback(new ActionMode.Callback() {
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuItem copy = menu.findItem(android.R.id.copy);
MenuItem cut = menu.findItem(android.R.id.cut);
MenuItem paste = menu.findItem(android.R.id.paste);
int copyOrder = copy != null ? copy.getOrder() : 0;
int cutOrder = cut != null ? cut.getOrder() : 0;
int pasteOrder = paste != null ? paste.getOrder() : 0;
int largestOrder = Math.max(copyOrder, Math.max(cutOrder, pasteOrder));
menu.add(0, R.id.edittext_bold, largestOrder, getContext().getString(R.string.TextFormatting_bold));
menu.add(0, R.id.edittext_italic, largestOrder, getContext().getString(R.string.TextFormatting_italic));
menu.add(0, R.id.edittext_strikethrough, largestOrder, getContext().getString(R.string.TextFormatting_strikethrough));
menu.add(0, R.id.edittext_monospace, largestOrder, getContext().getString(R.string.TextFormatting_monospace));
menu.add(0, R.id.edittext_spoiler, largestOrder, getContext().getString(R.string.TextFormatting_spoiler));
menu.add(0, R.id.edittext_bold, largestOrder, getContext().getString(R.string.TextFormatting_bold));
menu.add(0, R.id.edittext_italic, largestOrder, getContext().getString(R.string.TextFormatting_italic));
menu.add(0, R.id.edittext_strikethrough, largestOrder, getContext().getString(R.string.TextFormatting_strikethrough));
menu.add(0, R.id.edittext_monospace, largestOrder, getContext().getString(R.string.TextFormatting_monospace));
menu.add(0, R.id.edittext_spoiler, largestOrder, getContext().getString(R.string.TextFormatting_spoiler));
return true;
Editable text = getText();
if (text != null) {
int start = getSelectionStart();
int end = getSelectionEnd();
if (MessageStyler.hasStyling(text, start, end)) {
menu.add(0, R.id.edittext_clear_formatting, largestOrder, getContext().getString(R.string.TextFormatting_clear_formatting));
}
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
Editable text = getText();
if (text == null) {
return false;
}
if (item.getItemId() != R.id.edittext_bold &&
item.getItemId() != R.id.edittext_italic &&
item.getItemId() != R.id.edittext_strikethrough &&
item.getItemId() != R.id.edittext_monospace &&
item.getItemId() != R.id.edittext_spoiler) {
return false;
}
int start = getSelectionStart();
int end = getSelectionEnd();
CharSequence charSequence = text.subSequence(start, end);
SpannableString replacement = new SpannableString(charSequence);
Object style = null;
if (item.getItemId() == R.id.edittext_bold) {
style = MessageStyler.boldStyle();
} else if (item.getItemId() == R.id.edittext_italic) {
style = MessageStyler.italicStyle();
} else if (item.getItemId() == R.id.edittext_strikethrough) {
style = MessageStyler.strikethroughStyle();
} else if (item.getItemId() == R.id.edittext_monospace) {
style = MessageStyler.monoStyle();
} else if (item.getItemId() == R.id.edittext_spoiler) {
style = MessageStyler.spoilerStyle(MessageStyler.COMPOSE_ID, start, charSequence.length());
}
if (style != null) {
replacement.setSpan(style, 0, charSequence.length(), MessageStyler.SPAN_FLAGS);
}
clearComposingText();
text.replace(start, end, replacement);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
boolean handled = handleFormatText(item.getItemId());
if (handled) {
mode.finish();
return true;
}
return handled;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {}
});
}
@Override
public void onDestroyActionMode(ActionMode mode) {}
});
}
private void setHintWithChecks(@Nullable CharSequence newHint) {
@@ -559,6 +537,60 @@ public class ComposeText extends EmojiEditText {
return TIME_PATTERN.matcher(text.subSequence(startOfToken, endOfToken)).find();
}
public boolean isTextHighlighted() {
return getText() != null && getSelectionStart() < getSelectionEnd();
}
public boolean handleFormatText(@IdRes int id) {
Editable text = getText();
if (text == null) {
return false;
}
if (id != R.id.edittext_bold &&
id != R.id.edittext_italic &&
id != R.id.edittext_strikethrough &&
id != R.id.edittext_monospace &&
id != R.id.edittext_spoiler &&
id != R.id.edittext_clear_formatting)
{
return false;
}
int start = getSelectionStart();
int end = getSelectionEnd();
BodyRangeList.BodyRange.Style style = null;
if (id == R.id.edittext_bold) {
style = BodyRangeList.BodyRange.Style.BOLD;
} else if (id == R.id.edittext_italic) {
style = BodyRangeList.BodyRange.Style.ITALIC;
} else if (id == R.id.edittext_strikethrough) {
style = BodyRangeList.BodyRange.Style.STRIKETHROUGH;
} else if (id == R.id.edittext_monospace) {
style = BodyRangeList.BodyRange.Style.MONOSPACE;
} else if (id == R.id.edittext_spoiler) {
style = BodyRangeList.BodyRange.Style.SPOILER;
}
clearComposingText();
if (style != null) {
MessageStyler.toggleStyle(style, text, start, end);
} else {
MessageStyler.clearStyling(text, start, end);
}
Selection.setSelection(getText(), end);
if (stylingChangedListener != null) {
stylingChangedListener.onStylingChanged();
}
return true;
}
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
private static final String TAG = Log.tag(CommitContentListener.class);
@@ -605,4 +637,7 @@ public class ComposeText extends EmojiEditText {
void onCursorPositionChanged(int start, int end);
}
public interface StylingChangedListener {
void onStylingChanged();
}
}

View File

@@ -46,11 +46,17 @@ class ComposeTextStyleWatcher : TextWatcher {
try {
if (editStart < 0 || editEnd < 0 || editStart >= editEnd || (editStart == 0 && editEnd == s.length)) {
textSnapshotPriorToChange = null
return
}
val change = s.subSequence(editStart, editEnd)
if (change.isEmpty() || textSnapshotPriorToChange == null || (editEnd - editStart == 1 && !StringUtil.isVisuallyEmpty(change[0])) || TextUtils.equals(textSnapshotPriorToChange, change)) {
if (change.isEmpty() ||
textSnapshotPriorToChange == null ||
(editEnd - editStart == 1 && !StringUtil.isVisuallyEmpty(change[0])) ||
TextUtils.equals(textSnapshotPriorToChange, change) ||
editEnd - editStart > 1
) {
textSnapshotPriorToChange = null
return
}

View File

@@ -320,8 +320,14 @@ public class ConversationItemFooter extends ConstraintLayout {
} else if (MessageRecordUtil.isScheduled(messageRecord)) {
dateView.setText(DateUtils.getOnlyTimeString(getContext(), locale, ((MediaMmsMessageRecord) messageRecord).getScheduledDate()));
} else {
String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp());
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord instanceof MediaMmsMessageRecord && ((MediaMmsMessageRecord) messageRecord).isEditMessage()) {
long timestamp = messageRecord.getTimestamp();
if (messageRecord.isEditMessage()) {
if (displayMode == ConversationItemDisplayMode.EDIT_HISTORY) {
timestamp = messageRecord.getDateSent();
}
}
String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, timestamp);
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage()) {
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date);
}
dateView.setText(date);

View File

@@ -2,26 +2,32 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.LinearInterpolator;
import android.view.animation.RotateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.signal.core.util.logging.Log;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import org.signal.core.util.DimensionUnit;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class DeliveryStatusView extends FrameLayout {
public class DeliveryStatusView extends AppCompatImageView {
private static final String TAG = Log.tag(DeliveryStatusView.class);
private static final String STATE_KEY = "DeliveryStatusView.STATE";
private static final String ROOT_KEY = "DeliveryStatusView.ROOT";
private final RotateAnimation rotationAnimation;
private final ImageView pendingIndicator;
private final ImageView sentIndicator;
private final ImageView deliveredIndicator;
private final ImageView readIndicator;
private final int horizontalPadding = (int) DimensionUnit.DP.toPixels(2);
private RotateAnimation rotationAnimation;
private State state = State.NONE;
public DeliveryStatusView(Context context) {
this(context, null);
@@ -34,75 +40,157 @@ public class DeliveryStatusView extends FrameLayout {
public DeliveryStatusView(final Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
inflate(context, R.layout.delivery_status_view, this);
this.deliveredIndicator = findViewById(R.id.delivered_indicator);
this.sentIndicator = findViewById(R.id.sent_indicator);
this.pendingIndicator = findViewById(R.id.pending_indicator);
this.readIndicator = findViewById(R.id.read_indicator);
rotationAnimation = new RotateAnimation(0, 360f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
rotationAnimation.setInterpolator(new LinearInterpolator());
rotationAnimation.setDuration(1500);
rotationAnimation.setRepeatCount(Animation.INFINITE);
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DeliveryStatusView, 0, 0);
setTint(typedArray.getColor(R.styleable.DeliveryStatusView_iconColor, getResources().getColor(R.color.core_white)));
typedArray.recycle();
}
setNone();
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle stateBundle = (Bundle) state;
State s = State.fromCode(stateBundle.getInt(STATE_KEY, State.NONE.code));
switch (s) {
case NONE:
setNone();
break;
case PENDING:
setPending();
break;
case SENT:
setSent();
break;
case DELIVERED:
setDelivered();
break;
case READ:
setRead();
break;
}
Parcelable root = stateBundle.getParcelable(ROOT_KEY);
super.onRestoreInstanceState(root);
} else {
super.onRestoreInstanceState(state);
}
}
@Override
protected @Nullable Parcelable onSaveInstanceState() {
Parcelable root = super.onSaveInstanceState();
Bundle stateBundle = new Bundle();
stateBundle.putParcelable(ROOT_KEY, root);
stateBundle.putInt(STATE_KEY, state.code);
return stateBundle;
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
if (state == State.PENDING && rotationAnimation == null) {
final float pivotXValue;
if (ViewUtil.isLtr(this)) {
pivotXValue = (w - getPaddingEnd()) / 2f;
} else {
pivotXValue = ((w - getPaddingEnd()) / 2f) + getPaddingEnd();
}
final float pivotYValue = (h - getPaddingTop() - getPaddingBottom()) / 2f;
rotationAnimation = new RotateAnimation(0, 360f,
Animation.ABSOLUTE, pivotXValue,
Animation.ABSOLUTE, pivotYValue);
rotationAnimation.setInterpolator(new LinearInterpolator());
rotationAnimation.setDuration(1500);
rotationAnimation.setRepeatCount(Animation.INFINITE);
startAnimation(rotationAnimation);
}
}
@Override
public void clearAnimation() {
super.clearAnimation();
rotationAnimation = null;
}
public void setNone() {
this.setVisibility(View.GONE);
state = State.NONE;
clearAnimation();
setVisibility(View.GONE);
}
public boolean isPending() {
return pendingIndicator.getVisibility() == View.VISIBLE;
return state == State.PENDING;
}
public void setPending() {
this.setVisibility(View.VISIBLE);
pendingIndicator.setVisibility(View.VISIBLE);
pendingIndicator.startAnimation(rotationAnimation);
sentIndicator.setVisibility(View.GONE);
deliveredIndicator.setVisibility(View.GONE);
readIndicator.setVisibility(View.GONE);
state = State.PENDING;
setVisibility(View.VISIBLE);
ViewUtil.setPaddingStart(this, 0);
ViewUtil.setPaddingEnd(this, horizontalPadding);
setImageResource(R.drawable.ic_delivery_status_sending);
}
public void setSent() {
this.setVisibility(View.VISIBLE);
pendingIndicator.setVisibility(View.GONE);
pendingIndicator.clearAnimation();
sentIndicator.setVisibility(View.VISIBLE);
deliveredIndicator.setVisibility(View.GONE);
readIndicator.setVisibility(View.GONE);
state = State.SENT;
setVisibility(View.VISIBLE);
ViewUtil.setPaddingStart(this, horizontalPadding);
ViewUtil.setPaddingEnd(this, 0);
clearAnimation();
setImageResource(R.drawable.ic_delivery_status_sent);
}
public void setDelivered() {
this.setVisibility(View.VISIBLE);
pendingIndicator.setVisibility(View.GONE);
pendingIndicator.clearAnimation();
sentIndicator.setVisibility(View.GONE);
deliveredIndicator.setVisibility(View.VISIBLE);
readIndicator.setVisibility(View.GONE);
state = State.DELIVERED;
setVisibility(View.VISIBLE);
ViewUtil.setPaddingStart(this, horizontalPadding);
ViewUtil.setPaddingEnd(this, 0);
clearAnimation();
setImageResource(R.drawable.ic_delivery_status_delivered);
}
public void setRead() {
this.setVisibility(View.VISIBLE);
pendingIndicator.setVisibility(View.GONE);
pendingIndicator.clearAnimation();
sentIndicator.setVisibility(View.GONE);
deliveredIndicator.setVisibility(View.GONE);
readIndicator.setVisibility(View.VISIBLE);
state = State.READ;
setVisibility(View.VISIBLE);
ViewUtil.setPaddingStart(this, horizontalPadding);
ViewUtil.setPaddingEnd(this, 0);
clearAnimation();
setImageResource(R.drawable.ic_delivery_status_read);
}
public void setTint(int color) {
pendingIndicator.setColorFilter(color);
deliveredIndicator.setColorFilter(color);
sentIndicator.setColorFilter(color);
readIndicator.setColorFilter(color);
setColorFilter(color);
}
private enum State {
NONE(0),
PENDING(1),
SENT(2),
DELIVERED(3),
READ(4);
final int code;
State(int code) {
this.code = code;
}
static State fromCode(int code) {
for (State state : State.values()) {
if (state.code == code) {
return state;
}
}
return NONE;
}
}
}

View File

@@ -26,6 +26,9 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
private var inputId: Int? = null
private var input: Fragment? = null
val isInputShowing: Boolean
get() = input != null
lateinit var fragmentManager: FragmentManager
var listener: Listener? = null
@@ -34,10 +37,13 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
hideInput(resetKeyboardGuideline = false)
}
fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, toggled: (Boolean) -> Unit = { }) {
fun toggleInput(fragmentCreator: FragmentCreator, imeTarget: EditText, showSoftKeyOnHide: Boolean = false) {
if (fragmentCreator.id == inputId) {
hideInput(resetKeyboardGuideline = true)
toggled(false)
if (showSoftKeyOnHide) {
showSoftkey(imeTarget)
} else {
hideInput(resetKeyboardGuideline = true)
}
} else {
hideInput(resetKeyboardGuideline = false)
showInput(fragmentCreator, imeTarget)
@@ -55,6 +61,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
fragmentManager
.beginTransaction()
.replace(R.id.input_container, input!!)
.runOnCommit { (input as? InputFragment)?.show() }
.commit()
overrideKeyboardGuidelineWithPreviousHeight()
@@ -66,6 +73,7 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
private fun hideInput(resetKeyboardGuideline: Boolean) {
val inputHidden = input != null
input?.let {
(input as? InputFragment)?.hide()
fragmentManager
.beginTransaction()
.remove(it)
@@ -94,4 +102,9 @@ class InputAwareConstraintLayout @JvmOverloads constructor(
fun onInputShown()
fun onInputHidden()
}
interface InputFragment {
fun show()
fun hide()
}
}

View File

@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
import org.thoughtcrime.securesms.conversation.MessageStyler;
import org.thoughtcrime.securesms.conversation.VoiceNoteDraftView;
import org.thoughtcrime.securesms.database.DraftTable;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
@@ -396,6 +397,7 @@ public class InputPanel extends LinearLayout
public void enterEditMessageMode(@NonNull GlideRequests glideRequests, @NonNull ConversationMessage conversationMessageToEdit, boolean fromDraft) {
SpannableString textToEdit = conversationMessageToEdit.getDisplayBody(getContext());
if (!fromDraft) {
MessageStyler.convertSpoilersToComposeMode(textToEdit);
composeText.setText(textToEdit);
composeText.setSelection(textToEdit.length());
}

View File

@@ -55,9 +55,11 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
private val parentEndGuideline: Guideline? by lazy { findViewById(R.id.parent_end_guideline) }
private val keyboardGuideline: Guideline? by lazy { findViewById(R.id.keyboard_guideline) }
private val listeners: MutableList<KeyboardStateListener> = mutableListOf()
private val keyboardAnimator = KeyboardInsetAnimator()
private val displayMetrics = DisplayMetrics()
private var overridingKeyboard: Boolean = false
private var previousKeyboardHeight: Int = 0
init {
ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsetsCompat ->
@@ -74,6 +76,14 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
}
}
fun addKeyboardStateListener(listener: KeyboardStateListener) {
listeners += listener
}
fun removeKeyboardStateListener(listener: KeyboardStateListener) {
listeners.remove(listener)
}
private fun applyInsets(windowInsets: Insets, keyboardInsets: Insets) {
val isLtr = ViewUtil.isLtr(this)
@@ -96,6 +106,18 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
keyboardAnimator.endingGuidelineEnd = windowInsets.bottom
}
}
if (previousKeyboardHeight != keyboardInsets.bottom) {
listeners.forEach {
if (previousKeyboardHeight <= 0) {
it.onKeyboardShown()
} else {
it.onKeyboardHidden()
}
}
}
previousKeyboardHeight = keyboardInsets.bottom
}
protected fun overrideKeyboardGuidelineWithPreviousHeight() {
@@ -157,6 +179,11 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
private val Guideline?.guidelineEnd: Int
get() = if (this == null) 0 else (layoutParams as LayoutParams).guideEnd
interface KeyboardStateListener {
fun onKeyboardShown()
fun onKeyboardHidden()
}
/**
* Adjusts the [keyboardGuideline] to move with the IME keyboard opening or closing.
*/

View File

@@ -16,12 +16,16 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.calls.links.CallLinks;
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
@@ -166,9 +170,13 @@ public class LinkPreviewView extends FrameLayout {
spinner.setVisibility(GONE);
noPreview.setVisibility(GONE);
CallLinkRootKey callLinkRootKey = CallLinks.parseUrl(linkPreview.getUrl());
if (!Util.isEmpty(linkPreview.getTitle())) {
title.setText(linkPreview.getTitle());
title.setVisibility(VISIBLE);
} else if (callLinkRootKey != null) {
title.setText(R.string.Recipient_signal_call);
title.setVisibility(VISIBLE);
} else {
title.setVisibility(GONE);
}
@@ -176,6 +184,9 @@ public class LinkPreviewView extends FrameLayout {
if (showDescription && !Util.isEmpty(linkPreview.getDescription())) {
description.setText(linkPreview.getDescription());
description.setVisibility(VISIBLE);
} else if (callLinkRootKey != null) {
description.setText(R.string.LinkPreviewView__use_this_link_to_join_a_signal_call);
description.setVisibility(VISIBLE);
} else {
description.setVisibility(GONE);
}
@@ -205,7 +216,18 @@ public class LinkPreviewView extends FrameLayout {
if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail);
thumbnail.get().setImageResource(glideRequests, new ImageSlide(getContext(), linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION, false);
thumbnail.get().showDownloadText(false);
} else if (callLinkRootKey != null) {
thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail);
thumbnail.get().setImageDrawable(
glideRequests,
Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER
.getPhotoForCallLink()
.asDrawable(getContext(),
AvatarColorHash.forCallLink(callLinkRootKey.getKeyBytes()))
);
thumbnail.get().showDownloadText(false);
} else {
thumbnail.setVisibility(GONE);

View File

@@ -2,8 +2,11 @@ package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.core.content.withStyledAttributes
import com.google.android.material.card.MaterialCardView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.visible
/**
* A small card with a circular progress indicator in it. Usable in place
@@ -16,7 +19,25 @@ class ProgressCard @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : MaterialCardView(context, attrs) {
private val title: TextView
init {
inflate(context, R.layout.progress_card, this)
title = findViewById(R.id.progress_card_text)
if (attrs != null) {
context.withStyledAttributes(attrs, R.styleable.ProgressCard) {
setTitleText(getString(R.styleable.ProgressCard_progressCardTitle))
}
} else {
setTitleText(null)
}
}
fun setTitleText(titleText: String?) {
title.visible = !titleText.isNullOrEmpty()
title.text = titleText
}
}

View File

@@ -1,20 +1,42 @@
package org.thoughtcrime.securesms.components
import android.annotation.SuppressLint
import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import androidx.annotation.Discouraged
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.navArgs
import org.thoughtcrime.securesms.R
/**
* Displays a small progress spinner in a card view, as a non-cancellable dialog fragment.
*/
class ProgressCardDialogFragment : DialogFragment(R.layout.progress_card_dialog) {
class ProgressCardDialogFragment
@Discouraged("Use create() instead.")
constructor() : DialogFragment(R.layout.progress_card_dialog) {
companion object {
@SuppressLint("DiscouragedApi")
fun create(title: String? = null): ProgressCardDialogFragment {
return ProgressCardDialogFragment().apply {
arguments = ProgressCardDialogFragmentArgs.Builder(title).build().toBundle()
}
}
}
private val args: ProgressCardDialogFragmentArgs by navArgs()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
isCancelable = false
return super.onCreateDialog(savedInstanceState).apply {
this.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById<ProgressCard>(R.id.progress_card).setTitleText(args.title)
}
}

View File

@@ -125,6 +125,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
.signature(signature)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(DrawableTransitionOptions.withCrossFade())
.centerCrop()
.into(viewHolder.imageView);
viewHolder.imageView.setOnClickListener(v -> {

View File

@@ -28,6 +28,7 @@ import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.RequestOptions;
import org.signal.core.util.logging.Log;
import org.signal.glide.transforms.SignalDownsampleStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentTable;
@@ -454,6 +455,7 @@ public class ThumbnailView extends FrameLayout {
GlideRequest<Drawable> request = glideRequests.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.listener(listener);
if (animate) {
@@ -486,6 +488,7 @@ public class ThumbnailView extends FrameLayout {
GlideRequest<Drawable> request = glideRequests.load(model)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.placeholder(model.getPlaceholder())
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.transition(withCrossFade());
request = override(request, width, height);
@@ -554,6 +557,7 @@ public class ThumbnailView extends FrameLayout {
private GlideRequest<Drawable> buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) {
GlideRequest<Drawable> request = applySizing(glideRequests.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.transition(withCrossFade()));
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;

View File

@@ -201,7 +201,7 @@ public final class TransferControlView extends FrameLayout {
private String getDownloadText(@NonNull List<Slide> slides) {
if (slides.size() == 1) {
return slides.get(0).getContentDescription();
return slides.get(0).getContentDescription(getContext());
} else {
int downloadCount = Stream.of(slides).reduce(0, (count, slide) -> slide.getTransferState() != AttachmentTable.TRANSFER_PROGRESS_DONE ? count + 1 : count);
return getContext().getResources().getQuantityString(R.plurals.TransferControlView_n_items, downloadCount, downloadCount);

View File

@@ -62,6 +62,7 @@ abstract class WrapperDialogFragment : DialogFragment(R.layout.fragment_containe
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
findListener<WrapperDialogFragmentCallback>()?.onWrapperDialogFragmentDismissed()
}

View File

@@ -68,9 +68,10 @@ public class EmojiTextView extends AppCompatTextView {
private TextDirectionHeuristic textDirection;
private boolean isJumbomoji;
private boolean forceJumboEmoji;
private boolean renderSpoilers;
private MentionRendererDelegate mentionRendererDelegate;
private final SpoilerRendererDelegate spoilerRendererDelegate;
private MentionRendererDelegate mentionRendererDelegate;
private SpoilerRendererDelegate spoilerRendererDelegate;
public EmojiTextView(Context context) {
this(context, null);
@@ -90,6 +91,7 @@ public class EmojiTextView extends AppCompatTextView {
renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true);
measureLastLine = a.getBoolean(R.styleable.EmojiTextView_measureLastLine, false);
forceJumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
renderSpoilers = a.getBoolean(R.styleable.EmojiTextView_emoji_renderSpoilers, false);
a.recycle();
a = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.textSize });
@@ -99,7 +101,10 @@ public class EmojiTextView extends AppCompatTextView {
if (renderMentions) {
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.transparent_black_20));
}
spoilerRendererDelegate = new SpoilerRendererDelegate(this);
if (renderSpoilers) {
spoilerRendererDelegate = new SpoilerRendererDelegate(this);
}
textDirection = getLayoutDirection() == LAYOUT_DIRECTION_LTR ? TextDirectionHeuristics.FIRSTSTRONG_RTL : TextDirectionHeuristics.ANYRTL_LTR;
@@ -127,14 +132,16 @@ public class EmojiTextView extends AppCompatTextView {
}
}
private void drawSpecialRenderers(@NonNull Canvas canvas, @Nullable MentionRendererDelegate mentionDelegate, @NonNull SpoilerRendererDelegate spoilerDelegate) {
private void drawSpecialRenderers(@NonNull Canvas canvas, @Nullable MentionRendererDelegate mentionDelegate, @Nullable SpoilerRendererDelegate spoilerDelegate) {
int checkpoint = canvas.save();
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
try {
if (mentionDelegate != null) {
mentionDelegate.draw(canvas, (Spanned) getText(), getLayout());
}
spoilerDelegate.draw(canvas, (Spanned) getText(), getLayout());
if (spoilerDelegate != null) {
spoilerDelegate.draw(canvas, (Spanned) getText(), getLayout());
}
} finally {
canvas.restoreToCount(checkpoint);
}
@@ -431,7 +438,9 @@ public class EmojiTextView extends AppCompatTextView {
@Override
public void setTextColor(int color) {
super.setTextColor(color);
spoilerRendererDelegate.updateFromTextColor();
if (spoilerRendererDelegate != null) {
spoilerRendererDelegate.updateFromTextColor();
}
}
@Override

View File

@@ -105,7 +105,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
if (!isInitialised) initView();
setVisibility(VISIBLE);
if (keyboardListener != null) keyboardListener.onShown();
keyboardPagerFragment.show();
}
@@ -113,7 +112,6 @@ public class MediaKeyboard extends FrameLayout implements InputView {
public void hide(boolean immediate) {
setVisibility(GONE);
onCloseEmojiSearchInternal(false);
if (keyboardListener != null) keyboardListener.onHidden();
Log.i(TAG, "hide()");
keyboardPagerFragment.hide();
}

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components.identity;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -12,7 +11,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
@@ -24,9 +22,10 @@ public class UnverifiedBannerView extends LinearLayout {
private static final String TAG = Log.tag(UnverifiedBannerView.class);
private View container;
private TextView text;
private ImageView closeButton;
private View container;
private TextView text;
private ImageView closeButton;
private OnHideListener onHideListener;
public UnverifiedBannerView(Context context) {
super(context);
@@ -38,13 +37,11 @@ public class UnverifiedBannerView extends LinearLayout {
initialize();
}
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
public UnverifiedBannerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public UnverifiedBannerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
@@ -82,16 +79,27 @@ public class UnverifiedBannerView extends LinearLayout {
});
}
public void setOnHideListener(@Nullable OnHideListener onHideListener) {
this.onHideListener = onHideListener;
}
public void hide() {
if (onHideListener != null && onHideListener.onHide()) {
return;
}
setVisibility(View.GONE);
}
public interface DismissListener {
public void onDismissed(List<IdentityRecord> unverifiedIdentities);
void onDismissed(List<IdentityRecord> unverifiedIdentities);
}
public interface ClickListener {
public void onClicked(List<IdentityRecord> unverifiedIdentities);
void onClicked(List<IdentityRecord> unverifiedIdentities);
}
public interface OnHideListener {
boolean onHide();
}
}

View File

@@ -1,9 +1,7 @@
package org.thoughtcrime.securesms.components.location;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -11,16 +9,20 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.MapView;
import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import java.util.concurrent.ExecutionException;
public class SignalMapView extends LinearLayout {
private MapView mapView;
@@ -53,42 +55,50 @@ public class SignalMapView extends LinearLayout {
public ListenableFuture<Bitmap> display(final SignalPlace place) {
final SettableFuture<Bitmap> future = new SettableFuture<>();
this.mapView.onCreate(null);
this.mapView.onResume();
this.mapView.setVisibility(View.VISIBLE);
this.imageView.setVisibility(View.GONE);
this.mapView.getMapAsync(new OnMapReadyCallback() {
this.textView.setText(place.getDescription());
snapshot(place, mapView).addListener(new ListenableFuture.Listener<Bitmap>() {
@Override
public void onMapReady(final GoogleMap googleMap) {
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(place.getLatLong(), 13));
googleMap.addMarker(new MarkerOptions().position(place.getLatLong()));
googleMap.setBuildingsEnabled(true);
googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
googleMap.getUiSettings().setAllGesturesEnabled(false);
googleMap.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() {
@Override
public void onMapLoaded() {
googleMap.snapshot(new GoogleMap.SnapshotReadyCallback() {
@Override
public void onSnapshotReady(Bitmap bitmap) {
future.set(bitmap);
imageView.setImageBitmap(bitmap);
imageView.setVisibility(View.VISIBLE);
mapView.setVisibility(View.GONE);
mapView.onPause();
mapView.onDestroy();
}
});
}
});
public void onSuccess(Bitmap result) {
future.set(result);
imageView.setImageBitmap(result);
imageView.setVisibility(View.VISIBLE);
}
@Override
public void onFailure(ExecutionException e) {
future.setException(e);
}
});
this.textView.setText(place.getDescription());
return future;
}
public static ListenableFuture<Bitmap> snapshot(final LatLng place, @NonNull final MapView mapView) {
final SettableFuture<Bitmap> future = new SettableFuture<>();
mapView.onCreate(null);
mapView.onResume();
mapView.setVisibility(View.VISIBLE);
mapView.getMapAsync(googleMap -> {
googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(place, 13));
googleMap.addMarker(new MarkerOptions().position(place));
googleMap.setBuildingsEnabled(true);
googleMap.setMapType(GoogleMap.MAP_TYPE_NORMAL);
googleMap.getUiSettings().setAllGesturesEnabled(false);
googleMap.setOnMapLoadedCallback(() -> googleMap.snapshot(bitmap -> {
future.set(bitmap);
mapView.setVisibility(View.GONE);
mapView.onPause();
mapView.onDestroy();
}));
});
return future;
}
public static ListenableFuture<Bitmap> snapshot(final SignalPlace place, @NonNull final MapView mapView) {
return snapshot(place.getLatLong(), mapView);
}
}

View File

@@ -12,10 +12,14 @@ import androidx.annotation.NonNull;
import androidx.core.graphics.drawable.DrawableCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.spoiler.SpoilerAnnotation;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
/**
* Encapsulates the logic for determining the type of mention rendering needed (single vs multi-line) and then
* passing that information to the appropriate {@link MentionRenderer}.
@@ -57,6 +61,12 @@ public class MentionRendererDelegate {
if (MentionAnnotation.isMentionAnnotation(annotation)) {
int spanStart = text.getSpanStart(annotation);
int spanEnd = text.getSpanEnd(annotation);
List<Annotation> spoilerAnnotations = SpoilerAnnotation.getSpoilerAnnotations(text, spanStart, spanEnd, true);
if (Util.hasItems(spoilerAnnotations)) {
continue;
}
int startLine = layout.getLineForOffset(spanStart);
int endLine = layout.getLineForOffset(spanEnd);

View File

@@ -1,13 +1,12 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import org.thoughtcrime.securesms.R
class BubbleOptOutReminder(context: Context) : Reminder(null, context.getString(R.string.BubbleOptOutTooltip__description)) {
class BubbleOptOutReminder : Reminder(R.string.BubbleOptOutTooltip__description) {
init {
addAction(Action(context.getString(R.string.BubbleOptOutTooltip__turn_off), R.id.reminder_action_turn_off))
addAction(Action(context.getString(R.string.BubbleOptOutTooltip__not_now), R.id.reminder_action_not_now))
addAction(Action(R.string.BubbleOptOutTooltip__turn_off, R.id.reminder_action_bubble_turn_off))
addAction(Action(R.string.BubbleOptOutTooltip__not_now, R.id.reminder_action_bubble_not_now))
}
override fun isDismissable(): Boolean {

View File

@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import kotlin.time.Duration.Companion.days
@@ -8,12 +7,12 @@ import kotlin.time.Duration.Companion.days
/**
* Reminder shown when CDS is in a permanent error state, preventing us from doing a sync.
*/
class CdsPermanentErrorReminder(context: Context) : Reminder(null, context.getString(R.string.reminder_cds_permanent_error_body)) {
class CdsPermanentErrorReminder : Reminder(R.string.reminder_cds_permanent_error_body) {
init {
addAction(
Action(
context.getString(R.string.reminder_cds_permanent_error_learn_more),
R.string.reminder_cds_permanent_error_learn_more,
R.id.reminder_action_cds_permanent_error_learn_more
)
)

View File

@@ -1,18 +1,17 @@
package org.thoughtcrime.securesms.components.reminder
import android.content.Context
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
/**
* Reminder shown when CDS is rate-limited, preventing us from temporarily doing a refresh.
*/
class CdsTemporyErrorReminder(context: Context) : Reminder(null, context.getString(R.string.reminder_cds_warning_body)) {
class CdsTemporaryErrorReminder : Reminder(R.string.reminder_cds_warning_body) {
init {
addAction(
Action(
context.getString(R.string.reminder_cds_warning_learn_more),
R.string.reminder_cds_warning_learn_more,
R.id.reminder_action_cds_temporary_error_learn_more
)
)

View File

@@ -19,10 +19,9 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
@SuppressLint("BatteryLife")
public class DozeReminder extends Reminder {
@RequiresApi(api = Build.VERSION_CODES.M)
@RequiresApi(api = 23)
public DozeReminder(@NonNull final Context context) {
super(context.getString(R.string.DozeReminder_optimize_for_missing_play_services),
context.getString(R.string.DozeReminder_this_device_does_not_support_play_services_tap_to_disable_system_battery));
super(R.string.DozeReminder_optimize_for_missing_play_services, R.string.DozeReminder_this_device_does_not_support_play_services_tap_to_disable_system_battery);
setOkListener(v -> {
TextSecurePreferences.setPromptedOptimizeDoze(context, true);
@@ -40,5 +39,4 @@ public class DozeReminder extends Reminder {
Build.VERSION.SDK_INT >= 23 &&
!((PowerManager)context.getSystemService(Context.POWER_SERVICE)).isIgnoringBatteryOptimizations(context.getPackageName());
}
}

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