Compare commits

..

1248 Commits

Author SHA1 Message Date
Greyson Parrelli f0d40685df Bump version to 5.28.9 2022-01-03 19:40:54 -05:00
Greyson Parrelli 4cbed24244 Updated language translations. 2022-01-03 19:40:22 -05:00
Greyson Parrelli 0d0c74f358 Fix in-memory message updates.
We can also switch to using the message-specific update route for
receipts too.
2022-01-03 18:47:11 -05:00
Cody Henthorne 0dd2397fb4 Fix notification profile manually enabled and scheduled bug. 2022-01-03 18:47:11 -05:00
Cody Henthorne 3781e1dd60 Add notification profile information to debug log. 2022-01-03 18:47:11 -05:00
Cody Henthorne ae40a65924 Fix MediaRecorder crash when no data captured. 2022-01-03 18:47:11 -05:00
Greyson Parrelli 8968ef1b85 Remove routine GV1 migration checks.
We will now only migrate GV1 groups upon opening them.
2022-01-03 14:00:39 -05:00
Greyson Parrelli 25ab9a5ad6 Fix older database migrations that may recursively open database. 2022-01-03 11:59:06 -05:00
Greyson Parrelli 5c27842a01 Include queue in job logs. 2022-01-03 11:28:10 -05:00
Art Chaidarun 49a1a4a123 Fix large images sometimes not respecting EXIF orientation.
Fixes #11614
2022-01-03 10:31:04 -05:00
Alan Evans ac90eeb42f Protoc update to 3.18.0
Windows and M1 gradle verification sha256 values.

Using: gradlew --write-verification-metadata sha256 help

But aapt2 artifact had to be manually added.

Fixes #11871, Fixes #11878, Closes #11877
2022-01-03 10:17:17 -05:00
Greyson Parrelli 302e653d2f Only put message in the media queue if it has an attachment. 2022-01-03 09:03:12 -05:00
Greyson Parrelli 3d6ffe25f0 Bump version to 5.28.8 2021-12-22 14:17:26 -05:00
Greyson Parrelli 363eb22462 Updated language translations. 2021-12-22 14:17:26 -05:00
Cody Henthorne b04ae3a8b3 Use MediaRecorder for voice notes on capable devices.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2021-12-22 14:17:26 -05:00
Rashad Sookram e6451db888 Revert "Use localized AM/PM strings."
This reverts commit bdd48629c6.
2021-12-22 14:17:26 -05:00
Greyson Parrelli 2bbceaabd3 Fix possible crash for unregistered users. 2021-12-22 14:17:26 -05:00
Greyson Parrelli fefbf595cd Disable the notification profiles megaphone for fresh installs. 2021-12-22 14:17:26 -05:00
Rashad Sookram 85b3947150 Fix line wrap for EmojiTextViews with ImageSpans. 2021-12-22 14:17:26 -05:00
Alex Hart a0d70a955a Generate request credential presentation before submitting subscription job. 2021-12-22 14:17:26 -05:00
Greyson Parrelli a5c2595796 Bump version to 5.28.7 2021-12-21 16:41:04 -05:00
Greyson Parrelli 4193f7bcbd Updated language translations. 2021-12-21 16:35:36 -05:00
Greyson Parrelli 5102f5215c Use proper processing queue for sync messages. 2021-12-21 16:15:47 -05:00
Rashad Sookram bdd48629c6 Use localized AM/PM strings. 2021-12-21 16:15:47 -05:00
Alex Hart fde1e5ab77 Prevent KeepAlive Job from alerting user on 409 error. 2021-12-21 16:15:47 -05:00
Greyson Parrelli 46dd7f8a06 Improve performance of processing read syncs. 2021-12-21 16:15:47 -05:00
Alex Hart 282639469d Ensure unique names are used when saving batches of files. 2021-12-21 16:15:47 -05:00
Greyson Parrelli bad1cc1571 Improve logging around message processing. 2021-12-21 10:48:00 -05:00
Greyson Parrelli 3757449b8f Bump version to 5.28.6 2021-12-20 13:43:05 -05:00
Greyson Parrelli 0af65d1367 Updated language translations. 2021-12-20 13:42:47 -05:00
Cody Henthorne adcb1bae13 Fix bug with schedule end being set to midnight. 2021-12-20 13:31:18 -05:00
Cody Henthorne b69ffe4e15 Fix crash when processing group updates with remapped members. 2021-12-20 13:31:18 -05:00
Cody Henthorne 8c34357cc6 Fix phone number format crash. 2021-12-20 13:31:18 -05:00
Greyson Parrelli a17fd447a7 Add logging to help diagnose ratelimit issues. 2021-12-20 13:31:18 -05:00
Cody Henthorne eaf72b194f Include exception message in stack trace. 2021-12-20 13:31:18 -05:00
Jim Gustafson 09a3391761 Update to RingRTC v2.16.1 2021-12-20 13:31:18 -05:00
Cody Henthorne e0e25da6a9 Fix empty media send illegal state exception. 2021-12-20 13:31:18 -05:00
Cody Henthorne 90d4069d0a Fix crash and UI issues in call screen. 2021-12-20 13:31:18 -05:00
Cody Henthorne 0dcae81dba Fix share selection crash. 2021-12-20 13:31:18 -05:00
Alex Hart 9177f5637a Add support for manual cancellation proto field. 2021-12-20 13:31:18 -05:00
Cody Henthorne 5918227bff Add PagingMappingAdapter and convert GiphyMp4Adapter. 2021-12-20 13:31:18 -05:00
Rashad Sookram dd79688f48 Fix ellipsis bug in group names with emoji.
The bug was that system emoji and emoji from `EmojiSpan` had different
sizes. The `View` was being measured based off the `EmojiSpan`
(smaller), but ellipsizing was based off the size of system emoji.

Fixes #11772
2021-12-20 13:31:18 -05:00
Cody Henthorne dbce4be31d Refactor MappingAdapter code into package. 2021-12-20 13:31:18 -05:00
Cody Henthorne 4275877b47 Fix crash in media preview when scrolling and receiving new media. 2021-12-20 13:31:18 -05:00
Rashad Sookram 11221315e4 Add workaround for message line wrap bug. 2021-12-20 13:31:18 -05:00
Cody Henthorne a4f44a96fd Fix illegal argument navigation exceptions. 2021-12-20 13:31:18 -05:00
Cody Henthorne ba54051f8c Fix duplicate notification channels being created. 2021-12-20 13:31:18 -05:00
Cody Henthorne 130b796564 Fix registration crash. 2021-12-20 13:31:18 -05:00
Cody Henthorne a15ba60252 Fix navigation bug after deleting notification profile. 2021-12-20 13:31:18 -05:00
Cody Henthorne 8014a70134 Show backup progress as a percentage. 2021-12-20 13:31:18 -05:00
Alex Hart 4f73e36d72 Always generate a unique filename when saving files. 2021-12-20 13:31:18 -05:00
Alex Hart 68bd9c6e1e Refactor ShareableGroupLinkDialogFragment into a normal Fragment.
Co-authored-by: Rashad Sookram <rashad@signal.org>
2021-12-20 13:31:17 -05:00
Alex Hart 20d2c43356 Migrate identity verification activity to fragment. 2021-12-16 14:48:25 -05:00
Rashad Sookram b94624fd5a Treat SVGs as document attachments. 2021-12-16 14:48:25 -05:00
Rashad Sookram 4ae129d2af Use Gradle dependency verification.
Generated by running:
./gradlew --write-verification-metadata sha256 qa --rerun-tasks
2021-12-16 14:48:25 -05:00
Rashad Sookram 158505c8a8 Move Glide annotation processing out of the main module. 2021-12-16 14:48:25 -05:00
Rashad Sookram c98fd1a452 Speed up Gradle qa task. 2021-12-16 14:48:25 -05:00
Alex Hart c6d0ef218a Bump version to 5.28.5 2021-12-14 10:46:56 -04:00
Alex Hart ba5b3e01f2 Updated language translations. 2021-12-14 10:46:27 -04:00
Rashad Sookram e84ae83c28 Fix unverified banner theme. 2021-12-14 09:17:11 -05:00
Cody Henthorne 33eeca9e3e Bump version to 5.28.4 2021-12-13 12:44:14 -05:00
Cody Henthorne 6288dc19e9 Updated language translations. 2021-12-13 12:37:59 -05:00
Cody Henthorne b9ba1a3568 Fix notification schedule bug. 2021-12-13 12:34:53 -05:00
Cody Henthorne 93270b90df Fix 24hr time format bug on older OSes. 2021-12-13 10:16:27 -05:00
Cody Henthorne a0235cbc6c Bump version to 5.28.3 2021-12-10 13:20:37 -05:00
Cody Henthorne 28f0724d90 Updated language translations. 2021-12-10 13:09:11 -05:00
Cody Henthorne 08a305cb0f Fix bug when changing schedule end time and end is before start. 2021-12-10 13:05:17 -05:00
Greyson Parrelli 50d2faf381 Fix bug in reaction bottom sheet data observation. 2021-12-10 13:05:17 -05:00
Greyson Parrelli 49b9d5c3aa Use borderless ripple for emoji search buttons. 2021-12-10 13:05:17 -05:00
Alex Hart 7385112115 Apply selected state to custom boost field instead of relying on focus. 2021-12-10 13:05:17 -05:00
Alex Hart 3feb73789d Set custom amount on focus, do not clear on loss of focus. 2021-12-10 13:05:17 -05:00
Cody Henthorne b80c844a0b Fix schedule edit day backgrounds for older devices. 2021-12-10 13:05:17 -05:00
Alex Hart 755a25519a Add explicit log-line with status code for redemption success. 2021-12-10 09:37:06 -04:00
Cody Henthorne bbb9eab148 Bump version to 5.28.2 2021-12-09 15:09:00 -05:00
Cody Henthorne c8e62e5f60 Updated language translations. 2021-12-09 15:01:48 -05:00
Cody Henthorne 19818443ff Sort profiles by created at descending when shown in a list. 2021-12-09 14:58:08 -05:00
Cody Henthorne c30a43ef45 Fix conflict when manually enabled an older profile with a schedule overlap with a newer profile. 2021-12-09 14:58:08 -05:00
Alex Hart 76539ff0f2 Remove focus shade when displaying reactions bottom sheet. 2021-12-09 14:58:08 -05:00
Alex Hart 39f4ca10ef Fix crash when leaving groups during account deletion. 2021-12-09 14:58:08 -05:00
Cody Henthorne 3d77ce0d57 Only initially show up to 5 members on profile details. 2021-12-09 14:58:08 -05:00
Cody Henthorne 3b9cfc8e5a Fix unable to select contact from list bug. 2021-12-09 14:58:08 -05:00
Cody Henthorne 761d70851c Show recents and groups in add to notification profile. 2021-12-09 14:58:08 -05:00
Cody Henthorne 4c28619010 Update Schedule UI and use locale specific first day of week. 2021-12-09 14:58:08 -05:00
Greyson Parrelli 884710fc30 Fix crash in ProfileSharingUpdateMigrationJob.
A typo introduced in the java -> kt conversion.

Fixes #11824
2021-12-09 14:58:08 -05:00
Greyson Parrelli 2e96042578 Fix SMS contacts not showing in contact search results.
Introduced a typo in the java -> kt conversion.
2021-12-09 14:58:08 -05:00
Rashad Sookram 5b49be47f9 Fix conversation select menu options not showing.
Also fix unpin option being shown incorrectly.
2021-12-09 14:58:08 -05:00
Cody Henthorne d6a42daef7 Change copy for notification profiles setting to clarify feature. 2021-12-09 14:58:08 -05:00
Cody Henthorne d5679ef95f Fix notification profile selection UI bugs. 2021-12-09 14:58:08 -05:00
Cody Henthorne 60e54fb2af Bump version to 5.28.1 2021-12-08 20:56:43 -05:00
Cody Henthorne 575e00dcf8 Updated language translations. 2021-12-08 20:52:40 -05:00
Cody Henthorne a8a104242a Fix various issues regarding Notification Profile scheduling.
- Timezone conversion when detecting scheduled profile
- Not automatically enabling a scheduled profile on creation regardless
  of when other profiles were enabled/disabled
2021-12-08 20:49:45 -05:00
Cody Henthorne 372b0d9f2b Fix notification profiles megaphone typo. 2021-12-08 16:02:55 -05:00
Cody Henthorne d3b061c6a4 Bump version to 5.28.0 2021-12-08 15:35:45 -05:00
Cody Henthorne 1086749244 Updated language translations. 2021-12-08 15:28:14 -05:00
Rashad Sookram c16115f71a Fix color of close button on normal reminders.
This change also prevents the title from overlapping with the close
button.
2021-12-08 15:07:22 -05:00
Cody Henthorne 6c608e955e Add Notification profiles. 2021-12-08 15:07:22 -05:00
Cody Henthorne 31e0696395 Use long in exporter. 2021-12-08 15:07:22 -05:00
Alex Hart 99f43b997c Add logging for selected badge density. 2021-12-08 15:07:22 -05:00
Rashad Sookram b6f84dfa16 Add tooltip to opt-out of bubbles. 2021-12-08 15:07:22 -05:00
Ehren Kret 63c98e92f2 Remove old chat server hostnames. 2021-12-08 15:07:22 -05:00
Alex Hart 34d4c910f7 Only notify voice note progress handler if activity is not null. 2021-12-08 15:07:22 -05:00
Jordan Rose 7dc3454b37 Use low-bandwidth mode if call is believed to be on cellular 2021-12-08 15:06:44 -05:00
Greyson Parrelli 60047aecb9 Keep JobManagerFactories in alphabetical order. 2021-12-06 16:02:51 -05:00
Rashad Sookram 738c5db7c2 Fix post loop when View is GONE. 2021-12-06 15:54:40 -05:00
Greyson Parrelli c93457402c Store your own PNI. 2021-12-06 12:18:42 -05:00
Jim Gustafson 0a84f7f505 Update to RingRTC v2.16.0 2021-12-06 10:38:18 -05:00
Alex Hart c91a1e13d9 Fix checks on conversation list and recipient row. 2021-12-03 18:10:01 -05:00
Greyson Parrelli a346dd33d9 Do not allow recipient merges to remove your own E164.
This would only happen in some niche change number cases where an
unregistered device would continue to send sealed sender messages to you
using your old number.
2021-12-03 18:10:01 -05:00
Rashad Sookram 398fdd84b9 Ensure all conversations are loaded before selecting all.
They might not be loaded yet due to pagination.
2021-12-03 18:10:01 -05:00
Cody Henthorne 2c5f57486c Enable Change Number via FeatureFlags. 2021-12-03 18:10:01 -05:00
Cody Henthorne 0fa3b2f8f9 Revert "Enable Change Number."
This reverts commit 97642f555e8a1cb89c7bae209b218a1c63532ada.
2021-12-03 18:10:01 -05:00
Greyson Parrelli 88aa67b847 Fix typo that caused some invalid ContactRecords to slip through.
We were doing .equals() between an ACI and a UUID, so it was always
returning false. Fixed by swithing to the proper check.
2021-12-03 18:10:01 -05:00
Alex Hart 6154ff36c1 Keep around info logs from SubscriptionKeepAlive job. 2021-12-03 18:10:01 -05:00
Greyson Parrelli c0a83e7956 Migrate RecipientDatabase to Kotlin. 2021-12-03 18:10:01 -05:00
Cody Henthorne 59ad8bf76a Fix deadlock with web socket health monitor. 2021-12-03 18:10:01 -05:00
Greyson Parrelli 4ba4df706e Properly handle LockedException during PIN guess. 2021-12-03 18:10:01 -05:00
Cody Henthorne d48632d09d Enable Change Number. 2021-12-03 18:10:01 -05:00
Greyson Parrelli 8cb4cc5ac3 Disable scrolling while the context menu is showing. 2021-12-03 18:10:01 -05:00
Jim Gustafson 83d3e56dcf Update to RingRTC v2.15.0
Also adds audio processing option for internal users.
2021-12-03 18:10:01 -05:00
Greyson Parrelli deddb4f77d Use new endpoint for determining if ACI is a registered user. 2021-12-03 18:09:52 -05:00
Rashad Sookram 479ab10578 Allow dialog buttons to span two lines.
... so that buttons with long strings don't get truncated.

Fixes #11793
2021-12-03 18:09:52 -05:00
Alex Hart 321c84583b Ensure user leaves groups before deleting account. 2021-12-03 18:09:52 -05:00
Rashad Sookram 3242d97c75 Fix crash when opening contacts app when none are present.
This can also happen when the system contacts app has been disabled.

Fixes #11794
2021-12-03 18:09:52 -05:00
Greyson Parrelli 562a255478 Update libsignal-client to 0.11.0 2021-12-03 18:09:52 -05:00
Alex Hart a6a70f23e9 Bump version to 5.27.13 2021-12-03 18:04:14 -04:00
Alex Hart e3638791d9 Revert "Improve text entry for boosts."
This reverts commit 84833c9ad3.
2021-12-03 17:58:03 -04:00
Alex Hart 8501fdffc6 Bump version to 5.27.12 2021-12-03 14:06:14 -04:00
Alex Hart 4c4cbecd85 Updated language translations. 2021-12-03 14:05:53 -04:00
Alex Hart 84833c9ad3 Improve text entry for boosts. 2021-12-03 13:55:42 -04:00
Alex Hart 131a400921 Bump version to 5.27.11 2021-12-02 17:23:55 -04:00
Alex Hart 54d937d036 Improve text entry for donations. 2021-12-02 17:20:21 -04:00
Alex Hart a8659bf8e5 Bump version to 5.27.10 2021-12-02 13:55:21 -04:00
Alex Hart dfe78cdae6 Updated language translations. 2021-12-02 13:55:21 -04:00
Alex Hart 9434894dff Add additional logging around boost amounts. 2021-12-02 13:55:21 -04:00
Alex Hart d028165b51 Only display donate megaphone for those with Play Services. 2021-12-02 13:29:55 -04:00
Alex Hart 8771dbf49f Bump version to 5.27.9 2021-12-01 15:59:52 -04:00
Alex Hart 4615b0d32d Updated language translations. 2021-12-01 15:59:23 -04:00
Alex Hart f9a2208832 Fix non-standard numeral entry. 2021-12-01 15:53:55 -04:00
Greyson Parrelli b981ac4fe4 Bump version to 5.27.8 2021-11-30 17:38:07 -05:00
Greyson Parrelli a3219348b6 Updated language translations. 2021-11-30 17:38:07 -05:00
Greyson Parrelli a35a35cee8 Make the donation megaphone flag hot-swappable. 2021-11-30 17:38:07 -05:00
Greyson Parrelli 85f1f27b13 Remove donor badges warning in comment. 2021-11-30 17:17:44 -05:00
Alex Hart 388c91410b Fix vector related crash with Button strip on Kitkat. 2021-11-30 17:13:14 -05:00
Alex Hart 3c0afe4b24 Fix recipient bottom sheet buttons on devices with very large font selected. 2021-11-30 17:13:14 -05:00
Alex Hart ae1f834619 Add new subscription multi device sync message. 2021-11-30 17:13:14 -05:00
Alex Hart 9f9bf3c604 Remove TextDrawable dependency. 2021-11-30 17:13:14 -05:00
Greyson Parrelli f9c4fe736a Add a network constraint to ReactionSendJob. 2021-11-30 17:13:14 -05:00
Greyson Parrelli 638bae6de3 Update SQLCipher error handling. 2021-11-30 17:13:14 -05:00
Sgn-32 e363bac1a3 Replace only the first emoji in message body. 2021-11-30 17:13:12 -05:00
Alex Hart e690e9bd69 Display an error if we cannot open picker instead of crashing. 2021-11-30 17:13:12 -05:00
Alex Hart c0ac2176c1 Clean up dead code from database refactor. 2021-11-30 17:13:12 -05:00
Ehren Kret dccfafa9e8 Remove dead code referencing the old way directory lookup was performed. 2021-11-30 17:13:10 -05:00
Alex Hart 0edfb0bd68 Check whether recipient is blockable before allowing blocking. 2021-11-29 11:21:40 -04:00
Alex Hart 31a815013e Update onError signature in donations sample app. 2021-11-29 10:25:37 -04:00
Greyson Parrelli 4364e9513f Do not assume e164 is populated in storage service insert. 2021-11-28 18:01:44 -05:00
Greyson Parrelli 1621c060b5 Bump version to 5.27.7 2021-11-28 10:53:50 -05:00
Greyson Parrelli b1c32476b0 Updated language translations. 2021-11-28 10:52:56 -05:00
Greyson Parrelli ba96db2ae0 Update permission string at Google's request to reflect private contact discovery. 2021-11-27 18:12:23 -05:00
Greyson Parrelli 182a112cdd Bump version to 5.27.6 2021-11-24 16:41:12 -05:00
Greyson Parrelli a45e26ab6b Updated language translations. 2021-11-24 16:41:12 -05:00
Greyson Parrelli b6022be41f Fix possible crash in MediaPreviewActivity. 2021-11-24 16:41:12 -05:00
Greyson Parrelli 0801a0e329 Fix typo in badge density selection. 2021-11-24 16:41:12 -05:00
Greyson Parrelli 89cbfd3299 Log errorCode when a stripe request fails. 2021-11-24 16:41:12 -05:00
Cody Henthorne 5b2ca6a1d3 Display badge on Payment Details. 2021-11-24 16:41:12 -05:00
Cody Henthorne c5d7188dcb Fix country specific translations for badges. 2021-11-24 16:41:12 -05:00
Cody Henthorne 818eb81f87 Fix Boost bottom sheet dark theme bugs. 2021-11-24 16:41:12 -05:00
Cody Henthorne 510a295198 Fix crash in custom boost input. 2021-11-24 16:41:12 -05:00
Greyson Parrelli 98fab95683 Bump version to 5.27.5 2021-11-23 20:22:06 -05:00
Greyson Parrelli 79d45bb497 Updated language translations. 2021-11-23 20:18:58 -05:00
Greyson Parrelli fc3d77ed9a Remove emails from payments. 2021-11-23 20:18:57 -05:00
Greyson Parrelli ee05cf87aa Bump version to 5.27.4 2021-11-23 17:37:32 -05:00
Greyson Parrelli ae18aed15b Updated language translations. 2021-11-23 17:35:54 -05:00
Greyson Parrelli a5aa079216 Include more debug info around badges. 2021-11-23 17:28:24 -05:00
Greyson Parrelli ae7a03bc8f Improve boost expiration UI when you're also a sustainer. 2021-11-23 17:00:47 -05:00
Cody Henthorne 6ed797c031 Fix custom input formatting and display bugs. 2021-11-23 17:00:47 -05:00
Greyson Parrelli ef4015aec9 Fix badge size and navigation in expiration bottom sheet. 2021-11-23 17:00:47 -05:00
Greyson Parrelli ffedc3fa7d Fix error string placeholder. 2021-11-23 17:00:47 -05:00
Greyson Parrelli 20285e7e5b Ensure the user's profile gets uploaded. 2021-11-23 17:00:47 -05:00
Cody Henthorne 89e55a7133 Fix truncated text on View Badges bottom sheet. 2021-11-23 17:00:47 -05:00
Greyson Parrelli 11aa168a6b Improve handling of unregistered failure during sender key send. 2021-11-23 17:00:47 -05:00
Greyson Parrelli 0fc6e642fe Default the donor badge flags to 'on'. 2021-11-23 00:11:35 -05:00
Greyson Parrelli 8e0553c849 Bump version to 5.27.3 2021-11-22 23:53:48 -05:00
Greyson Parrelli 75b4ffc16e Updated language translations. 2021-11-22 23:53:18 -05:00
Greyson Parrelli 643b07d564 Reduce occurrence of the media preview jumping. 2021-11-22 23:39:59 -05:00
Greyson Parrelli 637a44379c Ensure onboarding cards are cleared with enough conversations. 2021-11-22 23:15:05 -05:00
Greyson Parrelli a2d42b0415 Fix clickable area of avatars. 2021-11-22 22:59:21 -05:00
Greyson Parrelli a76983ca0a Add logging around changes in badges on a profile. 2021-11-22 22:44:10 -05:00
Cody Henthorne 22e79a045c Fix boosts made in UGX. 2021-11-22 22:44:10 -05:00
Cody Henthorne 061b87ead0 Fix boosts buttons in RTL. 2021-11-22 22:44:10 -05:00
Greyson Parrelli 511abd67c6 Show correct animation in boost fragment. 2021-11-22 22:44:10 -05:00
Greyson Parrelli 1627d92009 Fix display of boost payment processing dialog. 2021-11-22 22:44:10 -05:00
Greyson Parrelli 2cb67f6ee3 Fix logic around storage crash. 2021-11-22 22:44:10 -05:00
Greyson Parrelli 13e0b8dec0 Fix issue with recycling mute icon in conversation list. 2021-11-22 22:44:10 -05:00
Greyson Parrelli 7626070c28 Make the badge a selectable area in the subscriptions screen. 2021-11-22 22:44:10 -05:00
Greyson Parrelli ca5140d3ec Fix overloaded usages of the word 'boost'. 2021-11-22 22:44:10 -05:00
Greyson Parrelli 3694431503 Fix navigation to badge management screen. 2021-11-22 22:44:10 -05:00
Greyson Parrelli cd1f0632fa Improve recognition of failed payment states. 2021-11-22 22:44:10 -05:00
Cody Henthorne 1508b1d401 Fix invalid string resource. 2021-11-22 22:44:10 -05:00
Cody Henthorne bf874e17e5 Fix various bottom sheet scroll but off bugs. 2021-11-22 22:44:10 -05:00
Cody Henthorne d2b8a17723 Fix load jump/jank when opening subscription. 2021-11-22 22:44:10 -05:00
Greyson Parrelli 67cfdf101d Better logging around redemption failures. 2021-11-22 22:44:10 -05:00
Greyson Parrelli 125840e5fc Fix rendering of subscription error string. 2021-11-22 22:44:10 -05:00
Cody Henthorne f5ab4bec7a Make Become a Sustainer scrollable for longer translations. 2021-11-22 22:44:10 -05:00
Greyson Parrelli ef7d5d55cb Protect against individual item updates being put into an invalidated list. 2021-11-22 10:40:06 -05:00
Greyson Parrelli 1a9d785cbb Fix typo in database call. 2021-11-21 22:02:48 -05:00
Cody Henthorne cad0bab435 Bump version to 5.27.2 2021-11-19 16:41:47 -05:00
Cody Henthorne bdc3435fc1 Updated language translations. 2021-11-19 16:32:37 -05:00
Alex Hart f260633c9d Update payment failure ux. 2021-11-19 16:28:39 -05:00
Alex Hart 8a00caabd7 Update how we deal with failed or in progress subscriptions. 2021-11-19 16:28:39 -05:00
Alex Hart b4fe5bdcc6 Add new night boost icon. 2021-11-19 16:28:39 -05:00
Alex Hart 1f649057d6 A lot more logging for donor badges. 2021-11-19 16:28:39 -05:00
Jim Gustafson 41059a2b67 Update to RingRTC v2.14.3 2021-11-19 16:28:39 -05:00
Alex Hart 3d65a957f4 Treat google payment request token error as setup failure in boost. 2021-11-19 16:28:39 -05:00
Greyson Parrelli ff038e3ade Fix some issues with restoring old backups.
There's a bug where if you restore a database with a different column
definition order than a new install, then column indexes in cursors
could be wrong. Closing and re-opening the database fixes this.

I also removed a reference to a possibly-closed database we were holding
onto in LiveRecipient.
2021-11-19 16:28:39 -05:00
Alex Hart 44fa42fca4 Do not select sub row as active if the sub itself is inactive. 2021-11-19 16:28:39 -05:00
Alex Hart 73d8c74718 Expand donation job logging. 2021-11-19 16:28:39 -05:00
Alex Hart db4a0deccc Slide badge on swipe to reply. 2021-11-19 16:28:39 -05:00
Alex Hart 8b23a409ef Fix custom amount parsing for languages that utilize , separator. 2021-11-19 16:28:39 -05:00
Alex Hart ec7e73bb7c Do not use secondary colors for titles of unset values in manage profile fragment. 2021-11-19 16:28:39 -05:00
Alex Hart 321b85d5d0 Fix badge redemption failure copy. 2021-11-19 16:28:39 -05:00
Alex Hart 98c9638bc4 Reintroduce native currency symbols. 2021-11-19 09:16:20 -04:00
Alex Hart de1c9f2581 Fix custom amount filter regex. 2021-11-19 09:11:12 -04:00
Alex Hart 1af6af5045 Increase max line count in badge viewer page to 4. 2021-11-19 08:55:21 -04:00
Alex Hart 0121811195 Add padding to the bottom of the Thank You bottom sheet. 2021-11-19 08:48:29 -04:00
Alex Hart 18cf55b156 Fix error when trying to create payment in languages which use , instead of . 2021-11-19 08:45:10 -04:00
Alex Hart 0d4e109c72 Implement several badge job tweaks to align with iOS. 2021-11-19 08:33:04 -04:00
Alex Hart 3e358da83a Do not show become a sustainer if user is already a sustainer. 2021-11-18 17:56:56 -04:00
Greyson Parrelli 85453ca442 Fix retrieval of PNI. 2021-11-18 14:38:13 -05:00
Cody Henthorne a5e5a73580 Bump version to 5.27.1 2021-11-18 13:29:53 -05:00
Cody Henthorne 95f7b8d79f Updated language translations. 2021-11-18 13:24:14 -05:00
Greyson Parrelli 42d0d84ae0 Handle the case where a number changes during a recipient merge. 2021-11-18 13:19:32 -05:00
Alex Hart 686219d473 Remove old donate megaphone and replace with sustainer megaphone. 2021-11-18 14:02:55 -04:00
Greyson Parrelli 843ed24bbb Introduce SignalDatabase as the main database entrypoint. 2021-11-18 12:36:52 -05:00
Alex Hart e17c49505c Implement several donor badge fixes and rotate flags.
* Add white Google Pay buttons for use in dark mode.
* Always display badges for self.
* Disallow toggling / feature selection if no network is present.
* Only display bottom sheet overscroll if content scrolls.
* Flatten settings xml for better animations.
* Add a bit of space to the bottom of subscribe fragment.
* Treat GooglePay errors as setup failures.
* Add quieter log for 404.
* Ensure we check case before initial currency code comparison.
* Fix timeout dialog copy.
* Fix double settings activity on top issue.
* Rotate FF.
2021-11-18 13:25:37 -04:00
Alex Hart 473747ee03 Fix missing space between also and become. 2021-11-18 11:42:08 -04:00
Alex Hart 9ea97aabbb Fix badge row count calculation. 2021-11-18 11:35:47 -04:00
Greyson Parrelli 811d79c873 Prefer 'nightly' tags when reading current git tag. 2021-11-17 21:17:36 -05:00
Cody Henthorne 018782e63d Bump version to 5.27.0 2021-11-17 16:22:05 -05:00
Cody Henthorne 01070a9cc0 Updated language translations. 2021-11-17 16:18:13 -05:00
Alex Hart 14aecc4684 Update payment method request with email. 2021-11-17 16:14:26 -05:00
Greyson Parrelli 8aea20f147 Migrate local account data into SignalStore. 2021-11-17 16:14:26 -05:00
Alex Hart 87f175a96b Ensure we print the status message if there is a GooglePay error. 2021-11-17 16:14:26 -05:00
Alex Hart 6b5117a609 Fix profile image flicker. 2021-11-17 16:14:26 -05:00
Alex Hart 0ab66f81be Remove LifecycleViewHolder / Adapter. 2021-11-17 16:14:26 -05:00
Alex Hart 12ec0ca84c Fix video playback after editing clip boundaries. 2021-11-17 16:14:26 -05:00
Alex Hart 915d56ac15 Kill animations for Avatar glide requests. 2021-11-17 16:14:26 -05:00
Alex Hart ecc43f1dea Add state logging when we reject an item animation from occurring. 2021-11-17 16:14:26 -05:00
Jim Gustafson d8a4678b8f Update to RingRTC v2.14.2 2021-11-16 20:18:46 -05:00
Alex Hart 306875478e Add become a sustainer bottom sheet. 2021-11-16 17:27:47 -05:00
Greyson Parrelli 2df303cde7 Add some additional endpoints for PNP. 2021-11-16 17:27:47 -05:00
Alex Hart 4309127b8c Correct text on learn more sheet. 2021-11-16 17:27:47 -05:00
Greyson Parrelli 39155b55a0 Send a sync message to fetch the local profile upon editing your profile. 2021-11-16 17:27:47 -05:00
Alex Hart 02dc457636 Fix expiring label. 2021-11-16 17:27:47 -05:00
Greyson Parrelli 732a6324d6 Include auth token in CDSH request. 2021-11-16 17:27:47 -05:00
Greyson Parrelli 54614e67aa Update CDSH with better error handling. 2021-11-16 17:27:47 -05:00
Greyson Parrelli 15362c04fb Remove the 'internal' distribution dimension. 2021-11-16 17:27:47 -05:00
Greyson Parrelli 658de3b6e7 Convert all database notifiers to use DatabaseObserver.
Lots of red in this diff to celebrate the release of Red (Taylor's Version).
2021-11-16 17:27:47 -05:00
Greyson Parrelli ab55fec6bd Move reactions into their own table. 2021-11-16 17:27:47 -05:00
Cody Henthorne 3a1f06f510 Address memory leaks. 2021-11-16 17:27:47 -05:00
AsamK 1ad0b0e6ae Close response body for all storage requests and for unsuccessful requests. 2021-11-16 17:27:47 -05:00
Jordan Rose 7ccc7ec856 Update to libsignal-client 0.10.0, which includes zkgroup. 2021-11-16 17:27:47 -05:00
Cody Henthorne f0ab919ca5 Fix EGL crash when ending call. 2021-11-16 17:27:47 -05:00
Cody Henthorne 8a05626791 Fix call setup state management bugs. 2021-11-16 17:27:47 -05:00
Jim Gustafson c0a468e42b Update to RingRTC v2.14.0 2021-11-16 17:27:47 -05:00
Cody Henthorne 8bee95eb02 Bump version to 5.26.11 2021-11-16 16:46:39 -05:00
Cody Henthorne dedb78e454 Updated language translations. 2021-11-16 16:38:16 -05:00
Cody Henthorne 2c1f30db1d Fix excludeNonTranslatables gradle task. 2021-11-16 16:35:15 -05:00
Alex Hart 6d3319bfb1 Rotate donor badge flags. 2021-11-16 15:51:07 -05:00
Cody Henthorne e4b9832045 Bump version to 5.26.10 2021-11-15 16:23:41 -05:00
Cody Henthorne 99aa4cbc98 Updated language translations. 2021-11-15 16:23:18 -05:00
Alex Hart 1f952bd31e Update Badge spritesheet transformer to include new sizing. 2021-11-15 16:37:33 -04:00
Alex Hart 882bdcc726 Send user an email after Stripe completes payment for boosts. 2021-11-15 13:48:19 -04:00
Alex Hart b0f43535c6 Implement checks for badge redemption progress for subscriptions. 2021-11-15 13:47:51 -04:00
Alex Hart 16ae2c870f Modify boost and subscribe error dialog logic. 2021-11-15 13:21:30 -04:00
Greyson Parrelli 18bb876d1b Fix payments banner causing weird conversation list animations. 2021-11-15 11:21:45 -05:00
Greyson Parrelli dce8fde195 Bump version to 5.26.9 2021-11-12 22:18:11 -05:00
Greyson Parrelli 270ab34c6a After review, everything looks good. Update MobileCoin Payments Beta country codes.
This reverts commit 0cb53f40f4.
2021-11-12 22:14:57 -05:00
Alex Hart aa872d29bc Bump version to 5.26.8 2021-11-12 15:07:10 -04:00
Alex Hart 6315d4b96c Updated language translations. 2021-11-12 15:06:41 -04:00
Alex Hart 0cb53f40f4 Update MobileCoin Payments Beta country codes. 2021-11-12 14:53:35 -04:00
Greyson Parrelli 51c86cab10 Add the ability to get the current state of a job. 2021-11-12 10:57:01 -04:00
Alex Hart 1f860d41b5 Swap boost button animations. 2021-11-12 10:14:48 -04:00
Alex Hart 573de99840 Remove circle from group member row. 2021-11-12 09:56:13 -04:00
Alex Hart 68e0a30c92 Remove invalidateItemDecorations call. 2021-11-12 09:39:34 -04:00
Alex Hart 6fc9db0aff Bump version to 5.26.7 2021-11-11 18:24:10 -04:00
Alex Hart 737d893c87 Updated language translations. 2021-11-11 18:23:50 -04:00
Alex Hart f06e1d9b98 Bump version to 5.26.6 2021-11-11 18:15:00 -04:00
Alex Hart 4cff0a3369 Updated language translations. 2021-11-11 18:14:03 -04:00
Alex Hart cc64a922d7 Change to country codes for Payments Beta. 2021-11-11 18:12:24 -04:00
Alex Hart e8c769bd1d Bump version to 5.26.5 2021-11-11 16:52:08 -04:00
Alex Hart deba07d6cb Updated language translations. 2021-11-11 16:52:08 -04:00
Alex Hart bacad359b2 Add better check boxes. 2021-11-11 16:52:08 -04:00
Greyson Parrelli a9d7417597 Fix toolbar shadow in conversation list. 2021-11-11 16:52:08 -04:00
Alex Hart 6b94fc82eb Add and sync displayBadgesOnProfile Flag. 2021-11-11 16:52:08 -04:00
Greyson Parrelli b9f060b442 Fix conversation list animations sometimes playing. 2021-11-11 16:52:08 -04:00
Alex Hart ca24682366 Fix a bunch UX bugs for donor badges. 2021-11-11 13:46:38 -04:00
Alex Hart 5047fc54f2 Enable Payments Beta for more country codes. 2021-11-11 13:45:48 -04:00
Alex Hart 48c115eba1 Bump version to 5.26.4 2021-11-10 15:32:20 -04:00
Alex Hart fd2677e8fe Updated language translations. 2021-11-10 15:32:20 -04:00
Alex Hart f6bd27eff9 Retry network call if subscription isn't active yet. 2021-11-10 15:32:20 -04:00
Cody Henthorne ff41816fef Fix incorrect profile upload flag for existing users. 2021-11-10 15:32:20 -04:00
Alex Hart 1e6a17adc3 Add google pay subject subscriber for boosts. 2021-11-10 15:32:20 -04:00
Alex Hart 55aff18b1f Increase logging in Boost codepath. 2021-11-10 15:32:20 -04:00
Alex Hart 5d6b3a8a75 Add support for 60dp badges in the spritesheet. 2021-11-10 15:32:20 -04:00
Alex Hart 31b98ec612 Add badge to Recipient row of reactions sheet. 2021-11-10 15:32:20 -04:00
Alex Hart 320bf45518 Add better UX while loading sustainer data and when a load failure happens. 2021-11-10 11:37:10 -04:00
Alex Hart 1893896254 Only perform subscriber id keep-alive when the user foregrounds the app. 2021-11-10 11:33:00 -04:00
Alex Hart 19a95f479e Adjust badge positioning. 2021-11-10 10:55:49 -04:00
Alex Hart 5bcb7cece4 Remove background from preview views. 2021-11-10 08:59:28 -04:00
Greyson Parrelli f4f5fe2789 Improve logging around database crashes. 2021-11-09 16:38:19 -05:00
Alex Hart e947212862 Bump version to 5.26.3 2021-11-09 13:18:07 -04:00
Alex Hart 57f86b14fc Updated language translations. 2021-11-09 13:18:06 -04:00
Greyson Parrelli e2dc7fb5bf Fix early close when navigating back to camera-first capture.
Fixes #11729
2021-11-09 13:18:06 -04:00
Greyson Parrelli 6499ed4637 Improve responsiveness of archive animations, other swipe tweaks. 2021-11-09 13:18:06 -04:00
Alex Hart 8c45600365 Swap string with currency. 2021-11-09 13:18:06 -04:00
Alex Hart f8ef850fba Update readmore text. 2021-11-09 13:18:06 -04:00
Alex Hart 151e2e5203 Increase logging around camera errors, skip toast if context is null. 2021-11-09 13:18:06 -04:00
Alex Hart 5dd3d8515f Increase minimum button width to 80dp 2021-11-09 13:18:06 -04:00
Alex Hart 0f6c16c373 Add LAST_END_OF_PERIOD to backup 2021-11-09 13:18:06 -04:00
Alex Hart 75bf3a7c7e Ensure we display badge in conversation settings. 2021-11-09 13:18:06 -04:00
Alex Hart 48e47c9d92 Implement several pieces of badge feedback. 2021-11-09 13:18:06 -04:00
Alex Hart 3d45ab1b36 Fix item animator slide on change. 2021-11-09 13:18:06 -04:00
Alex Hart 4d5d42157a Add in-progress (loading) states for subscriptions and boosts. 2021-11-09 13:18:06 -04:00
Alex Hart a6dfee16e9 Make play/pause button long-clickable. 2021-11-08 09:12:03 -04:00
Greyson Parrelli 0e8550748d Bump version to 5.26.2 2021-11-06 12:31:48 -04:00
Greyson Parrelli b82604953c Updated language translations. 2021-11-06 12:31:30 -04:00
Greyson Parrelli 100796b3b9 Fix tracking of created_at in SenderKeyDatabase. 2021-11-06 00:18:42 -04:00
Greyson Parrelli f5af964286 Fix selection getting stuck when exiting multiselect on conversation list. 2021-11-05 18:38:33 -04:00
Greyson Parrelli 2836a6060d Bump version to 5.26.1 2021-11-05 16:36:36 -04:00
Greyson Parrelli 80e31051e6 Updated language translations. 2021-11-05 16:36:36 -04:00
Greyson Parrelli 1fb0573fec Fix conversation list multiselect animation. 2021-11-05 16:36:36 -04:00
Greyson Parrelli 5ba04936b1 Add a log section for remapped recipients. 2021-11-05 15:57:13 -04:00
Greyson Parrelli 011f6e6cf4 Repair groups with remapped recipients. 2021-11-05 15:46:38 -04:00
Greyson Parrelli ed3f992b83 Tweak archive animation scaling, use new unarchive icon. 2021-11-05 15:36:30 -04:00
Alex Hart 782217a73d Remove audio view size restriction. 2021-11-05 15:36:30 -04:00
Greyson Parrelli a37b89feaf Fix NPE when rendering group member item. 2021-11-05 15:36:30 -04:00
Greyson Parrelli e5b628b467 Improve the archive animation. 2021-11-05 15:36:30 -04:00
Alex Hart 482a10de02 Improve handling of network timeouts for donor badges. 2021-11-05 15:36:30 -04:00
Greyson Parrelli c4164b17a2 Add basic animations to conversation list. 2021-11-05 15:36:30 -04:00
Alex Hart b8dc541fc5 Add better application error handling for badges and token redemption. 2021-11-05 15:36:30 -04:00
Cody Henthorne 2b6190bf34 Fix UI bug on welcome screen. 2021-11-05 15:36:30 -04:00
Alex Hart 2a70423a22 Fix boolean logic for isExpirationWithinAMonth 2021-11-05 15:36:30 -04:00
Alex Hart 35c74573e7 Update Internal SubscriberId setting to properly serialize. 2021-11-05 15:36:19 -04:00
Greyson Parrelli c26c455b3c Fix some sizing issues in the recipient bottom sheet. 2021-11-05 00:20:35 -04:00
Greyson Parrelli 4e2e525509 Bump version to 5.26.0 2021-11-04 18:29:51 -04:00
Greyson Parrelli ec83327eec Updated language translations. 2021-11-04 18:29:51 -04:00
Alex Hart bafb62f214 Add debug log to log out subscription level. 2021-11-04 18:29:51 -04:00
Greyson Parrelli 38f5e8b4eb Include subscriberId in internal details for Note to Self. 2021-11-04 18:29:51 -04:00
Cody Henthorne 9827deffd3 Make websocket timeouts stay on IO threads. 2021-11-04 18:29:51 -04:00
Alex Hart 65105fd3cb Allow subscription redemption to retry. 2021-11-04 18:29:51 -04:00
Alex Hart 5d604c4e55 Adjust done button spacing and action. 2021-11-04 18:29:51 -04:00
Alex Hart 22221222bd Add proper name and alignment to expired fragment. 2021-11-04 18:29:51 -04:00
Greyson Parrelli bad2f99968 Ensure store is properly cleaned up in conversation settings. 2021-11-04 18:29:51 -04:00
Alex Hart 392d582865 Add a feature flag for badge display. 2021-11-04 18:29:51 -04:00
Alex Hart 33dbf316a9 Add feature flag for donor badges megaphone. 2021-11-04 18:29:51 -04:00
Alex Hart 00a8565e91 Allow retries for redemption from server failure. Add internal preference to enqueue job. 2021-11-04 18:29:51 -04:00
Greyson Parrelli 0bac08dcc4 Extend log duration to max(3 days, 20MB). 2021-11-04 18:29:51 -04:00
Greyson Parrelli 3b2dfb6ede Ignore MediaBrowserService in LeakCanary. 2021-11-04 18:29:51 -04:00
Alex Hart 997f6ef534 Do not allow BadgeImageView to control its own visibility. 2021-11-04 18:29:50 -04:00
Greyson Parrelli fb0b1af056 Allow lazy creation of Recipient.self() 2021-11-04 18:29:50 -04:00
Alex Hart 3037a33267 Add animation for swipe to archive. 2021-11-04 18:29:50 -04:00
Greyson Parrelli ff633ddd59 Stop observing LiveRecipient in contact list when detached. 2021-11-04 17:00:04 -04:00
Greyson Parrelli cae5dad5d8 Guard against missing recipientIds in the media overview. 2021-11-04 17:00:04 -04:00
Greyson Parrelli 1a03b8fc1d Don't ask for permissions if none are needed. 2021-11-04 17:00:04 -04:00
Greyson Parrelli 049ba6a706 Remove WorkManager migration that is no longer necessary.
We migrated away from WorkManager over 2 years ago. We needed it at the
time because we wanted to migrate jobs that were scheduled on
WorkManager into the new system. However, at this point, the user's
client would have been expired for 2 years at the point of upgrade, and
there wouldn't be any jobs that need migrating.
2021-11-04 17:00:04 -04:00
Alex Hart f52364f75c Fix deeplinking into subscribe page. 2021-11-04 17:00:04 -04:00
Alex Hart 87b699f3d8 Update copy for help fragmemt. 2021-11-04 17:00:04 -04:00
Alex Hart f73b8a7fd2 Remove check for whether google pay is available. 2021-11-04 17:00:04 -04:00
Greyson Parrelli 8af8468f4d Show inferred stack traces when logging blocked threads. 2021-11-04 17:00:04 -04:00
Cody Henthorne 49270e677e Fix improper glare handling. 2021-11-04 17:00:04 -04:00
Alex Hart 09dd2583b9 Fix reaction shade on new conversations. 2021-11-04 17:00:04 -04:00
Greyson Parrelli dc22b27cd8 Fix issues rendering long button text in bottom sheet.
Fixes #11727
2021-11-04 17:00:04 -04:00
Alex Hart 2a9eb1bae0 Respect server currency lists for subscriptions and badges. 2021-11-04 17:00:04 -04:00
Greyson Parrelli c06fb81490 Render better crash stack traces for executors. 2021-11-04 17:00:04 -04:00
Alex Hart af1b9579b4 Add link for more payment options. 2021-11-04 17:00:04 -04:00
Alex Hart 7bbfc2d34c Add badge treatments as per spec. 2021-11-04 17:00:04 -04:00
Alex Hart 70355aa70e Add server-based localization of subscription names and badge information. 2021-11-04 17:00:04 -04:00
Greyson Parrelli 56c502c9bf Update libphonenumber to 8.12.33 2021-11-04 17:00:04 -04:00
Alex Hart a05793c882 Call show() on Google Pay material dialog. 2021-11-04 17:00:00 -04:00
Alex Hart 53f60f5a4c Add link out to donate support page. 2021-11-04 16:59:59 -04:00
Alex Hart 43d969f6b5 Allow PAN_ONLY payments in Google Pay. 2021-11-04 16:59:59 -04:00
Greyson Parrelli a51bb8e23f Add LeakCanary to flipper builds. 2021-11-04 16:59:59 -04:00
Alex Hart 5ceb3db0c4 Rotate donor badge feature flag. 2021-11-04 16:59:59 -04:00
Greyson Parrelli 9a65328c1b Inline the sender key feature flag. 2021-11-04 16:59:59 -04:00
Alex Hart 35eef0150d Do not hide badges via flag. 2021-11-04 16:59:59 -04:00
Greyson Parrelli 8511d3576f Use the SignalServiceNetworkAccess from ApplicationDependencies. 2021-11-04 16:59:59 -04:00
Alex Hart f6542440c7 Adjust boost dialog fragment to behave better with keyboard. 2021-11-04 16:59:59 -04:00
Greyson Parrelli 35393fc331 Make sender key max age remote configurable. 2021-11-04 16:59:59 -04:00
Alex Hart f31e12572a Fix profile editor layout issue. 2021-11-04 16:59:59 -04:00
Greyson Parrelli cef7878b47 Store the time that a sender key was shared. 2021-11-04 16:59:59 -04:00
Greyson Parrelli 3574be913a Log out sender key state for internal users. 2021-11-04 16:59:59 -04:00
Alex Hart b8cf0cc1be Always clear LevelUpdateOperation if an error occurs.
After speaking with the server team, it's been made clear that the
idempotency key should only ever be reutilized if we never heard
back from the server. Since we do not employ an automatic retry
mechanism for setting a user's subscription level (we simply
notify the user of the failure) it is less error-prone to simply
never reuse an idempotency key.
2021-11-04 16:59:59 -04:00
Alex Hart b0788f7307 Fix retry issue with payment processing. 2021-11-04 16:57:15 -04:00
Alex Hart cf9b91ebd4 Remove unneeded code for redraw. 2021-11-04 16:57:15 -04:00
Alex Hart 1af15842cc Add more polish to Badges.
* Better network error handling
* Marking user cancellations so we don't annoy them
* Manage Profile screen treatment.
2021-11-04 16:57:15 -04:00
Greyson Parrelli 17517cfc88 Log additional details around group sends. 2021-11-04 16:57:15 -04:00
Greyson Parrelli 4615f246ac Log additional info about 409/410 responses. 2021-11-04 16:57:10 -04:00
Greyson Parrelli 62ee60df82 Add full support for unknown fields in storage service. 2021-11-01 17:07:01 -04:00
Alex Hart 4f3c545eda Fix flashing when send/recv messages in a new conversation. 2021-11-01 17:07:01 -04:00
Alex Hart b92a41ab70 Fix strange scrolling behaviour for new messages. 2021-11-01 16:49:13 -04:00
Alex Hart 6673da0b04 Add subscriber information to storage service account record. 2021-11-01 16:48:42 -04:00
Alex Hart 102f9de06f Finish ShareActivity after external share. 2021-11-01 16:48:42 -04:00
Alex Hart 614d6ce04b Add fun emoji animations when selecting boost level. 2021-11-01 16:48:41 -04:00
Greyson Parrelli 5bb48caafd Strongly type UUIDs as ACIs. 2021-11-01 16:48:41 -04:00
Alex Hart 6c7d837964 Update badge copy with new strings. 2021-11-01 16:48:41 -04:00
Alex Hart 755ec672c0 Implement several pieces of UI polish for badges. 2021-11-01 16:48:41 -04:00
Alex Hart 186bd9db48 Implement new APIs for Boost badging. 2021-11-01 16:48:41 -04:00
Greyson Parrelli 48a81da883 Handle non-normalized phone number responses. 2021-11-01 16:48:41 -04:00
Greyson Parrelli 2980e547cb Bump version to 5.25.8 2021-11-01 15:02:56 -04:00
Greyson Parrelli c9c2bbcf80 Updated language translations. 2021-11-01 14:58:07 -04:00
Greyson Parrelli 33da599ee0 Properly unregister some database observers. 2021-11-01 14:58:07 -04:00
Alex Hart 113bcca277 Improve management of bubble animator listener lifecycle. 2021-11-01 14:58:06 -04:00
Alex Hart deca8e3feb Fix a few glide issues. 2021-11-01 14:42:37 -04:00
Alex Hart e02c8b9db7 Fix lifecycle of VoiceNoteProximityWakeLockManager. 2021-11-01 14:42:02 -04:00
Alex Hart 314ea98393 Bump version to 5.25.7 2021-10-28 16:35:20 -03:00
Alex Hart 0840cfc6e7 Updated language translations. 2021-10-28 16:34:50 -03:00
Alex Hart de4cb931f3 Fix crash when switching between color gradient tabs. 2021-10-28 10:58:10 -03:00
Alex Hart abde740ff7 Bump version to 5.25.6 2021-10-26 17:07:30 -03:00
Alex Hart 9efe216070 Updated language translations. 2021-10-26 17:07:30 -03:00
Alex Hart 2427c226a8 Disable message animations when scrolling. 2021-10-26 17:07:30 -03:00
Greyson Parrelli ae73601f52 Load thumbnails using an asynchronous Glide target. 2021-10-26 17:07:30 -03:00
Alex Hart 85551ca824 Fix keyboard issue on some Android devices. 2021-10-26 17:07:30 -03:00
Alex Hart 12565d28ae Fix possible NPE. 2021-10-26 10:25:44 -03:00
Greyson Parrelli f0a4956cdd Exclude the HeapTaskDaemon from blocked thread warnings.
It's just how the thing works in a lot of cases, and it's polluting the
logs with instances of nothing but several blocked HeapTaskDaemons.
2021-10-26 09:20:46 -04:00
Greyson Parrelli ba0befde20 Fix issue where delivery receipts may not update the thread summary.
We were notifying in a transaction, which we can't do anymore since
transactions don't block reads from other threads (meaning we could
notify and someone could read it before we end the transaction, so they
wouldn't see the update).
2021-10-26 09:10:59 -04:00
Alex Hart dd7652ad44 Bump version to 5.25.5 2021-10-25 15:37:49 -03:00
Alex Hart b41303ba0d Updated language translations. 2021-10-25 15:34:39 -03:00
Greyson Parrelli a70ab94d24 Disallow swiping on selected conversation list items. 2021-10-25 14:28:37 -04:00
Greyson Parrelli 10dd39abea Fix layout of long actionbar strings. 2021-10-25 14:26:51 -04:00
Alex Hart 5113f8b203 Ensure MP4 Gif vertical position updates as content slides. 2021-10-25 15:23:15 -03:00
Jim Gustafson 8f007a23cd Update to RingRTC v2.13.6 2021-10-25 14:15:47 -03:00
Alex Hart b34bb2e7d7 Drastically reduce number of projection instances we create.
Via SimplePool
2021-10-25 14:12:08 -03:00
Alex Hart 98fce53cf1 Fix several beta issues with new slide animations. 2021-10-25 13:39:01 -03:00
Greyson Parrelli ced05fe579 Fix conflict between plural and normal string keys. 2021-10-25 08:35:00 -04:00
Greyson Parrelli fae21e4dbb Bump version to 5.25.4 2021-10-24 14:32:14 -04:00
Greyson Parrelli 5e3a3e1da9 Updated language translations. 2021-10-24 14:31:41 -04:00
Greyson Parrelli 03ad5073d2 Adjust SignalExecutors.BOUNDED config to actually use extra threads. 2021-10-24 14:19:11 -04:00
Greyson Parrelli 3bd354289d Update r8 to 3.0.73
Fixes #11352
2021-10-23 00:46:56 -04:00
Greyson Parrelli 8808526d0b Bump version to 5.25.3 2021-10-22 22:43:55 -04:00
Greyson Parrelli 0a19440ffc Updated language translations. 2021-10-22 22:42:55 -04:00
Alex Hart 9815851bb9 Fix various issues with conversation animation. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 1581a6e1cc Adjust the SignalExecutor.BOUNDED config. 2021-10-22 22:42:55 -04:00
Greyson Parrelli e3aa244f31 Improve logging for thumbnail timeouts. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 8fcce9fba5 Additional logging for blocked thread pools. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 7d49c77d1a Add vertical translation to the bottom actionbar animation. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 947f59e81b Improve chat list multiselect animation performance. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 7cac62f3f2 Update thread after attachment downloads. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 4578c33968 Fix avatars being clickable in multiselect. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 0160303d19 Update text for internal preference. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 31aabd9851 Fix unread count font scaling. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 7f39b9b50f Reduce thumbnail generation threshold to 1 second. 2021-10-22 22:42:55 -04:00
Greyson Parrelli 69a2664668 Update bounded IO thread naming.
Helps with logging in DeadlockDetector.
2021-10-22 22:42:55 -04:00
Greyson Parrelli acebf5964c Update actionbar strings to allow for pluralization. 2021-10-22 22:42:55 -04:00
Greyson Parrelli ec2e3e29c3 Hide megaphones during multiselect. 2021-10-22 11:14:45 -04:00
Greyson Parrelli 0fc144d4a7 Bump version to 5.25.2 2021-10-22 10:46:17 -04:00
Greyson Parrelli 73025ec6de Updated language translations. 2021-10-22 10:46:17 -04:00
Greyson Parrelli 1d0e00648f Fix 30 day message duration.
Unfortunately leftover code from trying to repro a bug.
2021-10-22 10:46:17 -04:00
Greyson Parrelli 42b5654a99 Bump version to 5.25.1 2021-10-21 21:51:50 -04:00
Greyson Parrelli 2eb787d78b Updated language translations. 2021-10-21 21:51:29 -04:00
Greyson Parrelli 1249cced2d Set a timeout of 3 seconds to get a chat list thumbnail. 2021-10-21 21:32:07 -04:00
Greyson Parrelli 0be1a30766 Add the ability to mute on the chat list. 2021-10-21 21:22:19 -04:00
Greyson Parrelli ea253a2e67 Bump version to 5.25.0 2021-10-21 17:11:46 -04:00
Greyson Parrelli c4fadccf72 Updated language translations. 2021-10-21 17:11:46 -04:00
Greyson Parrelli fcf62512a7 Log when executors are full. 2021-10-21 17:11:46 -04:00
Alex Hart 16ab27084c Move multiselect animation code to decorator. 2021-10-21 17:11:46 -04:00
Alex Hart c1820459b7 Implement further features for badges.
* Add Subscriptions API
* Add Accept-Language header to profile requests
* Fix several UI bugs, add error dialogs, etc.
2021-10-21 17:11:46 -04:00
Greyson Parrelli d88999d6d4 Add new bottom actionbar to the media overview. 2021-10-21 17:11:46 -04:00
Alex Hart 68655194a6 Add bubble resize animation. 2021-10-21 17:11:46 -04:00
Greyson Parrelli f533a898f5 Add new bottom actionbar to chat list. 2021-10-21 17:11:46 -04:00
Alex Hart 2167522f7d Add sliding animation when a new message is received. 2021-10-21 17:11:46 -04:00
Robert Adam f198b890fa Update bug report issue template.
The instructions for obtaining a debug log were not really indicating where the Debug logs can be found (nowadays).
2021-10-21 17:11:46 -04:00
Greyson Parrelli 85cb41050e Re-order error handling in GroupSendJob. 2021-10-21 17:11:46 -04:00
Greyson Parrelli 00c131355f Log more specific database exceptions. 2021-10-21 17:11:46 -04:00
Greyson Parrelli 13ef53372e Remove the reset session button. 2021-10-21 17:11:46 -04:00
Greyson Parrelli f2cf77339e Fix logging of DEM deviceId. 2021-10-21 17:11:46 -04:00
Greyson Parrelli 3e5be2cfe2 Show a popup menu when long-pressing on the conversation list. 2021-10-21 17:11:46 -04:00
Greyson Parrelli c0a68202a7 Update some settings menus to use MaterialAlertDialogBuilder. 2021-10-21 17:11:46 -04:00
Alan Evans 07a6942ea8 Only copy distinct messages.
Fixes #11696
2021-10-21 17:11:46 -04:00
Jim Gustafson 41585699d2 Move device specific control to RingRTC 2021-10-21 17:11:46 -04:00
Jim Gustafson 2fcb240c2b Update to RingRTC v2.13.5 2021-10-21 17:11:46 -04:00
Alex Hart 566e981473 Catch IAE instead of checking lifecycle. 2021-10-21 17:11:46 -04:00
Greyson Parrelli 26e04ce6d2 Update conversation list multi-select to use checkboxes. 2021-10-21 17:11:46 -04:00
Greyson Parrelli 2e2b4e1406 Added a general test for recipient merging. 2021-10-21 17:11:46 -04:00
Greyson Parrelli b89e08dad7 Update libsignal-client to 0.9.7 2021-10-21 17:11:46 -04:00
Greyson Parrelli 5711b8a0fa Add instrumented tests for RecipientDatabase. 2021-10-21 17:11:46 -04:00
Alex Hart 62f9f19540 Do not autoplay in video editor. 2021-10-21 17:11:46 -04:00
Alex Hart 731683ae09 Implement adjustments to conversation list items to compensate for badge placement. 2021-10-21 17:11:46 -04:00
Alex Hart 343aadcd9a Bump version to 5.24.17 2021-10-14 16:42:37 -03:00
Alex Hart c4ad6c2992 Updated language translations. 2021-10-14 16:42:05 -03:00
Greyson Parrelli 97dd756136 Improve logging for decryption failures. 2021-10-14 13:17:35 -04:00
Greyson Parrelli 7989c40f52 fixup! Improve observer that logs blocked threads. 2021-10-14 10:57:35 -04:00
Greyson Parrelli 0749905909 Improve logging around sessions. 2021-10-13 15:29:05 -04:00
Greyson Parrelli 168481fee5 Improve observer that logs blocked threads. 2021-10-13 11:00:48 -04:00
Greyson Parrelli 7866e2e29c Bump version to 5.24.16 2021-10-13 08:57:10 -04:00
Greyson Parrelli 4eb0dca8f6 Updated language translations. 2021-10-13 08:56:42 -04:00
Alex Hart bc54f6ca07 Fix crash in locales without a currency. 2021-10-13 09:42:16 -03:00
Greyson Parrelli 223c0c4bce Bump version to 5.24.15 2021-10-12 16:28:45 -04:00
Greyson Parrelli b39099b84e Updated language translations. 2021-10-12 16:28:14 -04:00
Greyson Parrelli 22d6546704 Renamed EnterCodeFragment to EnterSmsCodeFragment.
I could never find the darn thing.
2021-10-12 15:45:26 -04:00
Greyson Parrelli a7af687f8e Add tap-for-debuglog to PinRestoreEntryFragment. 2021-10-12 15:45:26 -04:00
Alex Hart ce9cd132ec Never display badges if they are not enabled via feature flag. 2021-10-12 16:38:15 -03:00
Greyson Parrelli 62fa99e0ee Improve network reliability. 2021-10-12 15:23:46 -04:00
Alex Hart 43e4cba3d7 Implement the majority of the Donor UI. 2021-10-12 15:55:54 -03:00
Greyson Parrelli 6cbc2f684d Properly handle media validation errors. 2021-10-11 16:17:11 -04:00
Greyson Parrelli ffc9e8caff Add additional unit tests for phone number fuzzy matching. 2021-10-11 14:20:32 -04:00
Greyson Parrelli 49c9b0acde Remove concept of V1 vs V2 fuzzy phone number results.
V1 hasn't been used in a long time. So we can just delete that code then
remove the concept of a 'v2' from the other stuff.
2021-10-11 13:25:04 -04:00
franortiz 9c6908873c Handle multiple Argentina phone formats.
Fixes #10506
2021-10-11 13:18:08 -04:00
Greyson Parrelli 528fe67db9 Fix issue where conversation list wasn't updating for sent indicators.
We needed to add (back?) notifying the conversation list when sent
status changes.
2021-10-11 12:49:55 -04:00
Greyson Parrelli 39e14e922b Include milliseconds in generated file name.
Fixes #11670
2021-10-11 11:48:11 -04:00
Greyson Parrelli 0c8b6f8ef8 Add an observer to log blocked threads. 2021-10-08 15:18:52 -04:00
Greyson Parrelli f65de84c19 Update sender key store and MSL to be recipient-remap-safe.
The MSL is now remapped in the merge, and the sender key store is now
just keyed off of UUIDs.
2021-10-08 12:41:47 -04:00
Alex Hart 88074134af Fix case where dialog could be shown after user leaves fragment. 2021-10-07 10:45:41 -03:00
Alex Hart b5cc570363 Gracefully handle and log when a radio list does not have a default selection. 2021-10-07 08:49:30 -03:00
Alex Hart 3cbf0933ff Fix RTL placement of play icon in quote view. 2021-10-06 13:39:42 -03:00
Alex Hart 7f9c89483f Fix reactions shade issue in new conversations. 2021-10-06 10:02:17 -03:00
Alex Hart 8ef3d3fbbf Add extra protection to image editor crop.
Adds an extra 72dp (height of radial dial) to the protection value
for crop mode. This guarantees that the image is NOT going to have
the bottom inaccessible due to overlap with the radial dial.
2021-10-06 08:56:21 -03:00
Alex Hart c225c2b37d Check for NPE when bad data is passed to the PDUParser. 2021-10-05 11:30:01 -03:00
Alex Hart ff76c5fca5 Fix long name jitter as voice note position updates. 2021-10-05 11:08:42 -03:00
Alex Hart 5b99f590f8 Downsize fallback photos in conversation banner. 2021-10-05 11:08:28 -03:00
Alex Hart 2d0feca278 Eliminate flicker when entering multiselect. 2021-10-05 11:08:12 -03:00
Greyson Parrelli 92e506b117 Update libsignal-client to 0.9.6 2021-10-04 21:49:59 -04:00
Greyson Parrelli cac841d8e6 Flush logs before trimming to size.
There are situations where we may be hitting our SQLITE_BUSY timeout
when we go to trim. One possibility is that we may have a large ongoing
write when we go to trim.

So, this change just makes sure we're caught up before we go to trim,
which is the simplest thing we can do to address this. It's not a
foolproof solution though, so if we still see it crop up, we'll just
have to re-route all log operations through the single thread we have
setup in the PersistentLogger or something.
2021-10-04 21:49:59 -04:00
Greyson Parrelli 77cb9bc174 Update SQLCipher to 4.4.3-S8
This reverts commit e01381379c.
2021-10-04 21:49:59 -04:00
Cody Henthorne 309e33016a Prevent GV2 operations after becoming unregistered. 2021-10-04 21:49:59 -04:00
Jim Gustafson 938b24f623 Update to RingRTC v2.13.3 2021-10-04 21:49:59 -04:00
Cody Henthorne 82c637ef4b Add persistent sent media quality setting. 2021-10-04 21:49:59 -04:00
Alex Hart d9e8480a12 Add donations module. 2021-10-04 21:49:59 -04:00
Greyson Parrelli 5115717f67 Show internal conversation settings for groups. 2021-10-04 21:49:59 -04:00
Greyson Parrelli 33ac48e771 Show recipient threadId in internal settings. 2021-10-04 21:49:59 -04:00
Cody Henthorne c53f1fcecf Insert call logs for calls accepted by linked devices. 2021-10-04 21:49:59 -04:00
Greyson Parrelli 78704dce8a Add internal setting to force an emoji download. 2021-10-04 21:49:59 -04:00
Alex Hart 7f3ba1978d Add RedeemReceiptRequest object and DonationService. 2021-10-04 21:49:59 -04:00
Alex Hart 891dfc1b68 Upgrade zkgroups to 0.8.2 2021-10-04 21:49:59 -04:00
Cody Henthorne b0ccb543d1 Update thread archive status when sending media. 2021-10-04 21:49:59 -04:00
Alex Hart 7752b3aba3 Move FiatMoney object to core-util module. 2021-10-04 21:49:59 -04:00
Cody Henthorne 0fa13eb097 Fix overlap by not inlining messages with errors. 2021-10-04 21:49:59 -04:00
Cody Henthorne 641db1cbe2 Fix navigation crashes in registration and manage profile. 2021-10-04 21:49:59 -04:00
Alex Hart 8d53c2392a Update zkgroup to v0.8.1 2021-10-04 21:49:59 -04:00
Alex Hart 8d0acb277c Add support for updated server badge image url formats. 2021-10-04 21:49:59 -04:00
Greyson Parrelli 6e00920c95 Bump version to 5.24.14 2021-10-04 21:47:58 -04:00
Greyson Parrelli 13638dc1c9 Updated language translations. 2021-10-04 21:43:05 -04:00
Greyson Parrelli 1222d020ad Fix address list for sender key messages. 2021-10-04 20:50:08 -04:00
Greyson Parrelli d82b1ec69b Bump version to 5.24.13 2021-10-02 16:14:56 -04:00
Greyson Parrelli 8052c13526 Updated language translations. 2021-10-02 16:14:33 -04:00
Greyson Parrelli ed8538547f Improve handling of badly-serialized data.
h/t @i-infra
2021-10-02 16:06:58 -04:00
Cody Henthorne eb8de536e0 Bump version to 5.24.12 2021-10-01 15:29:47 -04:00
Cody Henthorne 76728c43e0 Updated language translations. 2021-10-01 15:18:26 -04:00
Alex Hart 52cfb57d36 Fix color offset on devices with notches. 2021-10-01 15:11:33 -04:00
Greyson Parrelli a385cb0b68 Dedupe network and identity failures. 2021-10-01 15:11:33 -04:00
Greyson Parrelli e01381379c Revert back to prod SQLCipher. 2021-10-01 15:11:33 -04:00
Cody Henthorne d01a52c5a8 Fix truncation calculation by accounting for compound drawables. 2021-10-01 12:29:46 -04:00
Cody Henthorne 204fff1b9b Fix registration enter phone number bug. 2021-10-01 10:23:35 -04:00
Cody Henthorne 1eda1477a8 Bump version to 5.24.11 2021-09-30 14:49:16 -04:00
Cody Henthorne 3135685c0e Updated language translations. 2021-09-30 14:44:42 -04:00
Greyson Parrelli 58fdb26f04 Update emoji dataset.
Includes some previously-missing gender neutral emoji.
2021-09-30 14:29:41 -04:00
Alex Hart 9bcb1bad8e Translate message details projection to correct coordinate system. 2021-09-30 13:00:06 -03:00
Alex Hart eb6ef3d005 Fix NPE when viewHolder has been removed from RecyclerView 2021-09-30 09:07:52 -03:00
Cody Henthorne f40ba0bf68 Prevent starting 1:1 call with a group recipient. 2021-09-29 16:44:21 -04:00
Cody Henthorne 89df0a2c04 Fix talkback crashes on EmojiTextView. 2021-09-29 16:22:21 -04:00
Cody Henthorne 69fbd4f3fc Fix bug with autoselecting wired headset for calls. 2021-09-29 16:17:52 -04:00
Cody Henthorne 45267f3590 Bump version to 5.24.10 2021-09-29 13:30:50 -04:00
Cody Henthorne 3fb8c6eda8 Updated language translations. 2021-09-29 13:26:03 -04:00
Cody Henthorne 27ce0fd65e Fix overlapping text when message contains mixed LTR and RTL text.
Fixes #11638
2021-09-29 13:17:58 -04:00
Alex Hart 7e91132e7e Fix multiple chatcolors issues from beta feedback.
- Fix issue where custom color would come out as black
- Completely remove mask view in favour of using the item decoration.
- Fix issue where video gifs wouldn't "cut through" bubble.
- Fix issue where multiselect shade would only appear if bottom or top item was not visible
2021-09-29 13:17:58 -04:00
Cody Henthorne 705839068a Fix crash when forwarding unknown media types. 2021-09-29 13:17:57 -04:00
Alex Hart 6625ac02d5 Fix NPE when eventListener is not set. 2021-09-29 13:17:57 -04:00
Alex Hart 4b3580d98a Fix issue where mentions did not propagate in message send flow. 2021-09-29 13:17:57 -04:00
Cody Henthorne 6dbbec2631 Bump version to 5.24.9 2021-09-28 17:22:57 -04:00
Cody Henthorne a7b6ebe7fc Updated language translations. 2021-09-28 17:19:03 -04:00
Cody Henthorne 76f52b9086 Fix various bugs around unread counts and scroll to bottom. 2021-09-28 17:12:25 -04:00
Greyson Parrelli 3310246351 Inline MP4 GIF flag.
This reverts commit 91645e6adc.
2021-09-28 17:12:25 -04:00
Alex Hart f3d0b4a671 Fix incorrect gradient rotation. 2021-09-28 17:12:25 -04:00
Cody Henthorne 83b9fbac11 Bump version to 5.24.8 2021-09-28 11:53:40 -04:00
Cody Henthorne 5ca843825f Updated language translations. 2021-09-28 11:48:40 -04:00
rainlion e92c83401b Fix a bug that unchanged returns true even if TransformationMethod is changed. 2021-09-28 11:42:51 -04:00
Fumiaki Yoshimatsu e18d9e665f Take padded bytes into account when decrypting a stream of data.
Fixes #11573
2021-09-28 11:42:51 -04:00
Greyson Parrelli cc99febe32 Allow use of the new CDSH service in staging. 2021-09-28 11:42:51 -04:00
Greyson Parrelli e72be42eff Put SMS messages in a separate sending queue. 2021-09-28 11:42:51 -04:00
Alex Hart bad382e2f3 Fix stretchy chat colors on Android 12. 2021-09-28 11:42:51 -04:00
Cody Henthorne e637f15a43 Refactor call audio routing and bluetooth management. 2021-09-28 11:42:51 -04:00
Cody Henthorne 6c55916cda Fix backup restore moving forward when backgrounded. 2021-09-28 11:42:51 -04:00
Greyson Parrelli fbabab0b70 Track down issues around empty preupload results. 2021-09-28 11:42:50 -04:00
Alex Hart e268887255 Fix crash if animating view was removed from parent. 2021-09-28 11:42:50 -04:00
Alex Hart 6b07922757 Add error logging for media gallery objects. 2021-09-28 11:42:50 -04:00
Alex Hart a464e57079 Fix media session reconnect issue for some devices. 2021-09-27 09:28:53 -03:00
Alex Hart b5af691cc4 Add badges to Avatars in a variety of places. 2021-09-24 13:39:28 -03:00
Alex Hart 5c1b57e4ba Implement ExoPlayerPool for better reuse and performance. 2021-09-24 13:10:48 -03:00
Greyson Parrelli a5c51ff801 Handle exception when reading from the log database. 2021-09-24 11:57:03 -04:00
Christelle Gloor d755e1e29e Set onClick to entire row, not just the checkbox. 2021-09-24 11:29:59 -03:00
Alex Hart b9361112b6 Resize the image when entering crop mode. 2021-09-24 10:58:37 -03:00
Greyson Parrelli 32101f7dda Update reaction text for GIFs. 2021-09-24 09:27:54 -04:00
Alex Hart 29e697265c Do not try to start next activity if we are not attached. 2021-09-24 09:21:09 -03:00
Alex Hart 4cd9ccc0f1 Fix crash when blocking and leaving a spam group. 2021-09-24 09:13:56 -03:00
Alex Hart 8936d81bc7 Fix 4.4 crash in image editor. 2021-09-23 17:12:14 -03:00
Alex Hart cc36f83d77 Fix horizontal translation of video player when in multiselect mode. 2021-09-23 14:49:13 -03:00
Greyson Parrelli 64996a8db7 Register mavenLocal() repo for all projects. 2021-09-23 11:35:21 -03:00
Greyson Parrelli 7267d77dcb Add support for syncing default reactions. 2021-09-23 11:35:21 -03:00
Greyson Parrelli 2281e83607 Log RecipientId for MissingAddressErrors. 2021-09-23 11:35:21 -03:00
Alex Hart e6b03b1a4a Implement ability to select featured badge to display on profile. 2021-09-23 11:35:21 -03:00
AsamK fb86fdfcd9 Fix syncing reactions in note to self to linked devices.
Fixes #11027
2021-09-23 11:35:21 -03:00
Alex Hart 77cf029fdc Implement ability to view badges and modify whether they appear.
Note: this is available in staging only.
2021-09-23 11:35:21 -03:00
Alex Hart 556ca5a573 Bump version to 5.24.7 2021-09-23 11:32:51 -03:00
Alex Hart 91645e6adc Revert "Inline MP4 GIF flag."
This reverts commit e2e0caa94a.
2021-09-23 11:17:54 -03:00
Alex Hart 4d6bb95aa4 Bump version to 5.24.6 2021-09-23 10:09:22 -03:00
Alex Hart 55ee68fa2d Updated language translations. 2021-09-23 10:08:38 -03:00
Alex Hart 747bc7c3bf Swap out expiring pinned mobilecoin cert. 2021-09-23 09:48:39 -03:00
Alex Hart 9c17201eaf Bump version to 5.24.5 2021-09-22 16:40:02 -03:00
Alex Hart a24b3d9a60 Updated language translations. 2021-09-22 16:39:38 -03:00
Alex Hart c93d882fe1 Don't allow API<23 to display gif videos in conversation list. 2021-09-22 16:21:13 -03:00
Alex Hart 67403a6a9f Bump version to 5.24.4 2021-09-21 17:00:21 -03:00
Alex Hart 5f9e72bb3c Updated language translations. 2021-09-21 16:59:58 -03:00
Greyson Parrelli 091b38ceb8 Use the GIF content type for quoted MP4 GIFs. 2021-09-21 15:52:08 -04:00
Cody Henthorne 83dfb984fb Update to RingRTC v2.13.1 2021-09-21 15:41:01 -04:00
Greyson Parrelli 9f14831fc4 Do not crash on issues with the log database. 2021-09-21 14:16:31 -04:00
Alex Hart 48bfcc9b16 Bump version to 5.24.3 2021-09-21 13:31:24 -03:00
Alex Hart 7028ca9411 Updated language translations. 2021-09-21 13:30:48 -03:00
Cody Henthorne 5175375483 Fix crash when getting update body on main thread. 2021-09-21 11:17:31 -04:00
Greyson Parrelli e2dbaa605b Fix potential stack overflow when getting identity record. 2021-09-21 09:16:58 -04:00
Alex Hart 93fd6e7a55 Fix issue with media controller lifecycle.
We were connecting and disconnecting in onStart and onStop,
which can get called in different orders depending on what the
system does. This results in sometimes trying to connect to an
already connected media session.
2021-09-21 10:09:43 -03:00
Alex Hart b070e6962f Remove view from parent before trying to insert into a new container. 2021-09-21 10:03:42 -03:00
Alex Hart b1d1b7e31e Fix NullPointerException when getting ringtone title. 2021-09-21 09:57:59 -03:00
Alex Hart 1a5ae603d5 Bump version to 5.24.2 2021-09-20 16:25:44 -03:00
Alex Hart 3b42bda63d Updated language translations. 2021-09-20 16:25:44 -03:00
Alex Hart 2f70a71a6c Fix several kotlin formatting issues from bug fixes. 2021-09-20 16:25:44 -03:00
Cody Henthorne aae368c049 Clear profile upload flag when unregistering. 2021-09-20 16:25:44 -03:00
Alex Hart dee3d2ff2d Fix restrictive sizing of speed toggle in voice note bar. 2021-09-20 16:25:44 -03:00
Alex Hart da2e2a99af Remove outdated stableId pattern from ConversationAdapter. 2021-09-20 11:53:47 -03:00
Alex Hart 0c00426c0c Fix internal preference issue with creating a clipboard service. 2021-09-20 11:41:04 -03:00
Alex Hart ccc96d5bfa Fix gif display when list is changed and view holders are not reused. 2021-09-20 11:19:33 -03:00
Alex Hart 82c12c2f6b Do not allow content to play if no media item is available. 2021-09-20 10:52:48 -03:00
Alex Hart 9416beb4aa Trigger pending transition at right time for video gifs. 2021-09-20 10:35:01 -03:00
Alex Hart 39b80a48c7 Ensure message details video container matches placement of recycler. 2021-09-20 10:22:43 -03:00
Alex Hart d5491a2e84 Fix vector load crash on Kitkat.
Fixes #11628
2021-09-20 10:19:37 -03:00
Alex Hart 07b19402e6 Fix wallpaper gallery toolbar behaviour.
Fixes #11619
2021-09-20 10:12:59 -03:00
Alex Hart 318b4703f2 Bump version to 5.24.1 2021-09-17 16:15:26 -03:00
Alex Hart d389697f27 Updated language translations. 2021-09-17 16:14:25 -03:00
Alex Hart 7bcc338a49 Implement radial dial.
Co-authored-by: Alan Evans <alan@signal.org>
2021-09-17 13:09:13 -03:00
Cody Henthorne ce2c2002c6 Revert thread updates to running inline again. 2021-09-17 11:50:46 -04:00
Greyson Parrelli d5fbd10406 Create a SignalDataSource class for all of our ExoPlayer needs.
Also fixes an issue around GIF playback within a conversation.
2021-09-17 09:58:11 -04:00
Cody Henthorne 6f6da699a3 Fix groups not showing after pin restore. 2021-09-17 09:56:49 -04:00
Cody Henthorne 62d8c115ba Enable group call notification settings when group ringing is enabled. 2021-09-17 09:53:28 -04:00
Alex Hart fd01ee2a87 Add stopwatches for a few possible pain points in MediaGallery. 2021-09-16 16:29:51 -03:00
Cody Henthorne 9aa517ad99 Fix UI bugs in dark mode change number flow. 2021-09-16 14:14:38 -04:00
Greyson Parrelli 6c3e1b6a29 Add internal preference to disable storage syncing.
Added to help debug certain scenarios, particularly around working with
emulator snapshots, since storage sync will often bring in state from earlier
snapshots you weren't expecting.
2021-09-16 13:32:25 -04:00
Alex Hart 5d5063ef5f Bump version to 5.24.0 2021-09-16 14:17:38 -03:00
Alex Hart b48668455c Updated language translations. 2021-09-16 14:17:38 -03:00
Lucio Maciel 18ba5fa291 Fix emoji avatar missing after edit. 2021-09-16 14:17:38 -03:00
Cody Henthorne 5e968eb831 Prevent group leave event from bumping conversation. 2021-09-16 14:17:38 -03:00
Aaron Labiaga b4465953d8 Set LocusID on shortcut and notification for on device intelligence. 2021-09-16 14:17:38 -03:00
Ducros Alix 08a7da3339 Add greek characters to the accent insensitive search of names.
Fixes #11534
2021-09-16 14:17:38 -03:00
essentialols c1a08616ab Add 1.5x playback speed for voice messages. 2021-09-16 14:17:38 -03:00
Cody Henthorne 3761859681 Fix kotlin compiler warnings. 2021-09-16 14:17:38 -03:00
RiseT 8984b763fb Replace typographical apostrophe by standard one. 2021-09-16 14:17:38 -03:00
Alex Hart 59c62671b9 Do not launch ShareActivity as singleTask.
Fixes #11620
2021-09-16 14:17:38 -03:00
Alex Hart bcfe8909e5 Add image editor sample app. 2021-09-16 14:17:38 -03:00
Lucio Maciel c43fe44e3e Fix transformation method issues. 2021-09-16 14:17:38 -03:00
Greyson Parrelli 4ac1134a9b Point to a new remote emoji version file.
There was a bug in older versions around caching, so by switching to a
new version file, we can make sure only fixed versions get the new
emoji.
2021-09-16 14:17:38 -03:00
Greyson Parrelli 08d03cb456 Clear emoji cache after downloading a new set. 2021-09-16 14:17:38 -03:00
Greyson Parrelli e5c172a819 Turn off noisy eventbus logs.
Fixes #11617
2021-09-16 14:17:38 -03:00
Alan Evans 4569011e0b Two point thumb control for scale and rotate. 2021-09-16 14:17:38 -03:00
Greyson Parrelli 1031a4e96c Improve logging around message sending and processing. 2021-09-16 14:17:38 -03:00
Peter Thatcher cdf8e4e1ed Only try to connect to bluetooth a limited number of times in a call. 2021-09-16 14:17:38 -03:00
Alex Hart b589449c34 Consolidate app dependencies using gradle version catalogs. 2021-09-16 14:17:38 -03:00
Cody Henthorne 7d7dd101df Fix note bug on payment details. 2021-09-16 14:17:38 -03:00
Cody Henthorne e687fea567 Fix race condition overriding profile on registration. 2021-09-16 14:17:38 -03:00
Cody Henthorne e2cb522e87 Prevent part files from being deleted prematurely. 2021-09-16 14:17:38 -03:00
Alex Hart 662ba85c5a Upgrade to Gradle 7.2 and AGP 7.0.2 2021-09-16 14:17:38 -03:00
Greyson Parrelli d29ebc7768 Update included emoji to 13.1 2021-09-14 09:35:56 -04:00
Alex Hart 95fabd7ed1 Initial modularization of core image editor code. 2021-09-14 09:35:56 -04:00
Jim Gustafson 5d5251054c Update to RingRTC v2.13.0 2021-09-14 09:35:56 -04:00
Sgn-32 c766ba9808 Use more icons in ConversationListItem 2021-09-14 09:35:56 -04:00
Greyson Parrelli 8b5fe79849 Update our image viewer versions. 2021-09-14 09:35:56 -04:00
Greyson Parrelli 903c5c6db6 Add an internal recipient details screen. 2021-09-14 09:35:56 -04:00
Greyson Parrelli e2e0caa94a Inline MP4 GIF flag. 2021-09-14 09:35:56 -04:00
Greyson Parrelli 520fe481e9 Bump version to 5.23.7 2021-09-14 09:25:33 -04:00
Greyson Parrelli 7262aefa34 Updated language translations. 2021-09-14 09:24:54 -04:00
Greyson Parrelli 8815cdc3de Fix potential crash during notification processing. 2021-09-14 09:18:27 -04:00
Greyson Parrelli 8df86962e9 Fix potential crash with a bad group update body. 2021-09-14 09:08:04 -04:00
Greyson Parrelli 573c0fad7f Update libsignal-client to 0.9.4 2021-09-14 09:07:43 -04:00
Greyson Parrelli a8419d5f02 Fix potential crash when reading bad GV1 ids in block sync. 2021-09-14 08:54:07 -04:00
Greyson Parrelli 6088f16e3a Bump version to 5.23.6 2021-09-10 12:32:45 -04:00
Greyson Parrelli 1a5ae592a7 Updated language translations. 2021-09-10 12:32:17 -04:00
Greyson Parrelli 6880dfeb62 Show 'Note to Self' for yourself in the media send flow. 2021-09-10 12:24:26 -04:00
Cody Henthorne dfecb0efd8 Only show change number event when previous e164 known and different. 2021-09-10 12:12:07 -04:00
Greyson Parrelli 2eaadd4337 Allow multi-line text in media send flow. 2021-09-10 10:47:32 -04:00
Greyson Parrelli 655f3c1219 Bump version to 5.23.5 2021-09-09 17:57:08 -04:00
Greyson Parrelli 1494a3559d Stop broadcasting the change number capability. 2021-09-09 17:51:18 -04:00
Greyson Parrelli f61d7a9f77 Bump version to 5.23.4 2021-09-09 17:18:57 -04:00
Greyson Parrelli ee0ab8f035 Updated language translations. 2021-09-09 17:18:57 -04:00
Alex Hart 6e85c74e3f Adjust camera button arc width. 2021-09-09 16:08:26 -04:00
Alex Hart 3b1aa5b176 Add shade behind trash icon for better visibility on white images. 2021-09-09 16:08:26 -04:00
Alex Hart 715ad0d459 Add text styles support to image editor.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2021-09-09 16:08:26 -04:00
Greyson Parrelli 05f7dce503 Fix potentional ClassCastException. 2021-09-09 11:57:37 -04:00
Greyson Parrelli ecfbeb69c5 Allow images to be cached in the image editor. 2021-09-09 11:31:11 -04:00
Greyson Parrelli c7fb343b93 Fix undesireable undo behavior when deleting. 2021-09-09 11:07:21 -04:00
Greyson Parrelli b0d0814b88 Prevent multitouch from accidentally deleting stickers. 2021-09-09 11:07:21 -04:00
Cody Henthorne 9f3765d368 Fix some vector assets not rendering properly on older OS versions. 2021-09-09 10:55:05 -04:00
Lucio Maciel fe82c4e487 Fix image partially shown after message sent. 2021-09-09 11:47:46 -03:00
Alex Hart e9bbb1b9ae Fix fade when re-entering text edit. 2021-09-09 11:35:49 -03:00
Cody Henthorne fd3e88707c Fix preupload in new Media Send flow. 2021-09-09 10:06:49 -04:00
Greyson Parrelli 8e8def8b03 Fix crash when opening camera without storage permissions. 2021-09-09 09:44:22 -04:00
Lucio Maciel 5b1069018f Fix long chat name overlapping the timestamp. 2021-09-09 10:11:38 -03:00
Greyson Parrelli abc71f4fb4 Bump version to 5.23.3 2021-09-08 21:14:38 -04:00
Greyson Parrelli 2f5e95c0e3 Updated language translations. 2021-09-08 21:13:16 -04:00
Greyson Parrelli d1fd70a807 Fix delete button collision detection. 2021-09-08 21:08:22 -04:00
Greyson Parrelli 1b924c606a Use a dashed line for object highlighting. 2021-09-08 21:04:21 -04:00
Greyson Parrelli 9b175fa0dd Add some padding to text selection. 2021-09-08 21:04:21 -04:00
Cody Henthorne 7e7bbad788 Ensure change number operation status before returning to normal app usage. 2021-09-08 21:04:21 -04:00
Alex Hart d8c82add78 Increase color circle radius in slider. 2021-09-08 21:04:21 -04:00
Alex Hart 6e1657b1bd Clamp start time value to be >= 0 2021-09-08 21:04:21 -04:00
Alex Hart 1f7b1d91c4 Improve trash can using in-renderer object. 2021-09-08 21:04:21 -04:00
Greyson Parrelli e7833df539 Fix display of names with emojis in forward selection. 2021-09-08 21:04:21 -04:00
Alex Hart fae80a242d Update animation interpolators in media send flow. 2021-09-08 21:04:06 -04:00
Cody Henthorne 77ff25ec49 Add Change Number capability and Conversation Update item. 2021-09-08 21:04:06 -04:00
Greyson Parrelli bb446ac1d5 Update SQLCipher to 4.4.3-S3 2021-09-08 21:04:05 -04:00
Cody Henthorne b6a4d01d42 Fix QR scan crash and add exchange data fallback for Create Payment. 2021-09-08 21:04:05 -04:00
Alex Hart bd4dd25460 Add brush width preview. 2021-09-08 21:04:05 -04:00
Alex Hart f86c1fe508 Support different width ranges for different brushes. 2021-09-08 21:04:05 -04:00
Alex Hart 38f6efbcae Fix NPE in VideoPlayer error handler. 2021-09-08 08:34:13 -03:00
Greyson Parrelli 30a542234b Bump version to 5.23.2 2021-09-07 23:13:34 -04:00
Greyson Parrelli 8c9bf678fa Updated language translations. 2021-09-07 23:13:34 -04:00
Greyson Parrelli 4b465b74e8 Save message in media flow as you type. 2021-09-07 23:13:19 -04:00
Greyson Parrelli 58a22f0eea Add black and white to the color picker. 2021-09-07 23:13:19 -04:00
Greyson Parrelli ddad9acef1 Add support for drag + drop in the media send flow. 2021-09-07 23:13:19 -04:00
Lucio Maciel 1dbb6013cb Fix alignment on Update messages. 2021-09-07 23:13:19 -04:00
Lucio Maciel 9cc1ae4a29 Fix Verify Identity screen on smaller devices. 2021-09-07 23:13:19 -04:00
Alex Hart 4eb24c3303 Add fade below text layer when editing text. 2021-09-07 23:13:19 -04:00
Alex Hart ec1935572e Fix bug where dialog would not dismiss after toggling between keyboards. 2021-09-07 23:13:19 -04:00
Alex Hart e419d70d51 Do not crash when we try to play from IDLE state. 2021-09-07 23:13:19 -04:00
Alex Hart 2af5526879 Add new flash icons. 2021-09-07 23:13:19 -04:00
Alex Hart 6a5aa089ae Fix crash if sensors disabled in developer mode. 2021-09-07 23:13:19 -04:00
Alex Hart 6b5f4ca8c2 Fix onBackPressed / toolbar navigation behaviour in MediaGalleryFragment. 2021-09-07 23:13:19 -04:00
Alex Hart 53e110560a Fix onBack behaviour of media gallery fragment. 2021-09-07 23:13:19 -04:00
Alex Hart 82e9c620e8 Show progress spinner if media send takes more than 300ms. 2021-09-07 23:13:19 -04:00
Lucio Maciel 076facbdc2 Fixes on Chat list. 2021-09-07 23:13:19 -04:00
Alex Hart a805f9b6b4 Utilize fast-in-extra-slow-out interpolator. 2021-09-07 23:13:19 -04:00
Alex Hart 969e763997 Fix several design feedback items for new media selection flow. 2021-09-07 23:13:19 -04:00
Alex Hart 9347227ff5 Reposition video editor and add new play button. 2021-09-07 23:13:19 -04:00
Cody Henthorne c9ba0432a0 Fix bug with currency localization. 2021-09-07 23:13:19 -04:00
Greyson Parrelli e3b7fe7509 Remove database notifications from within a transaction.
Having them in a transaction means there's a race where other threads
may not see the new database changes.
2021-09-07 23:13:13 -04:00
Cody Henthorne 5332669321 Potentially fix bad configuration change data with change to landscape. 2021-09-07 23:13:13 -04:00
Alex Hart a086305c38 Improve behaviour of media send flow in landscape. 2021-09-07 23:13:13 -04:00
Greyson Parrelli a712622891 Revert "Update URL for reaching Signal chat server."
This reverts commit 6179c087fb.
2021-09-07 22:58:17 -04:00
Alex Hart 1514f91687 Support deletion and guides when manipulating objects.
* Fix issue with avatar selection
* Remove save button on video editor screen (we never supported this)
* Fix mentions
2021-09-07 22:58:17 -04:00
Cody Henthorne 0dfa6aab09 Bump version to 5.23.1 2021-09-03 20:43:59 -04:00
Cody Henthorne 4b6dbac758 Updated language translations. 2021-09-03 20:38:17 -04:00
Cody Henthorne b816f901a5 Fix test for mac. 2021-09-03 20:33:03 -04:00
Lucio Maciel 76d1490810 Adjust conversation list item height and name margin. 2021-09-03 20:19:56 -04:00
Cody Henthorne f2ab0b6423 Initial work to support Change Number. 2021-09-03 20:19:56 -04:00
Lucio Maciel e09d162c1e Update conversations list UI. 2021-09-03 20:19:55 -04:00
Greyson Parrelli c84de8fa60 Add a cache for GIFs. 2021-09-03 20:19:55 -04:00
Greyson Parrelli 8e020c05f6 Improve IdentityDatabase e164 check. 2021-09-03 09:15:08 -04:00
Greyson Parrelli 8c9eb880cf Bump version to 5.23.0 2021-09-02 21:36:18 -04:00
Greyson Parrelli d7ddd85a90 Updated language translations. 2021-09-02 21:35:27 -04:00
Alex Hart 7d994b2ae1 Set proper money separator when presenting custom amount string to user in MoneyView. 2021-09-02 21:24:54 -04:00
Alex Hart 664d6475d9 Refresh media selection and sending flow with a shiny new UX. 2021-09-02 21:24:54 -04:00
Greyson Parrelli a940487611 Improve logging around rate-limiting. 2021-09-02 21:24:54 -04:00
Sgn-32 9f995d61f4 Fix padding for Payments icon and title. 2021-09-02 21:24:54 -04:00
Leonid Zavodnik a6690e1bde Update exoplayer version to v2.15
Fixes #11547
2021-09-02 21:24:54 -04:00
Greyson Parrelli d507df2e7e Increase max log size to 15mb. 2021-09-02 21:24:54 -04:00
Greyson Parrelli fa26eb2017 Switch back to mainline SQLCipher with true WAL mode. 2021-09-02 21:24:54 -04:00
Greyson Parrelli 0b53ba8950 Improve MMS database insertion performance. 2021-09-02 21:24:54 -04:00
Greyson Parrelli 7447e2497b Default the retry receipt flag to true. 2021-09-02 21:24:54 -04:00
Greyson Parrelli 7ac83625d3 Add a write-through cache to the identity store. 2021-09-02 21:24:53 -04:00
Cody Henthorne 50dfe7bc25 Update Staging KBS values. 2021-09-02 21:24:53 -04:00
Cody Henthorne 8e32592218 Clarify networking call order during registration flow. 2021-09-02 21:24:53 -04:00
Lucio Maciel a3d72fc06c Update message details UI. 2021-09-02 21:24:53 -04:00
Greyson Parrelli f5a6d61362 Add support for granular conversation data changes. 2021-09-02 21:24:53 -04:00
Greyson Parrelli bca2205945 Add measurements, improve MSL insert. 2021-09-02 21:24:53 -04:00
Alex Hart 1241f4c0e9 Enable MobileCoin in Germany, France, and Switzerland. 2021-09-02 21:24:53 -04:00
Graham Campbell f6253ad0bb Corrected Google trademark notice 2021-09-02 21:24:53 -04:00
Lucio Maciel 083301185c Update verify identity UI. 2021-09-02 21:24:53 -04:00
Lucio Maciel 0273d0f285 Save receipt timestamps on sms/mms database. 2021-09-02 21:24:53 -04:00
Cody Henthorne 3dc1ce3353 Bump version to 5.22.7 2021-09-02 16:44:02 -04:00
Cody Henthorne f8e077b824 Updated language translations. 2021-09-02 16:43:30 -04:00
Greyson Parrelli aec2ca1d87 Update libsignal-client to 0.9.0 2021-09-02 11:21:15 -04:00
Cody Henthorne 6e7a18ea11 Bump version to 5.22.6 2021-09-01 12:55:04 -04:00
Cody Henthorne fe54ec9d6c Updated language translations. 2021-09-01 12:49:23 -04:00
Greyson Parrelli 1819af3000 Fix possible crash when a contact merge results in no UUID.
After merging contacts, it's possible that we don't have a valid
UUID. We need to be careful not to use it.

Kind of a bummer, but the storage sync flow is currently the only flow
where we have this 'possibly not valid UUID'. In the future we should
probably use something else instead of a SignalServiceAddress to keep
that abstraction clean.
2021-09-01 10:46:42 -04:00
Cody Henthorne 3c177c4883 Bump version to 5.22.5 2021-08-31 10:18:33 -04:00
Cody Henthorne 2c871a36d0 Updated language translations. 2021-08-31 10:18:10 -04:00
Greyson Parrelli 6bde55f73b Only check remote registrationIds for active records. 2021-08-31 09:46:37 -04:00
Cody Henthorne 50b4e137b4 Bump version to 5.22.4 2021-08-30 20:43:11 -04:00
Cody Henthorne 4f6d39859c Updated language translations. 2021-08-30 20:38:20 -04:00
Greyson Parrelli 45a6894da1 Handle invalid registrationIds during sender key sends. 2021-08-30 20:32:41 -04:00
Alex Hart f71accea06 Revert "Replace use of AlertDialog.Builder with MaterialAlertDialogBuilder."
This reverts commit 9232eb7c16.
2021-08-30 20:32:41 -04:00
Greyson Parrelli 32888fa00b Re-enabled converation list observation while a conversation is open.
It honestly doesn't feel great to not have this, because when you back
out to the conversation list you have to wait for it to update.

Right now this seems like the lesser of two evils.
2021-08-30 20:32:41 -04:00
Greyson Parrelli eba3c55ec8 Fix issue where you couldn't delete a blocked announcement group. 2021-08-30 11:50:07 -04:00
Greyson Parrelli 21b82e291b Fix crash when building local e164-only contact record.
Fixes #11572
2021-08-30 10:03:18 -04:00
Alex Hart 8d9d84c4cc Add drawable padding to contact item. 2021-08-30 09:34:18 -03:00
Alex Hart 4c25264fbf Fix issue with conversation list invalidation. 2021-08-30 09:21:26 -03:00
Alex Hart 7410d664dd Bump version to 5.22.3 2021-08-27 14:43:38 -03:00
Alex Hart c878ba3cdf Updated language translations. 2021-08-27 14:43:38 -03:00
Greyson Parrelli 97798a146f Fix issue where request banner overlapped admin-only banner. 2021-08-27 14:43:38 -03:00
Greyson Parrelli 7c134a6c9d Fix issue where group leave failed to send in announcement group. 2021-08-27 14:43:38 -03:00
Greyson Parrelli 08008629b3 Fix some issues around SignalServiceAddress creation. 2021-08-27 14:43:38 -03:00
Greyson Parrelli a57adcb2b0 Remove identity store cache. 2021-08-27 14:43:38 -03:00
Alex Hart 7790cac0ee Invalidate conversation list when it is not newly started. 2021-08-27 14:43:38 -03:00
Alex Hart 349ad06c45 Fix crash when animation ends after onDestroyView. 2021-08-27 14:43:38 -03:00
Alex Hart 3a75d30732 Remove requireContext call from async runnable. 2021-08-27 09:10:54 -03:00
Alex Hart b48d4f3ec2 Bump version to 5.22.2 2021-08-26 17:41:09 -03:00
Alex Hart c92f36f9a8 Updated language translations. 2021-08-26 17:39:57 -03:00
Greyson Parrelli faa36d417c Switch back to mainline SQLCipher. 2021-08-26 16:05:52 -04:00
Alex Hart a2b6e003b6 Potential fix for bad contacts. 2021-08-26 16:42:40 -03:00
AsamK 406af58394 Use EmojiTextView to display group names in AvatarPreviewActivity. 2021-08-26 15:38:42 -03:00
Greyson Parrelli bd72fc8464 fixup! Revert some database transaction changes. 2021-08-26 12:06:28 -04:00
Greyson Parrelli 05fb1a52d2 Revert some database transaction changes. 2021-08-26 11:59:45 -04:00
Greyson Parrelli b21abb8e7e Fix crash during block list parsing. 2021-08-26 09:51:28 -04:00
Alex Hart b41e602539 Add hasGroupsInCommon to Recipient content check. 2021-08-26 10:46:06 -03:00
Alex Hart 3f233ed39f Use AttachmentsV2 if the resumable upload link from V3 becomes corrupted. 2021-08-26 10:24:20 -03:00
Alex Hart ade6f60e76 Skip attachment template if digest is null. 2021-08-26 10:14:12 -03:00
Greyson Parrelli 62d85e6878 Stop listening to database changes in conversation list when not visible. 2021-08-25 19:47:48 -04:00
Greyson Parrelli 4d985255a8 Fix deviceId log for retry receipts. 2021-08-25 19:33:50 -04:00
Alex Hart fd3ef0f557 Bump version to 5.22.1 2021-08-25 17:20:48 -03:00
Alex Hart 7f30300cd4 Updated language translations. 2021-08-25 17:20:48 -03:00
Greyson Parrelli 0459d118a3 Enable sender key by default. 2021-08-25 17:20:48 -03:00
Lucio Maciel c92f3b5dfd Fix theming on invite friends Activity. 2021-08-25 16:05:20 -03:00
Greyson Parrelli ba4d1c9844 Add a failsafe to prevent non-admin sends in announcement groups. 2021-08-25 14:20:49 -04:00
Greyson Parrelli 8c3a0c1f9f Fix crash after a backup restore. 2021-08-25 13:56:22 -04:00
Greyson Parrelli 1dc2a35d83 Fix overlapping text for not-in-group and announcement-only. 2021-08-25 13:52:19 -04:00
Greyson Parrelli 0a67731830 Add a write-through cache to the identity store. 2021-08-25 13:39:59 -04:00
Greyson Parrelli 28d86886bd Update handling of invalid unknown fields. 2021-08-25 13:34:29 -04:00
Greyson Parrelli b1fcea673a Allowing joining group calls in announcement groups. 2021-08-25 13:21:11 -04:00
Greyson Parrelli eb5418787a Disable the reply action in announcement groups. 2021-08-25 13:19:52 -04:00
Cody Henthorne adbda02aa4 Fix minor Group Call Ringing UI bugs. 2021-08-25 13:13:25 -04:00
Greyson Parrelli 307f47fa33 Prevent forwarding to announcement groups in new forward fragment. 2021-08-25 12:38:14 -04:00
Cody Henthorne c1fb4f9421 Include urgency in opaque call message sends. 2021-08-25 09:28:16 -04:00
Ehren Kret 6179c087fb Update URL for reaching Signal chat server. 2021-08-24 17:41:09 -04:00
Alex Hart ae2ba5d185 Bump version to 5.22.0 2021-08-24 16:59:09 -03:00
Alex Hart 91128be8f6 Updated language translations. 2021-08-24 16:59:09 -03:00
Greyson Parrelli 8748056130 Inline the announcement groups flag. 2021-08-24 16:59:09 -03:00
Greyson Parrelli 3c4e3cf048 Improve retrieval from the identity table. 2021-08-24 16:59:09 -03:00
Greyson Parrelli eb48ab1784 Disallow marking users as registered without a UUID. 2021-08-24 16:59:09 -03:00
Greyson Parrelli 665d9e31f6 Separate thread updates into a job and other perf improvements. 2021-08-24 16:59:09 -03:00
Cody Henthorne db7272730e Add Small Group Ringing support. 2021-08-24 16:59:09 -03:00
Greyson Parrelli 5787a5f68a Improve conversion of Recipient to SignalServiceAddress. 2021-08-24 16:59:09 -03:00
Alex Hart 1a21cafe6c Remove multi-forward feature flag. 2021-08-24 16:59:09 -03:00
Greyson Parrelli 7465818f44 Fix crash where we required a UUID from an unregistered user. 2021-08-24 16:59:09 -03:00
Lucio Maciel 62cb29fdb7 Update Invite friends screen UI. 2021-08-24 16:59:09 -03:00
Greyson Parrelli a85b08d9da Added an internal setting for disabling shake to report. 2021-08-24 16:59:09 -03:00
Greyson Parrelli b18c3ec1a9 Update filtered executor in LiveRecipientCache. 2021-08-24 16:59:09 -03:00
Greyson Parrelli 29489a664e Fix issue where synced media messages weren't downloading.
There was race where the AttachmentDownloadJob was run during a
transaction, which meant that it might not be able to see the message
that was just inserted.

Gotta be more careful now with WAL mode.
2021-08-24 16:59:09 -03:00
Greyson Parrelli dbb1e50d00 Migrate the identity table to be keyed off of libsignal IDs. 2021-08-24 16:59:09 -03:00
Greyson Parrelli 2068fa8041 Several sender key performance improvements.
- Remove extra unnecessary sync message
- Add a bulk session retrieval method
- Do the encrypt in a transaction
2021-08-24 16:59:09 -03:00
Cody Henthorne 194975d068 Fix lobby copy when another of your devices is solely already in the group call. 2021-08-24 09:09:27 -03:00
Greyson Parrelli b7a067e954 Use a more accurate starting point for message send timings. 2021-08-24 09:09:27 -03:00
Greyson Parrelli 1e050915ef Clean up unmigrated groups after recipient merge. 2021-08-24 09:09:27 -03:00
Alex Hart 6a5c234408 Always recalculate shown items when we update menu state in multiselect. 2021-08-24 09:09:27 -03:00
Alex Hart 7a1122b3f7 Force ConversationItem to intercept all touch events when in multiselect mode. 2021-08-24 09:09:27 -03:00
Sgn-32 962d943a22 Pretty print phone numbers of blocked users in privacy settings. 2021-08-24 09:09:27 -03:00
Goldmaster dbcc5d696d Update README.md copyright year and links.
add link to apk download from signals website in the readme

updated the copyright to the current year.
2021-08-24 09:09:27 -03:00
Sgn-32 9232eb7c16 Replace use of AlertDialog.Builder with MaterialAlertDialogBuilder. 2021-08-24 09:09:27 -03:00
Greyson Parrelli fc9b8f43dd Fix GV2 storage sync crash.
Fixes #11459
2021-08-24 09:09:27 -03:00
Lucio Maciel 5e8d74bc11 Fix lock screen issues. 2021-08-24 09:09:27 -03:00
Greyson Parrelli 642d1984c4 Ensure all SignalServiceAddresses have UUIDs. 2021-08-24 09:09:27 -03:00
Greyson Parrelli 0ab2100fa5 Update libsignal-client to 0.8.4 2021-08-24 09:09:27 -03:00
Greyson Parrelli 6618d696e4 Migrate the session table to be keyed off of libsignal IDs. 2021-08-24 09:09:27 -03:00
Greyson Parrelli c24dfdce34 Use a more readable method of listing selectable variants. 2021-08-24 09:09:27 -03:00
Greyson Parrelli 214e994e90 Update to SQLCipher with true WAL support. 2021-08-24 09:09:27 -03:00
Greyson Parrelli b904de5b50 Remove unused gradle code. 2021-08-24 09:07:54 -03:00
Ehren Kret ad7c81ef4e Limit JCenter dependencies 2021-08-24 09:07:54 -03:00
Alex Hart 3e8b5cdb61 Bump version to 5.21.6 2021-08-23 15:49:52 -03:00
Alex Hart 6aea849a42 Updated language translations. 2021-08-23 15:49:01 -03:00
Cody Henthorne cd0bf470a9 Fix applying default timer to first media message. 2021-08-23 10:18:42 -04:00
Greyson Parrelli c615b14c51 Bump version to 5.21.5 2021-08-19 21:19:56 -04:00
Greyson Parrelli 28bf6d300e Updated language translations. 2021-08-19 21:19:56 -04:00
Greyson Parrelli a1095f966c Do the account restore within a transaction. 2021-08-19 21:19:56 -04:00
Alex Hart 58a8902d4e Only finish action mode after forwards are sent. 2021-08-19 21:14:10 -04:00
Alex Hart e582976293 Fix issue with bad multiselect inset. 2021-08-19 15:34:14 -03:00
Alex Hart 143110047d Change counter to consider only unique conversation messages in multiselect. 2021-08-19 15:22:21 -03:00
Alex Hart c1324c7496 Fix check indicator covering update in multiselect. 2021-08-19 15:17:41 -03:00
Lucio Maciel 53eee2bd16 Fix timestamps with image+text. 2021-08-18 16:10:52 -03:00
Greyson Parrelli 86b1d104d9 Bump version to 5.21.4 2021-08-18 10:48:13 -04:00
Greyson Parrelli d1d2376210 Updated language translations. 2021-08-18 10:48:13 -04:00
Alex Hart 7bede7e98a Fix issue where forwarded messages would show unlock icon. 2021-08-18 10:48:13 -04:00
Lucio Maciel fec4a7692d Collapse timestamps on "deleted" messages. 2021-08-18 10:48:09 -04:00
Greyson Parrelli b58cede072 Fix issue with date header ID generation.
We render based on the date received, but were generating the ID with
the date sent. This caused the potential for a weird caching bug that
could cause us to render the wrong date.
2021-08-18 10:01:33 -04:00
Alex Hart 199fb517b1 Fix dark theme coloring for forward bottom sheet. 2021-08-18 09:33:29 -03:00
Alex Hart 921addf4c8 Fix error with vertical translation of quote cutout projection. 2021-08-18 09:33:29 -03:00
Greyson Parrelli 61aa991d79 Increase toast duration for forward error messages. 2021-08-18 08:32:21 -04:00
Alex Hart c1c95e1ae2 Disable predictive animation support on conversation layout manager. 2021-08-18 09:02:29 -03:00
Greyson Parrelli f95a29b0d4 Bump version to 5.21.3 2021-08-17 20:15:01 -04:00
Greyson Parrelli f7bb9c85af Updated language translations. 2021-08-17 20:14:35 -04:00
Greyson Parrelli ae30e4070c Default retry respond max age to 14 days. 2021-08-17 20:14:35 -04:00
Lucio Maciel 9a67c60b4e Don't inline jumbomoji timestamps. 2021-08-17 19:04:59 -04:00
Cody Henthorne e86b26bd11 Give call button text a bit more room and fix centering issue. 2021-08-17 16:46:05 -04:00
Lucio Maciel e7c259b1e9 Adjust timestamp alignment. 2021-08-17 17:22:23 -03:00
Alex Hart c65761a034 Fix several issues with multiforwarding.
* Better forwarding and animations.
* Handle audio with text.
* Increase max forwardable count to 32
* Onboarding dialog.
* Send forth link previews.
* Safety number support.
* Fix slide behaviour.
2021-08-17 16:15:09 -03:00
Alex Hart 0b37b0ee16 Fix crash with detached fragment. 2021-08-17 15:17:23 -03:00
Cody Henthorne d76e58ce09 Fix crash when updating empty thread on failed send. 2021-08-17 10:58:57 -04:00
Lucio Maciel 2b366f8c9c Fix audio with text footer. 2021-08-17 11:09:22 -03:00
Greyson Parrelli d43f7d6ad9 Bump version to 5.21.2 2021-08-16 21:22:09 -04:00
Greyson Parrelli 5b7932281e Updated language translations. 2021-08-16 21:18:15 -04:00
Lucio Maciel 0599f76ed5 Fix alignment issues for single line timestamps. 2021-08-16 20:50:33 -04:00
Niel Thiart 31e0f3edfb Fix Signal Direct Share Shortcuts not appearing in Android Sharesheet.
Fixes #11537
2021-08-16 20:50:33 -04:00
Alex Hart 17b568e6d1 Fix sticker forwarding. 2021-08-16 20:50:33 -04:00
Alex Hart 7c11962cb3 Fix custom notification vibration state. 2021-08-16 20:50:33 -04:00
Alex Hart a7c4199192 Add proper pluralization to message send toast. 2021-08-16 12:00:19 -03:00
Alex Hart 8cb3909093 Disable multiforward send button after the user presses it. 2021-08-16 11:50:53 -03:00
Alex Hart 7480ea66ec Fix issue where a document with text would cause a crash and not be multiselectable. 2021-08-16 11:36:03 -03:00
Cody Henthorne 8e94ced7b6 Bump version to 5.21.1 2021-08-13 17:50:08 -04:00
Cody Henthorne ffd86a96da Updated language translations. 2021-08-13 17:47:25 -04:00
Lucio Maciel d4cabce876 Fix crash when getLayout() is null. 2021-08-13 18:39:06 -03:00
Cody Henthorne a5790edb2b Bump version to 5.21.0 2021-08-13 14:17:32 -04:00
Cody Henthorne d247e2eabe Updated language translations. 2021-08-13 14:09:42 -04:00
Cody Henthorne f4d6de466b Fix long SMS send with no service failure loop. 2021-08-13 13:58:38 -04:00
Cody Henthorne 0838c0be27 Fix crash when sending media message as first message in conversation. 2021-08-13 13:58:38 -04:00
Alex Hart 7448183ff4 Update multi-forward work with tweaks from design. 2021-08-13 13:58:38 -04:00
Lucio Maciel 8e2a265cf3 Update emoji search index on system locale changes. 2021-08-13 13:58:38 -04:00
Cody Henthorne 8802cebb64 Prevent constantly requesting new video resolutions in group calls. 2021-08-13 13:58:38 -04:00
Lucio Maciel 0c6fe8bea3 Fix crash when encountering SMS calculate length security exception. 2021-08-13 13:58:38 -04:00
Alex Hart 49334ffd42 Implement proper mentions support for multiforward. 2021-08-13 13:58:38 -04:00
Lucio Maciel 4702ab1aeb Implement better detection of text only messages. 2021-08-13 13:58:38 -04:00
Lucio Maciel fe8fcb1394 Implement single line timestamps on conversation items. 2021-08-13 13:58:38 -04:00
Alex Hart dc1e56de4e Implement new bottom fragment UX for multiforward. 2021-08-13 13:58:38 -04:00
Jim Gustafson 561cca5208 Update RingRTC to v2.10.8 2021-08-13 13:58:38 -04:00
Alex Hart a291732c1a Check if already connected before connecting. 2021-08-13 13:58:38 -04:00
Alex Hart 28abc1e4ff Implement new Multiselect UX and groundwork for Multiforward. 2021-08-13 13:58:38 -04:00
Cody Henthorne 655e43a079 Update call UI to new designs. 2021-08-10 16:27:52 -04:00
Cody Henthorne 94b9a458e7 Bump version to 5.20.4 2021-08-10 16:27:31 -04:00
Cody Henthorne bb75730315 Updated language translations. 2021-08-10 16:21:59 -04:00
Alex Hart 824a8ac5f2 Fix RuntimeException during call initialization. 2021-08-10 16:08:55 -04:00
Cody Henthorne 3baf10f0e9 Fix bug with not showing entire long message when it contains no mentions. 2021-08-10 12:02:35 -04:00
Cody Henthorne cfab195e90 Bump version to 5.20.3 2021-08-09 14:40:04 -04:00
Cody Henthorne 503d7c77a0 Updated language translations. 2021-08-09 14:36:45 -04:00
Cody Henthorne f99ff32947 Fix NPE when operating on multiple conversations in batch mode. 2021-08-09 11:54:40 -04:00
Cody Henthorne 182c758d35 Revert "Fix NPE when operating on multiple conversations in batch mode."
This reverts commit fc51c4940c.
2021-08-09 11:52:38 -04:00
Greyson Parrelli f6b2d3faf8 Small refactor to building the sender key target list. 2021-08-06 17:49:08 -04:00
Greyson Parrelli 61c7959ffc Ensure typing indicators are sent as online messages with sender key. 2021-08-06 17:29:25 -04:00
Greyson Parrelli 67ccd14af2 Ensure certain sender key payloads are serialized properly. 2021-08-06 17:29:02 -04:00
Cody Henthorne 3c41b7322f Bump version to 5.20.2 2021-08-06 16:43:18 -04:00
Cody Henthorne d2118d0b53 Updated language translations. 2021-08-06 16:40:45 -04:00
Alex Hart 89af85c893 Fix MP4 Gif crash with ConversationUpdateItem
Due to adapter positions changing due to deletes and other such
nonsense, there are cases where update items are all of a sudden in our
playing or notplaying arrays. Checking for playable content before
trying to update positioning information seems to have fixed the issue.
2021-08-06 16:41:44 -03:00
Greyson Parrelli 0762a93787 Refactor protobuf validation exceptions. 2021-08-06 14:47:43 -04:00
Cody Henthorne 570b4d7150 Fix bug with processing and displaying long messages with mentions. 2021-08-06 13:19:44 -04:00
Cody Henthorne fc51c4940c Fix NPE when operating on multiple conversations in batch mode. 2021-08-06 11:14:33 -04:00
Alex Hart b9ffbb8e92 Fix issue where custom notifications were never enabled.
Older API levels do not have notification channel support, and
we were not checking this state to see if we should enable
the controls. Fix is to add a new controlsEnabled flag on the
state object and set it whenever we finish loading or when recp
changes.
2021-08-06 11:40:09 -03:00
Alex Hart de2c7d38bf Fix NPE in proximity sensor management.
If a device either does not have a proximity sensor or has a
non-functioning sensor, we can hit an NPE as soon as we hit
MainActivity. This fix ensures proper handling if a sensor is
unavailable.
2021-08-06 11:32:03 -03:00
Alex Hart c9597ef8dc Fix several small bugs with foldable calling.
* Set proper aspect ratio of pip in landscape mode.
* Fix some fade and adjustment from new UI states.
2021-08-06 11:27:27 -03:00
Greyson Parrelli a9bbee3880 Trim logs submitted via help and shake2report. 2021-08-05 18:23:20 -04:00
Greyson Parrelli 2bac1a7707 Fix race condition that could show an empty link preview after send. 2021-08-05 17:45:14 -04:00
Cody Henthorne 80e1b2c843 Bump version to 5.20.1 2021-08-05 16:08:17 -04:00
Cody Henthorne 7c2da69676 Updated language translations. 2021-08-05 16:02:58 -04:00
Alex Hart f25e8d602b Rewrite ContactSelectionListItem to utilize ConstraintLayout. 2021-08-05 16:42:32 -03:00
Greyson Parrelli b9c6c6b0f4 Include additional logging to assist in debugging. 2021-08-05 16:42:32 -03:00
Alex Hart 164f39e376 Fix issue where group count flashes in contact selection item. 2021-08-05 16:42:32 -03:00
Greyson Parrelli 49190125ef Locally track conversation open time. 2021-08-05 16:42:32 -03:00
Fumiaki Yoshimatsu 555e65d3ee Try a little harder to find a place to store the file before accepting a directory path that may not exist.
Fixes #11505
2021-08-05 16:42:32 -03:00
Greyson Parrelli 89b1243885 Add the "My Daily Life" sticker pack by Plastic Thing. 2021-08-05 16:42:32 -03:00
Cody Henthorne 3fca46de92 Alleviate database contention when archiving threads. 2021-08-05 16:42:32 -03:00
Alex Hart aa0d7c218f Stage secure outgoing message instead of unwrapped. 2021-08-05 16:42:32 -03:00
Greyson Parrelli 2b5b664a8f Update sender key flag. 2021-08-05 16:42:32 -03:00
Greyson Parrelli 784c373a0e Locally track message send time. 2021-08-05 16:42:32 -03:00
Alex Hart 37ae740138 Hide reveal dot if sender and recvr are both self. 2021-08-05 16:42:32 -03:00
Alex Hart fe9b8a9f47 Replace with new custom notifications page. 2021-08-05 16:42:32 -03:00
Alex Hart 3585667fb9 Bump version to 5.20.0 2021-08-04 13:39:31 -03:00
Alex Hart b4ed088529 Updated language translations. 2021-08-04 13:38:59 -03:00
Greyson Parrelli bdcf390e6e Verify a member is still in the group before using sender key. 2021-08-04 10:55:44 -04:00
Greyson Parrelli c7551881b8 Update handling of unrestricted UD access. 2021-08-04 10:36:49 -04:00
Greyson Parrelli c131754874 Add a system for locally tracking performance on-device. 2021-08-04 10:01:14 -04:00
Alex Hart c6c4988583 Apply proper rules for foldable aspect scaling in landscape and tabletop modes. 2021-08-04 10:57:21 -03:00
Alex Hart d43f044eb4 Add logic to only dismiss header views when in tabletop mode. 2021-08-04 10:53:53 -03:00
Fumiaki Yoshimatsu 9c71994804 Align text to "start" to show LTR text in RTL view correctly.
Fixes #11335
2021-08-04 10:29:03 -03:00
Sgn-32 654d98b0fe Use getSimpleRelativeTimeSpanString in getCallDateString 2021-08-04 10:01:15 -03:00
Fumiaki Yoshimatsu c06056d847 Force LTR layout in this view because the "playback" button should not be mirrored in RTL.
In this specific case, the drawable (triangle_right) used in this view
is _not_ autoMirrored which is correct. But the `layout_marginStart`
attribute adds the margin to the wrong side of the view that breaks the
appearance.

c.f. https://material.io/design/usability/bidirectionality.html#mirroring-elements
2021-08-04 09:44:56 -03:00
Cody Henthorne 615fbf87c9 Improve thread update performance by avoiding costly join query. 2021-08-03 14:33:14 -04:00
Alex Hart aca3d150bf Never display unplayed dot in note-to-self.
Fixes #11515
2021-08-03 13:45:30 -03:00
Cody Henthorne 6eae2d39a8 Improve thread update performance by removing need for message count. 2021-08-03 13:45:30 -03:00
Alex Hart c78e283084 Reimplement voice note proximity locking.
Proximity lock was tied to the VoiceNotePlaybackService instead of to the Activity, and it made for some strange code decisions. This rewrite now ties locking to the activity, where it should have been in the first place, and hopefully solves a few proximity / playback bugs on the way. In addition, it conforms to SRP better as it will send a command to the player to change the audio attributes as necessary instead of directly operating on a player instance.
2021-08-03 13:45:30 -03:00
Alex Hart 2d5492ffac Add accessibility descriptions to voice note player view.
Fixes #11518
2021-08-03 13:45:30 -03:00
Cody Henthorne 2830132b24 Reduce time to start PushTextSendJob. 2021-08-03 13:45:30 -03:00
Alex Hart e374f3afe6 Bump version to 5.19.4 2021-08-03 13:34:33 -03:00
Alex Hart 58a2e50904 Updated language translations. 2021-08-03 13:33:55 -03:00
Alex Hart be29dce7b7 Add lint suppression for API31 bluetooth permissions. 2021-08-03 13:33:54 -03:00
Lucio Maciel e58b617689 Revert grouping body+footer 2021-08-03 11:52:22 -03:00
Greyson Parrelli 9d3a9fc675 Bump version to 5.19.3 2021-08-02 16:32:23 -04:00
Greyson Parrelli 8bddf5206c Updated language translations. 2021-08-02 16:24:09 -04:00
Alex Hart 0ac234e7bf Wrap calls in separate checks for ISE so we do as many as possible. 2021-08-02 16:19:43 -04:00
Alex Hart 290f84e5b1 Ensure correct local device rotation information is retained when starting a call. 2021-08-02 16:19:43 -04:00
Alex Hart 149c138666 Fix negative audio message duration. 2021-08-02 16:19:43 -04:00
Cody Henthorne 065a39992a Fix crash when encountering null PendingIntent. 2021-08-02 16:19:43 -04:00
Lucio Maciel 4a52532256 Require CALL_PHONE permission on VoiceCallShare activity.
Thanks to Jouni Hiltunen for the report
2021-08-02 16:19:39 -04:00
Alex Hart 93f541ceca Fix issue where audio messages would hide their footer. 2021-08-02 16:19:39 -04:00
Cody Henthorne e97a1b2cf6 Fix mixed theme use when system force dark is on.
Thanks to flodo from the forum for the help.
2021-08-02 16:19:39 -04:00
Alex Hart f6b46f921c Fix issue where emojis would not appear on app launch. 2021-08-02 16:19:39 -04:00
AsamK 5fef0494b1 Fix crash with untitled sticker pack in sticker keyboard. 2021-08-02 16:19:39 -04:00
Alex Hart 6e1621fef1 Allow content of basic megaphones to scroll.
Fixes #11507
2021-08-02 16:19:39 -04:00
Alex Hart e5f1793eb3 Add content description to navigation button on settings toolbar. 2021-08-02 16:19:39 -04:00
Alex Hart a994712609 Add tint to checkmark in ContactNameEditActivity. 2021-08-02 16:19:39 -04:00
Alex Hart 3b8eac0b8d Disable registration lock toggle and pin reminder toggle if user does not have a pin. 2021-08-02 16:19:39 -04:00
Alex Hart 52978b1b42 Ensure landscape operation is only enabled on foldable displays. 2021-08-02 16:19:39 -04:00
Greyson Parrelli 922d0d7203 Stop showing empty group updates for internal users. 2021-08-01 00:26:20 -04:00
Cody Henthorne 429fdf0d76 Bump version to 5.19.2 2021-07-30 17:39:38 -04:00
Cody Henthorne 1f718009bd Updated language translations. 2021-07-30 17:39:38 -04:00
Greyson Parrelli c1c9ca7c4c Give the service direct knowledge of linked device status. 2021-07-30 17:39:29 -04:00
Greyson Parrelli 75421b1af8 Rebuild list of send targets after sending distribution key. 2021-07-30 13:17:43 -04:00
Greyson Parrelli d40bb2d9ee Clear all sender key knowledge for a device after a 409/410. 2021-07-30 13:17:43 -04:00
Greyson Parrelli 7c8549bf5e Don't unnecessarily create threads for groups. 2021-07-30 12:27:28 -04:00
Cody Henthorne fb8f481a87 Bump version to 5.19.1 2021-07-29 16:52:16 -04:00
Cody Henthorne 8caa690086 Updated language translations. 2021-07-29 16:46:16 -04:00
Greyson Parrelli d7011e3353 Improve clarity around time conversions. 2021-07-29 16:24:20 -04:00
Cody Henthorne 9af966b030 Improve width calculation for span count. 2021-07-29 15:57:57 -04:00
Lucio Maciel a46accfcc0 Fix link preview margins 2021-07-29 16:47:28 -03:00
Lucio Maciel c0c4092cd9 Update view-once messages 2021-07-29 16:46:32 -03:00
Cody Henthorne 9398716848 Improve speed of sending single messages. 2021-07-29 14:07:39 -04:00
Greyson Parrelli 25234496bf Add support for announcement groups. 2021-07-28 17:21:19 -04:00
Cody Henthorne 1a56924a56 Bump version to 5.19.0 2021-07-28 16:05:33 -04:00
Cody Henthorne d168d35362 Updated language translations. 2021-07-28 15:58:12 -04:00
Greyson Parrelli 3cc2cd0b17 Add support for signal.me links. 2021-07-28 11:58:03 -04:00
Lucio Maciel 138b7ea796 Update message bubble and date header timestamps. 2021-07-28 12:39:50 -03:00
Cody Henthorne 1f1a4eb351 Fix incorrect emojis used in Settings. 2021-07-28 09:40:29 -04:00
Lucio Maciel b9081dc942 Update message collapsing criteria 2021-07-27 19:52:28 -03:00
Lucio Maciel e76808a000 Adjust conversation updates margins 2021-07-27 19:40:39 -03:00
Lucio Maciel e31fd8d578 Update timer icons and message bubble margins 2021-07-27 19:37:59 -03:00
Greyson Parrelli 7d8f780d60 Clean up bookkeeping around threads. 2021-07-27 13:52:49 -04:00
Greyson Parrelli 0478757af4 Sync archive status changes after thread updates. 2021-07-27 13:47:15 -04:00
Cody Henthorne 712b0c147a Improve WebSocket health monitoring. 2021-07-27 13:40:33 -04:00
Jim Gustafson fc6db45e59 Update to RingRTC v2.10.7 2021-07-26 13:42:14 -04:00
Alex Hart 5229e24397 Implement initial support for foldables in calling. 2021-07-26 13:42:14 -04:00
Alex Hart 927b6096c6 Upgrade AGP and Gradle. 2021-07-26 13:42:14 -04:00
Greyson Parrelli 16f1128990 Bump version to 5.18.4 2021-07-26 13:31:39 -04:00
Greyson Parrelli 4fd1d05503 Updated language translations. 2021-07-26 13:28:04 -04:00
Greyson Parrelli dfac05a118 Do not use constants in LogDatabase#onUpgrade. 2021-07-26 11:29:25 -04:00
Greyson Parrelli cd869bcb89 Fix name of nightly build. 2021-07-26 11:16:24 -04:00
Greyson Parrelli 427119cef2 Do not backup the avatar picker database. 2021-07-26 10:40:55 -04:00
Lucio Maciel dada7a4f06 Revert "Update timer icons and received text bubble."
This reverts commits 26c9b5166e,
833f90ce53,
0ba7ff911b and 38adb0373d.
2021-07-26 11:13:26 -03:00
Greyson Parrelli 44a84210d8 Fix backup setting summary text consistency. 2021-07-26 10:08:20 -04:00
Greyson Parrelli 5ac8d3b0bd Do not show profile photo when tapping note to self. 2021-07-26 10:00:09 -04:00
Greyson Parrelli 7ccba5b1c8 Handle missing file browser during backup selection. 2021-07-26 09:59:49 -04:00
Greyson Parrelli c2ffd8adbb Fix crash when submitting a debuglog during registration. 2021-07-26 09:39:03 -04:00
Greyson Parrelli 7e4396ae3f Use custom emoji for avatars. 2021-07-26 08:56:20 -04:00
Greyson Parrelli d0827eb48e Fix emoji rendering artifact.
There's sometimes this one pixel line that can appear next to them.
Easiest solution for now is to trim it off.
2021-07-26 08:23:09 -04:00
Greyson Parrelli 90397165c3 Bump version to 5.18.3 2021-07-23 17:58:40 -04:00
Greyson Parrelli e3e47504a6 Updated language translations. 2021-07-23 17:58:18 -04:00
Greyson Parrelli 42269efa57 Fix reaction sizing issue. 2021-07-23 17:53:52 -04:00
Lucio Maciel 38adb0373d Fix mentions and thumbnail size. 2021-07-23 17:53:52 -04:00
Alex Hart 8bde389398 Scroll to selected on state change. 2021-07-23 17:52:51 -04:00
Alex Hart d29b0609a3 Create nicer animation for moving between pages. 2021-07-23 14:02:47 -03:00
Alex Hart 740977164b Apply several fixes for beta feedback.
* Remove overscroll from avatar picker recyclers.
* Center crop wallpaper previews.
* If no media thumb exists, return bubble projection instead.
2021-07-23 13:47:43 -03:00
Greyson Parrelli 2dd8f24e14 Bump version to 5.18.2 2021-07-23 08:27:56 -04:00
Greyson Parrelli 4e409fc9ed Updated language translations. 2021-07-23 08:27:15 -04:00
Greyson Parrelli 136826be69 Update order of onboarding cards. 2021-07-23 08:07:49 -04:00
Lucio Maciel 0ba7ff911b Fix margins on message bubbles. 2021-07-23 08:05:50 -04:00
Alex Hart bfbdbdcbc0 Add Photo onboarding card. 2021-07-23 08:05:28 -04:00
Greyson Parrelli f2533ac4b7 Fallback to legacy sends upon getting a 401 during sender key. 2021-07-23 08:05:28 -04:00
Greyson Parrelli 15a5f5966d Update logging to be size-limited and more performant. 2021-07-23 08:05:28 -04:00
Alex Hart 3c748b2df6 Fix NullPointerException if there is no cursor drawable set. 2021-07-23 08:05:28 -04:00
Alex Hart c1b54b3532 Fix several issues with new avatar picker.
* Fix silliness with text behaviour
* Fix long click behaviour
* Make views play nicer with landscape mode
* Do not show megaphone if user has an avatar (or had one and removed it)
* Fix bad heading on vector color picker
2021-07-23 08:05:28 -04:00
Alex Hart ab56856f41 Adjust sizing of default group icon in chat settings. 2021-07-23 08:05:28 -04:00
Alex Hart ce31e642dd Fix missing background on video player bar. 2021-07-22 11:10:57 -03:00
Greyson Parrelli aa67c82634 Bump version to 5.18.1 2021-07-22 03:04:15 -04:00
Greyson Parrelli c9c4187d2e Updated language translations. 2021-07-22 03:04:15 -04:00
Greyson Parrelli 60b4862b1b Ensure SQLCipher is loaded before logging begins. 2021-07-22 03:04:15 -04:00
Greyson Parrelli b2c3a34d68 Bump version to 5.18.0 2021-07-21 16:57:04 -04:00
Greyson Parrelli 90925f4d8c Updated language translations. 2021-07-21 16:57:04 -04:00
Lucio Maciel 833f90ce53 Fix margins on message clusters and 1:1 messages. 2021-07-21 16:57:04 -04:00
Lucio Maciel 26c9b5166e Update timer icons and received text bubble. 2021-07-21 16:57:04 -04:00
Alex Hart a27d60f830 Adjust new avatar picker logic.
* Better emoji rendering support
* Deleting an avatar will deselect it
* Added padding to the bottom of recyclers
* Disabled save if no edit / selection has been made.
* Clearing and saving will remove a user's avatar.
2021-07-21 16:57:04 -04:00
Alex Hart a75f634c0a Add megaphone for new avatar picker. 2021-07-21 16:57:04 -04:00
lucio-signal 963c018e0c Add SingleLineEmojiTextView to fix flickering on conversations list. 2021-07-21 16:57:04 -04:00
Alex Hart 6cc0eed5fe Fail linked preview thumbnail request instead of crashing app. 2021-07-21 16:57:04 -04:00
Alex Hart cdcc7b6fa5 Fix voice note player crash in Android 4.4 2021-07-21 16:57:04 -04:00
Alex Hart 24482b5a65 Disable conversation overscroll for Android 12. 2021-07-21 16:57:03 -04:00
Alex Hart b100262c6a Fix crash when sending video (due to IllegalStateException). 2021-07-21 16:57:03 -04:00
Alex Hart ed23c3fe7c Add avatar picker and defaults. 2021-07-21 16:57:03 -04:00
Greyson Parrelli 0093e1d3eb Add the ability to increase log lifespan. 2021-07-21 16:57:03 -04:00
Greyson Parrelli 7419da7247 Move logging into a database. 2021-07-21 16:57:03 -04:00
Greyson Parrelli 0b85852621 Bump version to 5.17.3 2021-07-19 13:05:11 -04:00
Greyson Parrelli 556518973d Fix crash during cache warming for fresh installs. 2021-07-19 13:01:53 -04:00
Greyson Parrelli b9514d0b94 Bump version to 5.17.2 2021-07-19 12:40:21 -04:00
Greyson Parrelli a9dab90a1e Updated language translations. 2021-07-19 12:40:21 -04:00
Greyson Parrelli 39709c8d64 Fix some timing issues around recipient events. 2021-07-19 12:40:21 -04:00
Greyson Parrelli c2a6963a6d Warm up some recipients from the contact selection screen. 2021-07-19 11:57:26 -04:00
Greyson Parrelli bfdebbfa5d Sort contacts that start with a number at the end. 2021-07-19 11:57:26 -04:00
Alex Hart 167a691018 Update SMS tag visibility in onRecipientChanged. 2021-07-19 11:57:26 -04:00
Greyson Parrelli 8004565c84 Bump version to 5.17.1 2021-07-16 15:55:54 -04:00
Greyson Parrelli a101dc4fd1 Updated language translations. 2021-07-16 15:53:42 -04:00
Alex Hart 57f730d8ee Fix cursor issue for non-signal-contact searches. 2021-07-16 16:34:38 -03:00
Alex Hart 3543cc80ba Don't show SMS label for push groups. 2021-07-16 16:34:02 -03:00
Greyson Parrelli 71613d9db1 Ensure SQLCipher libraries are loaded. 2021-07-16 14:12:00 -04:00
Greyson Parrelli 4a0e6a3eb2 Improve logging around message sends. 2021-07-16 12:53:29 -04:00
Alex Hart f1a87518e1 Fix contact search query returning outdated or bad recipients. 2021-07-16 13:53:17 -03:00
Alex Hart 61f880fd78 Hide all media for new conversations. 2021-07-16 13:20:30 -03:00
Greyson Parrelli 09904e7a16 Remove GIF button from attachment keyboard.
We've had it there for ~45 days for education purposes, but we can
remove it now.
2021-07-16 11:01:10 -04:00
Alex Hart 94658e9090 Fix bug where marquee text stopped scrolling. 2021-07-16 09:23:47 -03:00
Greyson Parrelli a47448b6c6 Bump version to 5.17.0 2021-07-15 16:41:11 -04:00
Greyson Parrelli 7e4b9b685a Updated language translations. 2021-07-15 16:40:28 -04:00
lucio-signal 64922a8e51 Fix custom vibration settings. 2021-07-15 16:29:43 -04:00
Cody Henthorne f65f4704c9 Improve routine around bulk attachment deletion. 2021-07-15 16:29:11 -04:00
Greyson Parrelli b04ca202f6 Fix ApplicationMigrations UI. 2021-07-15 16:28:13 -04:00
Greyson Parrelli 83086a5a2b Make Sms/MmsDatabase ID's autoincrement. 2021-07-15 16:28:13 -04:00
Cody Henthorne 51a521594f Fix crash when deleting threads directly after backup restore. 2021-07-15 16:28:13 -04:00
Greyson Parrelli 0a7a7cf5a9 Fix envelope type conversion. 2021-07-15 16:28:13 -04:00
Greyson Parrelli 6bd689504c Make internal recipient details selectable. 2021-07-15 16:28:13 -04:00
Greyson Parrelli efec40ff57 Fix crash with GV2 group repair during storage sync. 2021-07-15 16:28:13 -04:00
Greyson Parrelli 69716dde4a Fix navigation directly to the help screen. 2021-07-15 16:28:13 -04:00
Greyson Parrelli e90fa05d60 Update recipient merging. 2021-07-15 16:28:13 -04:00
Greyson Parrelli 580c000bda Move distribution message processing into the decryption phase. 2021-07-15 16:28:13 -04:00
lucio-signal 2f3d04d3e8 Add EmojiFilter to SearchView input field. 2021-07-15 16:28:13 -04:00
Greyson Parrelli bf37d412e9 Add message trimming info to the debug log. 2021-07-15 16:28:13 -04:00
Alex Hart fd115ebb72 Drop voice notes that do not have a URI. 2021-07-15 16:28:13 -04:00
Greyson Parrelli b9657208fe Make ThreadDatabase ID's autoincrement. 2021-07-15 16:28:13 -04:00
Cody Henthorne 5d6d78a51e Initial WebSocket refactor. 2021-07-15 16:28:13 -04:00
Cody Henthorne 916006e664 Tweak sizes and padding of various keyboard elements. 2021-07-15 16:28:13 -04:00
Cody Henthorne 55c69cd50a Add additional fallback logic for change dialog. 2021-07-15 16:28:13 -04:00
Cody Henthorne 14565b0864 Fix crash when building notification state for messages without threads. 2021-07-15 16:28:13 -04:00
Alex Hart a157c1ae1d Refresh contact search views. 2021-07-15 16:28:13 -04:00
Greyson Parrelli a4d458f969 Use current tag for nightly versionName. 2021-07-15 16:28:13 -04:00
Alex Hart 3f53abedab Migrate to new Share APIs. 2021-07-15 16:28:13 -04:00
Jordan Rose 68a2d5ed20 Reimplement ProfileCipherInputStream using libsignal-client.
libsignal-client provides an AES-GCM streaming interface that can
replace the implementation in AES-GCM-Provider. Using it from
ProfileCipherInputStream requires some knowledge about the tag size of
AES-GCM, but frees it from the JCE interface.

Note that it remains a serious error to not read the *entire* stream,
since the authentication tag is at the end!
2021-07-15 16:28:11 -04:00
Jordan Rose 35e9e31a7b Update to libsignal-client 0.8.3
This also fixes a misalignment where signal-client-android was on
0.8.0 but signal-client-java was 0.8.1, which was fortunately harmless
for this particular pair of versions.
2021-07-12 20:29:07 -04:00
Cody Henthorne 444d947743 Add RxJava. 2021-07-12 20:29:07 -04:00
Cody Henthorne c427dbad08 Bump version to 5.16.3 2021-07-12 20:27:40 -04:00
Cody Henthorne 2cefe813e4 Updated language translations. 2021-07-12 20:17:43 -04:00
Alex Hart 123ffe42c3 Fix crash saving a FLAC file. 2021-07-12 13:37:59 -03:00
Alex Hart da20e66ecd Fix issue where shared contact render would not hide audio view. 2021-07-12 13:24:04 -03:00
Alex Hart 901440017a Fix audio view width on very narrow screens. 2021-07-12 13:20:56 -03:00
Cody Henthorne 0be76a37fe Bump version to 5.16.2 2021-07-09 15:43:11 -04:00
Cody Henthorne 36dadc8777 Updated language translations. 2021-07-09 15:37:19 -04:00
Cody Henthorne 182749c101 Fix bug where some profile fetches would 400 over the websocket. 2021-07-09 15:30:08 -04:00
Alex Hart d9228bd911 Fix issue where compose views still display under draft. 2021-07-09 15:30:08 -04:00
Greyson Parrelli a361fcc8f3 Add additional logging to media send jobs. 2021-07-09 15:30:08 -04:00
Alex Hart ff4f0b9f42 Stop voice note playback after user locks Signal. 2021-07-09 15:30:07 -04:00
Alex Hart 060dffc9cc Fix crash caused when quote draft left and re-entered. 2021-07-09 15:30:07 -04:00
Alex Hart 172cc302fc Add warning dialog for chat color deletion with no uses. 2021-07-09 15:30:07 -04:00
Alex Hart 416e62112f Refresh shared media screens. 2021-07-09 15:30:07 -04:00
Alex Hart e584a90f81 Fix several voice note beta bugs.
* Sim label positioning
* Bad player state when navigating to and from conversations
* Scrolling date header placement
2021-07-09 15:29:40 -04:00
Cody Henthorne 9876ffb5e4 Bump version to 5.16.1 2021-07-08 17:52:50 -04:00
Cody Henthorne 53e10f2cad Updated language translations. 2021-07-08 17:43:16 -04:00
Cody Henthorne cb79f75ac1 Fix bug when calling non-Signal contacts from Settings.
Fixes #11450
2021-07-08 17:36:14 -04:00
Cody Henthorne 5ec9c1cd90 Fix crash when saving media with octet stream content type. 2021-07-08 17:36:14 -04:00
Greyson Parrelli 1f28a30ace Add a nightly build type. 2021-07-08 17:36:14 -04:00
Alex Hart 7715917436 Fix issue where position would not update in draft. 2021-07-08 17:36:14 -04:00
Alex Hart f79b445fdf Fix issue where drafts might not be properly deleted. 2021-07-08 17:36:14 -04:00
Alex Hart 14484deabe Implement count-down in inline player. 2021-07-08 17:36:14 -04:00
Alex Hart 3ac395d33e Fix row item size issue with huge fonts. 2021-07-08 17:36:14 -04:00
Alex Hart f83b520ca9 Bump version to 5.16.0 2021-07-07 14:58:51 -03:00
Alex Hart 0123f9aa87 Updated language translations. 2021-07-07 14:58:51 -03:00
Alex Hart 06b64fe619 Add inline voice note player to conversation and conversation list. 2021-07-07 14:58:51 -03:00
Greyson Parrelli 1bb87834d8 Reduce recipient resolves in MessageContentProcessor. 2021-07-07 14:58:51 -03:00
Greyson Parrelli ae4167ddae Write to RecipientIdCache on cache miss. 2021-07-07 14:58:51 -03:00
Greyson Parrelli 383beafdef Move 'you' to end of unnamed groups. 2021-07-07 14:58:51 -03:00
Greyson Parrelli 062e88b24f Rotate sender key flag. 2021-07-07 14:58:51 -03:00
Greyson Parrelli 8299d49042 Show an error for internal users for decryption failures. 2021-07-07 14:58:51 -03:00
Greyson Parrelli 4677883838 Improve mapping SignalServiceAddresses to Recipients. 2021-07-07 14:58:51 -03:00
Greyson Parrelli 7f0a0bef5a Incrementally insert MSL entries for legacy group sends. 2021-07-07 14:58:50 -03:00
Greyson Parrelli acc825971b Handle additional places where MSL entries need to be deleted. 2021-07-07 14:58:50 -03:00
Greyson Parrelli 62040d06b4 Create a write-through cache for PendingRetryReceiptDatabase. 2021-07-07 14:58:50 -03:00
Greyson Parrelli 0921ebe5f1 Add read and viewed receipts to the MSL. 2021-07-07 14:58:50 -03:00
Greyson Parrelli 3d0e15e2b8 Add delivery receipts to the MSL. 2021-07-07 14:58:50 -03:00
Greyson Parrelli 5372f79c40 Allow for MSL entries to be associated with multiple messages. 2021-07-07 14:58:50 -03:00
Christian 92e8f9de0e Do not collapse list to hide only one entry. 2021-07-07 14:58:50 -03:00
Christian c3cf846a10 Fix OutdatedBuildReminder duration.
Fixes #11438
2021-07-07 14:58:50 -03:00
Alex Hart 5826b0c068 Implement drafts for voice notes. 2021-07-07 14:58:50 -03:00
Alex Hart 2d7c043398 Implement a playback speed toggle for voice notes. 2021-07-07 14:58:50 -03:00
Alex Hart e20d6b63cf Fix adaptive shortcut icon shapes. 2021-07-07 14:58:50 -03:00
Cody Henthorne b85c5eb54a Make it more likely 8 emoji fit on a row, fix emoji search emoticons. 2021-07-07 14:58:50 -03:00
Greyson Parrelli a1c8573fad Insert resent messages at the proper location. 2021-07-07 14:58:50 -03:00
Cody Henthorne 90a27d2227 Fix device transfer test dependent on native library. 2021-07-07 14:58:50 -03:00
Cody Henthorne c54c6018b2 Remove dead keyboard code after refresh. 2021-06-30 16:13:42 -04:00
Rainer Matischek 7419570f94 Fix rotation not updated on phones using 'Legacy API'.
Fixes #10940
2021-06-30 16:13:42 -04:00
Jim Gustafson 8860f792c4 Update to RingRTC v2.10.6 2021-06-30 16:13:42 -04:00
Greyson Parrelli e47db0d532 Ensure recipients added to the cache have an identifier. 2021-06-30 16:13:42 -04:00
Greyson Parrelli ab5d3badc2 Enable WAL mode. 2021-06-30 16:13:42 -04:00
Greyson Parrelli fce362960f Switch to LinkifyCompat.
We've seen some inconsistencies across OEMs with Linkify. Hopefully
LinkifyCompat will resolve them.
2021-06-30 16:13:42 -04:00
Greyson Parrelli 5bf23dcfb3 Bump version to 5.15.6 2021-06-30 16:12:58 -04:00
Greyson Parrelli 65c7dc6ca2 Updated language translations. 2021-06-30 16:12:36 -04:00
Greyson Parrelli e30a8b6954 Use proper EmojiTextView in conversation settings toolbar. 2021-06-30 15:43:21 -04:00
Alex Hart 838e318200 Fix edit profile theming issue and mute until issue. 2021-06-30 11:11:35 -03:00
Greyson Parrelli 62ee411901 Bump version to 5.15.5 2021-06-29 14:16:06 -04:00
Greyson Parrelli ceefd2d92f Updated language translations. 2021-06-29 14:15:28 -04:00
Cody Henthorne e3870f5656 Fix Customize Reactions shadow. 2021-06-29 12:38:40 -04:00
Alex Hart 6e7022ab70 Fix custom notifications toggle and enable copy phone number on long press. 2021-06-29 11:19:51 -03:00
Alex Hart 031d1551e7 Prevent crash by ignoring call if view is null. 2021-06-29 11:02:57 -03:00
Greyson Parrelli 6755b25361 Bump version to 5.15.4 2021-06-28 18:07:36 -04:00
Greyson Parrelli 11d0a73675 Updated language translations. 2021-06-28 18:07:36 -04:00
Alex Hart 44119b6437 Do not crash if we try to access an item outside of the bounds of the conversation. 2021-06-28 18:07:36 -04:00
Cody Henthorne d4a3b442f4 Add vertical scrolling to Sticker Keyboard. 2021-06-28 18:07:36 -04:00
Cody Henthorne aba5774446 Fix share contact list updating improperly on selection change. 2021-06-28 18:07:36 -04:00
Alex Hart 911dd9efb1 Fix conversation media overview underline flicker. 2021-06-28 11:38:14 -03:00
Alex Hart f2a490b07e Fix several conversation settings feedback issues.
* Mute icon in wrong location in RTL
* No exit animation when dismissing conversation settings
* Thumbnails flicker when you come back to conversation settings
* Rounded corners for mute dialog don't match other dialogs
* Mute button in note-to-self conversation settings
* Explore adding contact details to the contact bottom sheet
2021-06-28 11:11:57 -03:00
Cody Henthorne 5675f080f2 Fix text emoticons not showing up in recents. 2021-06-28 10:11:01 -04:00
Greyson Parrelli f0dbe230b5 Bump version to 5.15.3 2021-06-25 17:41:53 -04:00
Greyson Parrelli 8b81800052 Updated language translations. 2021-06-25 17:41:53 -04:00
Greyson Parrelli f598c14298 Update the sender key feature flag. 2021-06-25 17:41:53 -04:00
Greyson Parrelli 58b070e6e3 Fix job info log formatting. 2021-06-25 17:25:59 -04:00
Greyson Parrelli 71c92a1c90 Fix syncing group messages when you're the only member. 2021-06-25 17:00:14 -04:00
Greyson Parrelli b86acb9773 Increase log size for internal users. 2021-06-25 16:55:15 -04:00
Greyson Parrelli 1b8758b657 Fix text wrapping issues in message details. 2021-06-25 16:49:48 -04:00
Cody Henthorne ed4bab1b8b Add vertical scroll to Emoji Keyboard. 2021-06-25 16:39:04 -04:00
Greyson Parrelli a71fe0fd75 Fix issue with group creation on linked devices. 2021-06-25 16:35:42 -04:00
Alan Evans 3d2a634aac Apply the ringer volume to the join/hangup sounds with 15% minimum. 2021-06-25 16:46:59 -03:00
Alex Hart 01047f0546 Apply style changes to shared media, color icon, and wallpaper previews. 2021-06-25 14:27:51 -03:00
Alex Hart 9dac5691f0 Fix issue where all content would be displayed if thread id was -1. 2021-06-25 11:28:28 -03:00
Alex Hart 3c489ad247 Check admin status in areContentsTheSame. 2021-06-25 11:16:53 -03:00
Alex Hart 7797351341 Fix see more icon tint and fix recipient bottom sheet scroll. 2021-06-25 11:09:49 -03:00
Alex Hart f7212b9916 Update legacy text and fix small animation bug. 2021-06-25 10:57:58 -03:00
Alex Hart 93bb49dc16 Fix inconsistent toolbar animation state on back. 2021-06-25 10:35:07 -03:00
Alex Hart e504c490c8 Prevent all menu invalidations if we have requested a conversation search. 2021-06-25 09:28:55 -03:00
Cody Henthorne 42e865813c Bump version to 5.15.2 2021-06-24 16:49:02 -04:00
Cody Henthorne fc14d1d464 Updated language translations. 2021-06-24 16:45:50 -04:00
Cody Henthorne 2a1e5e4471 Add React With Any Search and update UX. 2021-06-24 16:36:13 -04:00
Alex Hart da2ee33dff Refactor conversation settings screens into a single fragment with new UI. 2021-06-24 16:36:13 -04:00
Greyson Parrelli f19033a7a2 Implement the message send log for sender key retries. 2021-06-24 16:36:13 -04:00
Greyson Parrelli 6502ef64ce Read the group history response as a stream. 2021-06-23 17:47:05 -04:00
Alan Evans b3ebf778fd Group call server selection for internal users. 2021-06-23 17:50:59 -03:00
Cody Henthorne 1dca3698d2 Fix crash when adding person to an existing mms group. 2021-06-22 17:03:20 -04:00
Cody Henthorne 2bfe1198d1 Bump version to 5.15.1 2021-06-21 20:24:07 -04:00
Cody Henthorne 4f704670b1 Updated language translations. 2021-06-21 20:01:03 -04:00
Cody Henthorne a1aafd7453 Fix incorrect mark as read behavior when leaving conversation. 2021-06-21 19:55:02 -04:00
Alex Hart 4932623937 Allow FABs to go as high as the bottom of the toolbar on the conversation list. 2021-06-21 19:55:02 -04:00
Alex Hart b93568d9c6 Invoke onTick immediately in onResume. 2021-06-21 19:55:02 -04:00
Alex Hart b3041ab6e0 Always update ViewOnceState before rendering hud. 2021-06-21 14:27:28 -03:00
Alex Hart 3a151b30ac Catch MediaCodecException in extractThumbnails for configuration crash. 2021-06-21 14:19:11 -03:00
Alex Hart 97b3d36433 Add support to MessageDetailsActivity for viewed reciepts. 2021-06-21 14:11:36 -03:00
Cody Henthorne 81e3252128 Do not apply universal timer to SMS chats. 2021-06-21 11:04:28 -04:00
Cody Henthorne 426c83c6cc Fix baby emoji in Help and Profile. 2021-06-21 10:54:55 -04:00
Greyson Parrelli b427754a81 Fix quoted media rendering issue. 2021-06-21 10:31:14 -04:00
Cody Henthorne 08f023fb12 Revert "Fix ANR when leaving MediaPreviewActivity."
This reverts commit 8be659c1c8.
2021-06-21 09:55:40 -04:00
Greyson Parrelli 5f1454aeb8 Improve the performance of detecting duplicate messages.
To do this, we do two things:
- Make the index on DATE_SENT also include the other relevant fields:
the recipientId and threadId
- Use the most minimal projection possible
2021-06-21 09:51:51 -04:00
Greyson Parrelli 0d254e0724 Fix the mock data initializer.
Needed to ignore the emoji_data FTS tables.
2021-06-20 17:36:27 -04:00
Cody Henthorne e882e6e111 Bump version to 5.15.0 2021-06-18 15:21:41 -04:00
Cody Henthorne 4b0811f9aa Revert "Temporarily block payments in all regions."
This reverts commit 4637e1b5d8.
2021-06-18 15:10:29 -04:00
Greyson Parrelli 817f1ee938 Add a feature flag to disable SMS megaphone.
As part of this work, we also make sure we fetch feature flags during
registration.
2021-06-18 15:10:16 -04:00
Cody Henthorne 2d93d74b9f Fix incorrect linting by preventing Github Actions from using Android S. 2021-06-18 15:10:16 -04:00
Greyson Parrelli 93f37ad70f Reduce fetches when you open a conversation. 2021-06-18 15:10:16 -04:00
Cody Henthorne 3c6bed90db Fix ANR by upgrading Firebase Messaging. 2021-06-18 15:10:16 -04:00
Greyson Parrelli fa26fb6b8b Improve conversation query performance.
For the conversation query at least, we stopped joining on the
attachments tables, and instead get attachments on a page-by-page basis.
2021-06-18 15:10:15 -04:00
Cody Henthorne 263ddb0d1e Fix main thread recipient resolve in contact selection. 2021-06-18 15:10:15 -04:00
Cody Henthorne 8be659c1c8 Fix ANR when leaving MediaPreviewActivity. 2021-06-18 15:10:15 -04:00
Cody Henthorne e5c9dddb5a Fix ANR when generating group message snippets. 2021-06-18 15:10:15 -04:00
Greyson Parrelli 6da72aad6d Log the build variant. 2021-06-18 15:10:15 -04:00
Greyson Parrelli 5dd5a024c9 Narrow locking in LiveRecipientCache.
This should make it so that we never hold a lock while accessing the
database.
2021-06-18 15:10:15 -04:00
Greyson Parrelli c0eac5564c Clean up message processing locks. 2021-06-18 15:10:15 -04:00
Cody Henthorne 0d0ee753df Make portrait bubbled keyboard height dynamic based on bubble height. 2021-06-18 15:10:15 -04:00
Aaron Labiaga 908f952893 Update API for Activity in bubble check. 2021-06-18 15:10:15 -04:00
Cody Henthorne 1c80e65c5a Bump version to 5.14.5 2021-06-18 15:02:33 -04:00
Cody Henthorne 20b13a929b Updated language translations. 2021-06-18 14:55:16 -04:00
Alex Hart 4637e1b5d8 Temporarily block payments in all regions. 2021-06-18 14:47:32 -04:00
Greyson Parrelli 4b6cb79c75 Fix message exception handling. 2021-06-18 13:52:31 -04:00
Greyson Parrelli feaf2a33a9 Bump version to 5.14.4 2021-06-17 17:39:30 -04:00
Greyson Parrelli 4c893a11fc Updated language translations. 2021-06-17 17:39:30 -04:00
Cody Henthorne f4dd80c929 Switch logic order for detecting conversation channel changes. 2021-06-15 13:09:11 -04:00
Cody Henthorne 4af078007e Attempt to recover from encountering octet stream media. 2021-06-15 11:54:14 -04:00
Greyson Parrelli be297120a1 Include 'you' in dynamic group name. 2021-06-15 11:37:28 -04:00
Cody Henthorne a9741cadbf Fix logging around dialog flow. 2021-06-15 11:31:56 -04:00
Cody Henthorne 79200c82da Fix create bubble conversation notification. 2021-06-14 16:51:18 -04:00
Cody Henthorne d9c9ae8dae Update MobileCoin dependency and add new configuration. 2021-06-14 13:25:50 -04:00
2713 changed files with 170149 additions and 55888 deletions
+1 -1
View File
@@ -50,5 +50,5 @@ Describe here the issue that you are experiencing.
**Signal version:** 0.0.0 **Signal version:** 0.0.0
### Link to debug log ### Link to debug log
<!-- immediately after the bug has happened capture a debug log via Signal's advanced settings and paste the link below --> <!-- immediately after the bug has happened capture a debug log via Signal's settings (Help -> Debug log) and paste the link below -->
+5 -2
View File
@@ -16,14 +16,17 @@ jobs:
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: set up JDK 1.8 - name: set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v1
with: with:
java-version: 1.8 java-version: 11
- name: Validate Gradle Wrapper - name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1 uses: gradle/wrapper-validation-action@v1
- name: Remove Android 31 (S)
run: $ANDROID_HOME/tools/bin/sdkmanager --uninstall "platforms;android-31"
- name: Build with Gradle - name: Build with Gradle
run: ./gradlew qa run: ./gradlew qa
+38 -9
View File
@@ -8,11 +8,48 @@
<option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" /> <option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" />
<option name="ALIGN_MULTILINE_ANNOTATION_PARAMETERS" value="true" /> <option name="ALIGN_MULTILINE_ANNOTATION_PARAMETERS" value="true" />
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" /> <option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
<option name="IMPORT_LAYOUT_TABLE">
<value>
<package name="android" withSubpackages="true" static="false" />
<emptyLine />
<package name="androidx" withSubpackages="true" static="false" />
<emptyLine />
<package name="com" withSubpackages="true" static="false" />
<emptyLine />
<package name="junit" withSubpackages="true" static="false" />
<emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
<emptyLine />
<package name="java" withSubpackages="true" static="false" />
<emptyLine />
<package name="javax" withSubpackages="true" static="false" />
<emptyLine />
<package name="" withSubpackages="true" static="false" />
<emptyLine />
<package name="android" withSubpackages="true" static="true" />
<package name="androidx" withSubpackages="true" static="true" />
<package name="com" withSubpackages="true" static="true" />
<package name="junit" withSubpackages="true" static="true" />
<package name="net" withSubpackages="true" static="true" />
<package name="org" withSubpackages="true" static="true" />
<package name="java" withSubpackages="true" static="true" />
<package name="javax" withSubpackages="true" static="true" />
<package name="" withSubpackages="true" static="true" />
<emptyLine />
</value>
</option>
</JavaCodeStyleSettings> </JavaCodeStyleSettings>
<JetCodeStyleSettings> <JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" /> <option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings> </JetCodeStyleSettings>
<codeStyleSettings language="JAVA"> <codeStyleSettings language="JAVA">
<option name="BRACE_STYLE" value="5" /> <option name="BRACE_STYLE" value="5" />
@@ -178,13 +215,5 @@
</rules> </rules>
</arrangement> </arrangement>
</codeStyleSettings> </codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
</codeStyleSettings>
</code_scheme> </code_scheme>
</component> </component>
+3 -3
View File
@@ -4,7 +4,7 @@ Signal is a messaging app for simple private communication with friends.
Signal uses your phone's data connection (WiFi/3G/4G) to communicate securely, optionally supports plain SMS/MMS to function as a unified messenger, and can also encrypt the stored messages on your phone. Signal uses your phone's data connection (WiFi/3G/4G) to communicate securely, optionally supports plain SMS/MMS to function as a unified messenger, and can also encrypt the stored messages on your phone.
Currently available on the Play store. Currently available on the Play store and [signal.org](https://signal.org/android/apk/).
<a href='https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height='80px'/></a> <a href='https://play.google.com/store/apps/details?id=org.thoughtcrime.securesms&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height='80px'/></a>
@@ -59,8 +59,8 @@ The form and manner of this distribution makes it eligible for export under the
## License ## License
Copyright 2013-2020 Signal Copyright 2013-2021 Signal
Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html Licensed under the GPLv3: http://www.gnu.org/licenses/gpl-3.0.html
Google Play and the Google Play logo are trademarks of Google Inc. Google Play and the Google Play logo are trademarks of Google LLC.
+241 -190
View File
@@ -1,48 +1,50 @@
import org.signal.signing.ApkSignerUtil
import java.security.MessageDigest
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.google.protobuf' apply plugin: 'com.google.protobuf'
apply plugin: 'androidx.navigation.safeargs' apply plugin: 'androidx.navigation.safeargs'
apply plugin: 'witness'
apply plugin: 'org.jlleitschuh.gradle.ktlint' apply plugin: 'org.jlleitschuh.gradle.ktlint'
apply from: 'translations.gradle' apply from: 'translations.gradle'
apply from: 'witness-verifications.gradle' apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'app.cash.exhaustive'
apply plugin: 'kotlin-parcelize'
repositories { repositories {
maven {
url "https://raw.github.com/signalapp/maven/master/photoview/releases/"
content {
includeGroupByRegex "com\\.github\\.chrisbanes.*"
}
}
maven { maven {
url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/" url "https://raw.github.com/signalapp/maven/master/circular-progress-button/releases/"
content { content {
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*" includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
} }
} }
maven { // textdrawable maven {
url 'https://dl.bintray.com/amulyakhare/maven' url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
content { content {
includeGroupByRegex "com\\.amulyakhare.*" includeGroupByRegex "org\\.signal.*"
} }
} }
maven {
url "https://www.jitpack.io"
}
google() google()
mavenCentral() mavenCentral()
jcenter()
mavenLocal() mavenLocal()
maven { maven {
url "https://dl.cloudsmith.io/qxAgwaeEE1vN8aLU/mobilecoin/mobilecoin/maven/" url "https://dl.cloudsmith.io/qxAgwaeEE1vN8aLU/mobilecoin/mobilecoin/maven/"
} }
jcenter {
content {
includeVersion "mobi.upod", "time-duration-picker", "1.1.3"
includeVersion "cn.carbswang.android", "NumberPickerView", "1.0.9"
includeVersion "com.takisoft.fix", "colorpicker", "0.9.1"
includeVersion "com.codewaves.stickyheadergrid", "stickyheadergrid", "0.9.4"
includeVersion "com.google.android", "flexbox", "0.3.0"
}
}
} }
protobuf { protobuf {
protoc { protoc {
artifact = 'com.google.protobuf:protoc:3.10.0' artifact = 'com.google.protobuf:protoc:3.18.0'
} }
generateProtoTasks { generateProtoTasks {
all().each { task -> all().each { task ->
@@ -55,8 +57,8 @@ protobuf {
} }
} }
def canonicalVersionCode = 864 def canonicalVersionCode = 982
def canonicalVersionName = "5.14.3" def canonicalVersionName = "5.28.9"
def postFixSize = 100 def postFixSize = 100
def abiPostFix = ['universal' : 0, def abiPostFix = ['universal' : 0,
@@ -67,6 +69,24 @@ def abiPostFix = ['universal' : 0,
def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ] def keystores = [ 'debug' : loadKeystoreProperties('keystore.debug.properties') ]
def selectableVariants = [
'nightlyProdFlipper',
'nightlyProdPerf',
'nightlyProdRelease',
'playProdDebug',
'playProdFlipper',
'playProdPerf',
'playProdRelease',
'playStagingDebug',
'playStagingFlipper',
'playStagingPerf',
'playStagingRelease',
'studyProdMock',
'studyProdPerf',
'websiteProdFlipper',
'websiteProdRelease',
]
android { android {
buildToolsVersion BUILD_TOOL_VERSION buildToolsVersion BUILD_TOOL_VERSION
compileSdkVersion COMPILE_SDK compileSdkVersion COMPILE_SDK
@@ -75,6 +95,7 @@ android {
useLibrary 'org.apache.http.legacy' useLibrary 'org.apache.http.legacy'
kotlinOptions { kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = ["-Xallow-result-return-type"] freeCompilerArgs = ["-Xallow-result-return-type"]
} }
@@ -107,31 +128,42 @@ android {
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L" buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\"" buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\"" buildConfigField "String", "SIGNAL_URL", "\"https://chat.signal.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\"" buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\"" buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\"" buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\"" buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_CDSH_URL", "\"https://cdsh.staging.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"" buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"" buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\"}"
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\"}"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\"" buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443" buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\"" buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "CDSH_PUBLIC_KEY", "\"2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74\""
buildConfigField "String", "CDSH_CODE_HASH", "\"ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\"" buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," + buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\"," +
"\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " + "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"; "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")";
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]" buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\"" buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXQ==\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}' buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode" buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\"" buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44}" buildConfigField "int[]", "MOBILE_COIN_BLACKLIST", "new int[]{98,963,53,850,7}"
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\"" buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"" buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\""
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"unset\""
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"unset\""
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"unset\""
buildConfigField "String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\""
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\""
ndk { ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
} }
@@ -148,6 +180,21 @@ android {
} }
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true'
}
testOptions {
execution 'ANDROIDX_TEST_ORCHESTRATOR'
}
sourceSets {
test {
java.srcDirs += "$projectDir/src/testShared"
}
androidTest {
java.srcDirs += "$projectDir/src/testShared"
}
} }
compileOptions { compileOptions {
@@ -196,28 +243,34 @@ android {
'proguard/proguard.cfg' 'proguard/proguard.cfg'
testProguardFiles 'proguard/proguard-automation.pro', testProguardFiles 'proguard/proguard-automation.pro',
'proguard/proguard.cfg' 'proguard/proguard.cfg'
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
} }
flipper { flipper {
initWith debug initWith debug
isDefault false isDefault false
minifyEnabled false minifyEnabled false
matchingFallbacks = ['debug'] matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
} }
release { release {
minifyEnabled true minifyEnabled true
proguardFiles = buildTypes.debug.proguardFiles proguardFiles = buildTypes.debug.proguardFiles
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Release\""
} }
perf { perf {
initWith debug initWith debug
isDefault false isDefault false
debuggable false debuggable false
matchingFallbacks = ['debug'] matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
} }
mock { mock {
initWith debug initWith debug
isDefault false isDefault false
minifyEnabled false minifyEnabled false
matchingFallbacks = ['debug'] matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Mock\""
} }
} }
@@ -228,6 +281,7 @@ android {
ext.websiteUpdateUrl = "null" ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"play\""
} }
website { website {
@@ -235,13 +289,16 @@ android {
ext.websiteUpdateUrl = "https://updates.signal.org/android" ext.websiteUpdateUrl = "https://updates.signal.org/android"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true" buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\"" buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
} }
internal { nightly {
dimension 'distribution' dimension 'distribution'
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
ext.websiteUpdateUrl = "null" ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\""
} }
study { study {
@@ -251,6 +308,7 @@ android {
ext.websiteUpdateUrl = "null" ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false" buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl" buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"study\""
} }
prod { prod {
@@ -259,6 +317,7 @@ android {
isDefault true isDefault true
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\"" buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\""
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Prod\""
} }
staging { staging {
@@ -266,33 +325,44 @@ android {
applicationIdSuffix ".staging" applicationIdSuffix ".staging"
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\"" buildConfigField "String", "SIGNAL_URL", "\"https://chat.staging.signal.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\"" buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\"" buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\"" buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\"" buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"" buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\"" buildConfigField "String", "CDS_MRENCLAVE", "\"c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15\""
buildConfigField "KbsEnclave", "KBS_ENCLAVE", "new KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " + buildConfigField "org.thoughtcrime.securesms.KbsEnclave", "KBS_ENCLAVE", "new org.thoughtcrime.securesms.KbsEnclave(\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\", " +
"\"51a56084c0b21c6b8f62b1bc792ec9bedac4c7c3964bb08ddcab868158c09982\", " + "\"16b94ac6d2b7f7b9d72928f36d798dbb35ed32e7bb14c42b4301ad0344b46f29\", " +
"\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")" "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\")"
buildConfigField "KbsEnclave[]", "KBS_FALLBACKS", "new KbsEnclave[0]" buildConfigField "org.thoughtcrime.securesms.KbsEnclave[]", "KBS_FALLBACKS", "new org.thoughtcrime.securesms.KbsEnclave[0]"
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"" buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\"" buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXQ==\""
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"" buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"" buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
buildConfigField "String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\""
} }
} }
android.applicationVariants.all { variant -> android.applicationVariants.all { variant ->
variant.outputs.each { output -> variant.outputs.each { output ->
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk") if (output.baseName.contains('nightly')) {
def abiName = output.getFilter("ABI") ?: 'universal' output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
def postFix = abiPostFix.get(abiName, 0) def tag = getCurrentGitTag()
if (tag != null && tag.length() > 0) {
output.versionNameOverride = tag
}
} else {
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
if (postFix >= postFixSize) throw new AssertionError("postFix is too large") if (postFix >= postFixSize) throw new AssertionError("postFix is too large")
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
} }
} }
@@ -300,15 +370,15 @@ android {
def distribution = variant.getFlavors().get(0).name def distribution = variant.getFlavors().get(0).name
def environment = variant.getFlavors().get(1).name def environment = variant.getFlavors().get(1).name
def buildType = variant.buildType.name def buildType = variant.buildType.name
def fullName = distribution + environment.capitalize() + buildType.capitalize()
if (distribution == 'study' && buildType != 'perf' && buildType != 'mock') { if (!selectableVariants.contains(fullName)) {
variant.setIgnore(true)
} else if (distribution != 'study' && buildType == 'mock') {
variant.setIgnore(true) variant.setIgnore(true)
} }
} }
lintOptions { lintOptions {
checkReleaseBuilds false
abortOnError true abortOnError true
baseline file("lint-baseline.xml") baseline file("lint-baseline.xml")
disable "LintError" disable "LintError"
@@ -322,209 +392,163 @@ android {
} }
dependencies { dependencies {
implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation libs.androidx.core.ktx
implementation libs.androidx.fragment.ktx
lintChecks project(':lintchecks') lintChecks project(':lintchecks')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' coreLibraryDesugaring libs.android.tools.desugar
implementation ('androidx.appcompat:appcompat:1.2.0') { implementation (libs.androidx.appcompat) {
force = true version {
strictly '1.2.0'
}
} }
implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation libs.androidx.window
implementation 'com.google.android.material:material:1.3.0' implementation libs.androidx.recyclerview
implementation 'androidx.legacy:legacy-support-v13:1.0.0' implementation libs.material.material
implementation 'androidx.cardview:cardview:1.0.0' implementation libs.androidx.legacy.support
implementation 'androidx.preference:preference:1.0.0' implementation libs.androidx.cardview
implementation 'androidx.legacy:legacy-preference-v14:1.0.0' implementation libs.androidx.preference
implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation libs.androidx.legacy.preference
implementation 'androidx.exifinterface:exifinterface:1.0.0' implementation libs.androidx.gridlayout
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation libs.androidx.exifinterface
implementation 'androidx.multidex:multidex:2.0.1' implementation libs.androidx.constraintlayout
implementation 'androidx.navigation:navigation-fragment:2.1.0' implementation libs.androidx.multidex
implementation 'androidx.navigation:navigation-ui:2.1.0' implementation libs.androidx.navigation.fragment.ktx
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0' implementation libs.androidx.navigation.ui.ktx
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05' implementation libs.androidx.lifecycle.extensions
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0' implementation libs.androidx.lifecycle.viewmodel.savedstate
implementation "androidx.camera:camera-core:1.0.0-beta11" implementation libs.androidx.lifecycle.common.java8
implementation "androidx.camera:camera-camera2:1.0.0-beta11" implementation libs.androidx.lifecycle.reactivestreams.ktx
implementation "androidx.camera:camera-lifecycle:1.0.0-beta11" implementation libs.androidx.camera.core
implementation "androidx.camera:camera-view:1.0.0-alpha18" implementation libs.androidx.camera.camera2
implementation "androidx.concurrent:concurrent-futures:1.0.0" implementation libs.androidx.camera.lifecycle
implementation "androidx.autofill:autofill:1.0.0" implementation libs.androidx.camera.view
implementation "androidx.biometric:biometric:1.1.0" implementation libs.androidx.concurrent.futures
implementation libs.androidx.autofill
implementation libs.androidx.biometric
implementation libs.androidx.sharetarget
implementation ('com.google.firebase:firebase-messaging:20.2.0') { implementation (libs.firebase.messaging) {
exclude group: 'com.google.firebase', module: 'firebase-core' exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics' exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
} }
implementation 'com.google.android.gms:play-services-maps:16.1.0' implementation libs.google.play.services.maps
implementation 'com.google.android.gms:play-services-auth:16.0.1' implementation libs.google.play.services.auth
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1' implementation libs.bundles.exoplayer
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'com.google.android.exoplayer:extension-mediasession:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.0.0' implementation libs.conscrypt.android
implementation 'org.signal:aesgcmprovider:0.0.3' implementation libs.signal.aesgcmprovider
implementation project(':libsignal-service') implementation project(':libsignal-service')
implementation project(':paging') implementation project(':paging')
implementation project(':core-util') implementation project(':core-util')
implementation project(':glide-config')
implementation project(':video') implementation project(':video')
implementation project(':device-transfer') implementation project(':device-transfer')
implementation project(':image-editor')
implementation project(':donations')
implementation 'org.signal:zkgroup-android:0.7.0' implementation libs.signal.client.android
implementation 'org.whispersystems:signal-client-android:0.8.0' implementation libs.google.protobuf.javalite
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
implementation('com.mobilecoin:android-sdk:1.0.0') { implementation(libs.mobilecoin) {
exclude group: 'com.google.protobuf' exclude group: 'com.google.protobuf'
} }
implementation 'org.signal:argon2:13.1@aar' implementation(libs.signal.argon2) {
artifact {
type = "aar"
}
}
implementation 'org.signal:ringrtc-android:2.10.1.1' implementation libs.signal.ringrtc
implementation "me.leolin:ShortcutBadger:1.1.22" implementation libs.leolin.shortcutbadger
implementation 'se.emilsjolander:stickylistheaders:2.7.0' implementation libs.emilsjolander.stickylistheaders
implementation 'com.jpardogo.materialtabstrip:library:1.0.9' implementation libs.jpardogo.materialtabstrip
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5' implementation libs.apache.httpclient.android
implementation 'com.github.chrisbanes:PhotoView:2.1.3' implementation libs.photoview
implementation 'com.github.bumptech.glide:glide:4.11.0' implementation libs.glide.glide
kapt 'com.github.bumptech.glide:compiler:4.11.0' implementation libs.roundedimageview
kapt 'androidx.annotation:annotation:1.1.0' implementation libs.materialish.progress
implementation 'com.makeramen:roundedimageview:2.1.0' implementation libs.greenrobot.eventbus
implementation 'com.pnikosis:materialish-progress:1.5' implementation libs.waitingdots
implementation 'org.greenrobot:eventbus:3.0.0' implementation libs.floatingactionbutton
implementation 'pl.tajchert:waitingdots:0.1.0' implementation libs.google.zxing.android.integration
implementation 'com.melnykov:floatingactionbutton:1.3.0' implementation libs.time.duration.picker
implementation 'com.google.zxing:android-integration:3.1.0' implementation libs.google.zxing.core
implementation 'mobi.upod:time-duration-picker:1.1.3' implementation (libs.subsampling.scale.image.view) {
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.zxing:core:3.2.1'
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
exclude group: 'com.android.support', module: 'support-annotations' exclude group: 'com.android.support', module: 'support-annotations'
} }
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') { implementation (libs.numberpickerview) {
exclude group: 'com.android.support', module: 'appcompat-v7' exclude group: 'com.android.support', module: 'appcompat-v7'
} }
implementation ('com.tomergoldst.android:tooltips:1.0.6') { implementation (libs.android.tooltips) {
exclude group: 'com.android.support', module: 'appcompat-v7' exclude group: 'com.android.support', module: 'appcompat-v7'
} }
implementation ('com.klinkerapps:android-smsmms:4.0.1') { implementation (libs.android.smsmms) {
exclude group: 'com.squareup.okhttp', module: 'okhttp' exclude group: 'com.squareup.okhttp', module: 'okhttp'
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection' exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
} }
implementation 'com.annimon:stream:1.1.8' implementation libs.stream
implementation ('com.takisoft.fix:colorpicker:0.9.1') { implementation (libs.colorpicker) {
exclude group: 'com.android.support', module: 'appcompat-v7' exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7' exclude group: 'com.android.support', module: 'recyclerview-v7'
} }
implementation 'com.airbnb.android:lottie:3.6.0' implementation libs.lottie
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4' implementation libs.stickyheadergrid
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2' implementation libs.circular.progress.button
implementation "net.zetetic:android-database-sqlcipher:4.4.3" implementation libs.signal.android.database.sqlcipher
implementation "androidx.sqlite:sqlite:2.1.0" implementation libs.androidx.sqlite
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') { implementation (libs.google.ez.vcard) {
exclude group: 'com.fasterxml.jackson.core' exclude group: 'com.fasterxml.jackson.core'
exclude group: 'org.freemarker' exclude group: 'org.freemarker'
} }
implementation 'dnsjava:dnsjava:2.1.9' implementation libs.dnsjava
flipperImplementation 'com.facebook.flipper:flipper:0.91.0' flipperImplementation libs.facebook.flipper
flipperImplementation 'com.facebook.soloader:soloader:0.10.1' flipperImplementation libs.facebook.soloader
flipperImplementation libs.square.leakcanary
testImplementation 'junit:junit:4.12' testImplementation testLibs.junit.junit
testImplementation 'org.assertj:assertj-core:3.11.1' testImplementation testLibs.assertj.core
testImplementation 'org.mockito:mockito-core:2.8.9' testImplementation testLibs.mockito.core
testImplementation 'org.powermock:powermock-api-mockito2:1.7.4' testImplementation testLibs.powermock.api.mockito
testImplementation 'org.powermock:powermock-module-junit4:1.7.4' testImplementation testLibs.powermock.module.junit4.core
testImplementation 'org.powermock:powermock-module-junit4-rule:1.7.4' testImplementation testLibs.powermock.module.junit4.rule
testImplementation 'org.powermock:powermock-classloading-xstream:1.7.4' testImplementation testLibs.powermock.classloading.xstream
testImplementation 'androidx.test:core:1.2.0' testImplementation testLibs.androidx.test.core
testImplementation ('org.robolectric:robolectric:4.4') { testImplementation (testLibs.robolectric.robolectric) {
exclude group: 'com.google.protobuf', module: 'protobuf-java' exclude group: 'com.google.protobuf', module: 'protobuf-java'
} }
testImplementation 'org.robolectric:shadows-multidex:4.4' testImplementation testLibs.robolectric.shadows.multidex
testImplementation 'org.hamcrest:hamcrest:2.2' testImplementation testLibs.hamcrest.hamcrest
androidTestImplementation 'androidx.test.ext:junit:1.1.1' testImplementation(testFixtures(project(":libsignal-service")))
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" androidTestImplementation testLibs.androidx.test.ext.junit
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0" androidTestImplementation testLibs.espresso.core
}
dependencyVerification { testImplementation testLibs.espresso.core
configuration = '(play|website)(Prod|Staging)(Debug|Release)RuntimeClasspath'
}
def assembleWebsiteDescriptor = { variant, file -> implementation libs.kotlin.stdlib.jdk8
if (file.exists()) { implementation libs.kotlin.reflect
MessageDigest md = MessageDigest.getInstance("SHA-256"); implementation libs.jackson.module.kotlin
file.eachByte 4096, {bytes, size ->
md.update(bytes, 0, size);
}
String digest = md.digest().collect {String.format "%02x", it}.join(); implementation libs.rxjava3.rxandroid
String url = variant.productFlavors.get(0).ext.websiteUpdateUrl implementation libs.rxjava3.rxkotlin
String apkName = file.getName() implementation libs.rxdogtag
String descriptor = "{" + androidTestUtil 'androidx.test:orchestrator:1.4.0'
"\"versionCode\" : ${canonicalVersionCode * postFixSize + abiPostFix['universal']}," +
"\"versionName\" : \"$canonicalVersionName\"," +
"\"sha256sum\" : \"$digest\"," +
"\"url\" : \"$url/$apkName\"" +
"}"
File descriptorFile = new File(file.getParent(), apkName.replace(".apk", ".json"))
descriptorFile.write(descriptor)
}
}
def signProductionRelease = { variant ->
variant.outputs.collect { output ->
String apkName = output.outputFile.name
File inputFile = new File(output.outputFile.path)
File outputFile = new File(output.outputFile.parent, apkName.replace('-unsigned', ''))
new ApkSignerUtil('sun.security.pkcs11.SunPKCS11',
'pkcs11.config',
'PKCS11',
'file:pkcs11.password').calculateSignature(inputFile.getAbsolutePath(),
outputFile.getAbsolutePath())
inputFile.delete()
outputFile
}
}
task signProductionPlayRelease {
doLast {
signProductionRelease(android.applicationVariants.find { (it.name == 'playProdRelease') })
}
}
task signProductionInternalRelease {
doLast {
signProductionRelease(android.applicationVariants.find { (it.name == 'internalProdRelease') })
}
}
task signProductionWebsiteRelease {
doLast {
def variant = android.applicationVariants.find { (it.name == 'websiteProdRelease') }
File signedRelease = signProductionRelease(variant).find { it.name.contains('universal') }
assembleWebsiteDescriptor(variant, signedRelease)
}
} }
def getLastCommitTimestamp() { def getLastCommitTimestamp() {
@@ -556,6 +580,27 @@ def getGitHash() {
return stdout.toString().trim() return stdout.toString().trim()
} }
def getCurrentGitTag() {
if (!(new File('.git').exists())) {
return ''
}
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'tag', '--points-at', 'HEAD'
standardOutput = stdout
}
def output = stdout.toString().trim()
if (output != null && output.size() > 0) {
def tags = output.split('\n').toList()
return tags.stream().filter(t -> t.contains('nightly')).findFirst().orElse(tags.get(0))
} else {
return null
}
}
tasks.withType(Test) { tasks.withType(Test) {
testLogging { testLogging {
events "failed" events "failed"
@@ -576,3 +621,9 @@ def loadKeystoreProperties(filename) {
return null; return null;
} }
} }
def getDateSuffix() {
def date = new Date()
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
return formattedDate
}
+4 -1
View File
@@ -2,4 +2,7 @@
-keep class org.sqlite.database.** { *; } -keep class org.sqlite.database.** { *; }
-keep class net.sqlcipher.** { *; } -keep class net.sqlcipher.** { *; }
-dontwarn net.sqlcipher.** -dontwarn net.sqlcipher.**
-keep class net.zetetic.** { *; }
-dontwarn net.zetetic.**
+2
View File
@@ -9,3 +9,5 @@
# Protobuf lite # Protobuf lite
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; } -keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
-keep class androidx.window.** { *; }
@@ -0,0 +1,436 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.keyvalue.AccountValues
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.MockKeyValuePersistentStorage
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.push.ACI
import java.lang.IllegalArgumentException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest {
private lateinit var recipientDatabase: RecipientDatabase
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
ensureDbEmpty()
}
// ==============================================================
// If both the ACI and E164 map to no one
// ==============================================================
/** If all you have is an ACI, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireAci())
assertFalse(recipient.hasE164())
}
/** If all you have is an ACI, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciOnly_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireAci())
assertFalse(recipient.hasE164())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasAci())
}
/** If all you have is an E164, you can just store that, regardless of trust level. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_e164Only_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(E164_A, recipient.requireE164())
assertFalse(recipient.hasAci())
}
/** With high trust, you can associate an ACI-e164 pair. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_highTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireAci())
assertEquals(E164_A, recipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore can only store the ACI. */
@Test
fun getAndPossiblyMerge_aciAndE164MapToNoOne_aciAndE164_lowTrust() {
val recipientId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
val recipient = Recipient.resolved(recipientId)
assertEquals(ACI_A, recipient.requireAci())
assertFalse(recipient.hasE164())
}
// ==============================================================
// If the ACI maps to an existing user, but the E164 doesn't
// ==============================================================
/** With high trust, you can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore cannot store the e164. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_lowTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
}
/** Basically the change number case. High trust lets you update the existing user. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_B, retrievedRecipient.requireE164())
}
/** Low trust means you cant update the underlying data, but you also dont need to create any new rows. */
@Test
fun getAndPossiblyMerge_aciMapsToExistingUserButE164DoesNot_aciAndE164_2_lowTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, false)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
// ==============================================================
// If the E164 maps to an existing user, but the ACI doesn't
// ==============================================================
/** With high trust, you can associate an e164 with an existing ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** With low trust, you cannot associate an ACI-e164 pair, and therefore need to create a new person with just the ACI. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_lowTrust() {
val existingId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(E164_A, existingRecipient.requireE164())
assertFalse(existingRecipient.hasAci())
}
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. But high trust lets us take the e164 from the current holder. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireAci())
assertFalse(existingRecipient.hasE164())
}
/** We never change the ACI of an existing row. New ACI = new person, regardless of trust. And low trust means we cant take the e164. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_aciAndE164_2_lowTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, false)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireAci())
assertEquals(E164_A, existingRecipient.requireE164())
}
/** We never want to remove the e164 of our own contact entry. So basically treat this as a low-trust case, and leave the e164 alone. */
@Test
fun getAndPossiblyMerge_e164MapsToExistingUserButAciDoesNot_e164BelongsToLocalUser_highTrust() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_A.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
assertNotEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_B, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingRecipient = Recipient.resolved(existingId)
assertEquals(ACI_A, existingRecipient.requireAci())
assertEquals(E164_A, existingRecipient.requireE164())
}
// ==============================================================
// If both the ACI and E164 map to an existing user
// ==============================================================
/** Regardless of trust, if your ACI and e164 match, youre good. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_highTrust() {
val existingId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
}
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_merge_highTrust() {
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(retrievedId, existingE164Recipient.id)
}
/** Low trust means you cant merge. If youre retrieving a user from the table with this data, prefer the ACI one. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_lowTrust() {
val existingAciId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val existingE164Id: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingAciId, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val existingE164Recipient = Recipient.resolved(existingE164Id)
assertEquals(E164_A, existingE164Recipient.requireE164())
assertFalse(existingE164Recipient.hasAci())
}
/** Another high trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_highTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireAci())
assertFalse(existingRecipient2.hasE164())
}
/** Another low trust case. No new rules here, just a more complex scenario to show how different rules interact. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_complex_lowTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, false)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_B, retrievedRecipient.requireE164())
val existingRecipient2 = Recipient.resolved(existingId2)
assertEquals(ACI_B, existingRecipient2.requireAci())
assertEquals(E164_A, existingRecipient2.requireE164())
}
/**
* Another high trust case that results in a merge. Nothing strictly new here, but this case is called out because its a merge but *also* an E164 change,
* which clients may need to know for UX purposes.
*/
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_aciAndE164_mergeAndPhoneNumberChange_highTrust() {
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_B, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(null, E164_A, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId1, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
assertFalse(recipientDatabase.getByE164(E164_B).isPresent)
val recipientWithId2 = Recipient.resolved(existingId2)
assertEquals(retrievedId, recipientWithId2.id)
}
/** We never want to remove the e164 of our own contact entry. So basically treat this as a low-trust case, and leave the e164 alone. */
@Test
fun getAndPossiblyMerge_bothAciAndE164MapToExistingUser_e164BelongsToLocalUser() {
val dataSet = KeyValueDataSet().apply {
putString(AccountValues.KEY_E164, E164_A)
putString(AccountValues.KEY_ACI, ACI_B.toString())
}
SignalStore.inject(KeyValueStore(MockKeyValuePersistentStorage.withDataSet(dataSet)))
val existingId1: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_B, E164_A, true)
val existingId2: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, null, true)
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
assertEquals(existingId2, retrievedId)
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertFalse(retrievedRecipient.hasE164())
val recipientWithId1 = Recipient.resolved(existingId1)
assertEquals(ACI_B, recipientWithId1.requireAci())
assertEquals(E164_A, recipientWithId1.requireE164())
}
// ==============================================================
// Misc
// ==============================================================
@Test
fun createByE164SanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
// WHEN I retrieve one by E164
val possible: Optional<RecipientId> = recipientDatabase.getByE164(E164_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.e164.isPresent)
assertEquals(E164_A, recipient.e164.get())
}
@Test
fun createByUuidSanityCheck() {
// GIVEN one recipient
val recipientId: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
// WHEN I retrieve one by UUID
val possible: Optional<RecipientId> = recipientDatabase.getByAci(ACI_A)
// THEN I get it back, and it has the properties I expect
assertTrue(possible.isPresent)
assertEquals(recipientId, possible.get())
val recipient = Recipient.resolved(recipientId)
assertTrue(recipient.aci.isPresent)
assertEquals(ACI_A, recipient.aci.get())
}
@Test(expected = IllegalArgumentException::class)
fun getAndPossiblyMerge_noArgs_invalid() {
recipientDatabase.getAndPossiblyMerge(null, null, true)
}
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0))
}
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
}
}
@@ -0,0 +1,262 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.signal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.thoughtcrime.securesms.util.CursorUtil
import org.whispersystems.libsignal.IdentityKey
import org.whispersystems.libsignal.SignalProtocolAddress
import org.whispersystems.libsignal.state.SessionRecord
import org.whispersystems.libsignal.util.guava.Optional
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientDatabaseTest_merges {
private lateinit var recipientDatabase: RecipientDatabase
private lateinit var identityDatabase: IdentityDatabase
private lateinit var groupReceiptDatabase: GroupReceiptDatabase
private lateinit var groupDatabase: GroupDatabase
private lateinit var threadDatabase: ThreadDatabase
private lateinit var smsDatabase: MessageDatabase
private lateinit var mmsDatabase: MessageDatabase
private lateinit var sessionDatabase: SessionDatabase
private lateinit var mentionDatabase: MentionDatabase
private lateinit var reactionDatabase: ReactionDatabase
private lateinit var notificationProfileDatabase: NotificationProfileDatabase
@Before
fun setup() {
recipientDatabase = SignalDatabase.recipients
identityDatabase = SignalDatabase.identities
groupReceiptDatabase = SignalDatabase.groupReceipts
groupDatabase = SignalDatabase.groups
threadDatabase = SignalDatabase.threads
smsDatabase = SignalDatabase.sms
mmsDatabase = SignalDatabase.mms
sessionDatabase = SignalDatabase.sessions
mentionDatabase = SignalDatabase.mentions
reactionDatabase = SignalDatabase.reactions
notificationProfileDatabase = SignalDatabase.notificationProfiles
ensureDbEmpty()
}
/** High trust lets you merge two different users into one. You should prefer the ACI user. Not shown: merging threads, dropping e164 sessions, etc. */
@Test
fun getAndPossiblyMerge_general() {
// Setup
val recipientIdAci: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_A)
val recipientIdE164: RecipientId = recipientDatabase.getOrInsertFromE164(E164_A)
val recipientIdAciB: RecipientId = recipientDatabase.getOrInsertFromAci(ACI_B)
val smsId1: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 0, body = "0")).get().messageId
val smsId2: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdE164, time = 1, body = "1")).get().messageId
val smsId3: Long = smsDatabase.insertMessageInbox(smsMessage(sender = recipientIdAci, time = 2, body = "2")).get().messageId
val mmsId1: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 3, body = "3"), -1).get().messageId
val mmsId2: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdE164, time = 4, body = "4"), -1).get().messageId
val mmsId3: Long = mmsDatabase.insertSecureDecryptedMessageInbox(mmsMessage(sender = recipientIdAci, time = 5, body = "5"), -1).get().messageId
val threadIdAci: Long = threadDatabase.getThreadIdFor(recipientIdAci)!!
val threadIdE164: Long = threadDatabase.getThreadIdFor(recipientIdE164)!!
assertNotEquals(threadIdAci, threadIdE164)
mentionDatabase.insert(threadIdAci, mmsId1, listOf(Mention(recipientIdE164, 0, 1)))
mentionDatabase.insert(threadIdE164, mmsId2, listOf(Mention(recipientIdAci, 0, 1)))
groupReceiptDatabase.insert(listOf(recipientIdAci, recipientIdE164), mmsId1, 0, 3)
val identityKeyAci: IdentityKey = identityKey(1)
val identityKeyE164: IdentityKey = identityKey(2)
identityDatabase.saveIdentity(ACI_A.toString(), recipientIdAci, identityKeyAci, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
identityDatabase.saveIdentity(E164_A, recipientIdE164, identityKeyE164, IdentityDatabase.VerifiedStatus.VERIFIED, false, 0, false)
sessionDatabase.store(SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord())
reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1))
reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1))
val profile1: NotificationProfile = notificationProfile(name = "Test")
val profile2: NotificationProfile = notificationProfile(name = "Test2")
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdAci)
notificationProfileDatabase.addAllowedRecipient(profileId = profile1.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdE164)
notificationProfileDatabase.addAllowedRecipient(profileId = profile2.id, recipientId = recipientIdAciB)
// Merge
val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true)
val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!!
assertEquals(recipientIdAci, retrievedId)
// Recipient validation
val retrievedRecipient = Recipient.resolved(retrievedId)
assertEquals(ACI_A, retrievedRecipient.requireAci())
assertEquals(E164_A, retrievedRecipient.requireE164())
val existingE164Recipient = Recipient.resolved(recipientIdE164)
assertEquals(retrievedId, existingE164Recipient.id)
// Thread validation
assertEquals(threadIdAci, retrievedThreadId)
assertNull(threadDatabase.getThreadIdFor(recipientIdE164))
assertNull(threadDatabase.getThreadRecord(threadIdE164))
// SMS validation
val sms1: MessageRecord = smsDatabase.getMessageRecord(smsId1)!!
val sms2: MessageRecord = smsDatabase.getMessageRecord(smsId2)!!
val sms3: MessageRecord = smsDatabase.getMessageRecord(smsId3)!!
assertEquals(retrievedId, sms1.recipient.id)
assertEquals(retrievedId, sms2.recipient.id)
assertEquals(retrievedId, sms3.recipient.id)
assertEquals(retrievedThreadId, sms1.threadId)
assertEquals(retrievedThreadId, sms2.threadId)
assertEquals(retrievedThreadId, sms3.threadId)
// MMS validation
val mms1: MessageRecord = mmsDatabase.getMessageRecord(mmsId1)!!
val mms2: MessageRecord = mmsDatabase.getMessageRecord(mmsId2)!!
val mms3: MessageRecord = mmsDatabase.getMessageRecord(mmsId3)!!
assertEquals(retrievedId, mms1.recipient.id)
assertEquals(retrievedId, mms2.recipient.id)
assertEquals(retrievedId, mms3.recipient.id)
assertEquals(retrievedThreadId, mms1.threadId)
assertEquals(retrievedThreadId, mms2.threadId)
assertEquals(retrievedThreadId, mms3.threadId)
// Mention validation
val mention1: MentionModel = getMention(mmsId1)
assertEquals(retrievedId, mention1.recipientId)
assertEquals(retrievedThreadId, mention1.threadId)
val mention2: MentionModel = getMention(mmsId2)
assertEquals(retrievedId, mention2.recipientId)
assertEquals(retrievedThreadId, mention2.threadId)
// Group receipt validation
val groupReceipts: List<GroupReceiptDatabase.GroupReceiptInfo> = groupReceiptDatabase.getGroupReceiptInfo(mmsId1)
assertEquals(retrievedId, groupReceipts[0].recipientId)
assertEquals(retrievedId, groupReceipts[1].recipientId)
// Identity validation
assertEquals(identityKeyAci, identityDatabase.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
assertNull(identityDatabase.getIdentityStoreRecord(E164_A))
// Session validation
assertNotNull(sessionDatabase.load(SignalProtocolAddress(ACI_A.toString(), 1)))
// Reaction validation
val reactionsSms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(smsId1, false))
val reactionsMms: List<ReactionRecord> = reactionDatabase.getReactions(MessageId(mmsId1, true))
assertEquals(1, reactionsSms.size)
assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0])
assertEquals(1, reactionsMms.size)
assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0])
// Notification Profile validation
val updatedProfile1: NotificationProfile = notificationProfileDatabase.getProfile(profile1.id)!!
val updatedProfile2: NotificationProfile = notificationProfileDatabase.getProfile(profile2.id)!!
assertThat("Notification Profile 1 should now only contain ACI $recipientIdAci", updatedProfile1.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci))
assertThat("Notification Profile 2 should now contain ACI A ($recipientIdAci) and ACI B ($recipientIdAciB)", updatedProfile2.allowedMembers, Matchers.containsInAnyOrder(recipientIdAci, recipientIdAciB))
}
private val context: Application
get() = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application
private fun ensureDbEmpty() {
SignalDatabase.rawDatabase.rawQuery("SELECT COUNT(*) FROM ${RecipientDatabase.TABLE_NAME}", null).use { cursor ->
assertTrue(cursor.moveToFirst())
assertEquals(0, cursor.getLong(0))
}
}
private fun smsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.absent()): IncomingTextMessage {
return IncomingTextMessage(sender, 1, time, time, time, body, groupId, 0, true, null)
}
private fun mmsMessage(sender: RecipientId, time: Long = 0, body: String = "", groupId: Optional<GroupId> = Optional.absent()): IncomingMediaMessage {
return IncomingMediaMessage(sender, groupId, body, time, time, time, emptyList(), 0, 0, false, false, true, Optional.absent())
}
private fun identityKey(value: Byte): IdentityKey {
val bytes = ByteArray(33)
bytes[0] = 0x05
bytes[1] = value
return IdentityKey(bytes)
}
private fun groupMasterKey(value: Byte): GroupMasterKey {
val bytes = ByteArray(32)
bytes[0] = value
return GroupMasterKey(bytes)
}
private fun decryptedGroup(members: Collection<UUID>): DecryptedGroup {
return DecryptedGroup.newBuilder()
.addAllMembers(members.map { DecryptedMember.newBuilder().setUuid(UuidUtil.toByteString(it)).build() })
.build()
}
private fun getMention(messageId: Long): MentionModel {
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionDatabase.TABLE_NAME} WHERE ${MentionDatabase.MESSAGE_ID} = $messageId").use { cursor ->
cursor.moveToFirst()
return MentionModel(
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionDatabase.RECIPIENT_ID)),
threadId = CursorUtil.requireLong(cursor, MentionDatabase.THREAD_ID)
)
}
}
private fun notificationProfile(name: String): NotificationProfile {
return (notificationProfileDatabase.createProfile(name = name, emoji = "", color = AvatarColor.A210, System.currentTimeMillis()) as NotificationProfileDatabase.NotificationProfileChangeResult.Success).notificationProfile
}
/** The normal mention model doesn't have a threadId, so we need to do it ourselves for the test */
data class MentionModel(
val recipientId: RecipientId,
val threadId: Long
)
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val E164_A = "+12221234567"
val E164_B = "+13331234567"
}
}
@@ -1,6 +1,9 @@
package org.thoughtcrime.securesms.lock; package org.thoughtcrime.securesms.lock;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.Hex;
import org.whispersystems.signalservice.api.kbs.HashedPin; import org.whispersystems.signalservice.api.kbs.HashedPin;
import org.whispersystems.signalservice.api.kbs.KbsData; import org.whispersystems.signalservice.api.kbs.KbsData;
@@ -12,6 +15,7 @@ import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@RunWith(AndroidJUnit4.class)
public final class PinHashing_hashPin_Test { public final class PinHashing_hashPin_Test {
@Test @Test
@@ -1,27 +0,0 @@
package org.thoughtcrime.securesms;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.soloader.SoLoader;
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter;
public class FlipperApplicationContext extends ApplicationContext {
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
FlipperClient client = AndroidFlipperClient.getInstance(this);
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
client.addPlugin(new DatabasesFlipperPlugin(new FlipperSqlCipherAdapter(this)));
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
client.start();
}
}
@@ -0,0 +1,60 @@
package org.thoughtcrime.securesms
import com.facebook.flipper.android.AndroidFlipperClient
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin
import com.facebook.flipper.plugins.inspector.DescriptorMapping
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin
import com.facebook.soloader.SoLoader
import leakcanary.LeakCanary
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter
import shark.AndroidReferenceMatchers
class FlipperApplicationContext : ApplicationContext() {
override fun onCreate() {
super.onCreate()
SoLoader.init(this, false)
val client = AndroidFlipperClient.getInstance(this)
client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
client.addPlugin(DatabasesFlipperPlugin(FlipperSqlCipherAdapter(this)))
client.addPlugin(SharedPreferencesFlipperPlugin(this))
client.start()
LeakCanary.config = LeakCanary.config.copy(
referenceMatchers = AndroidReferenceMatchers.appDefaults +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.service.media.MediaBrowserService\$ServiceBinder",
fieldName = "this\$0"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "androidx.media.MediaBrowserServiceCompat\$MediaBrowserServiceImplApi26\$MediaBrowserServiceApi26",
fieldName = "mBase"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.support.v4.media.MediaBrowserCompat",
fieldName = "mImpl"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.support.v4.media.session.MediaControllerCompat",
fieldName = "mToken"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "android.support.v4.media.session.MediaControllerCompat",
fieldName = "mImpl"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackService",
fieldName = "mApplication"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "org.thoughtcrime.securesms.service.GenericForegroundService\$LocalBinder",
fieldName = "this\$0"
) +
AndroidReferenceMatchers.ignoredInstanceField(
className = "org.thoughtcrime.securesms.contacts.ContactsSyncAdapter",
fieldName = "mContext"
)
)
}
}
@@ -11,12 +11,11 @@ import androidx.annotation.Nullable;
import com.facebook.flipper.plugins.databases.DatabaseDescriptor; import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
import com.facebook.flipper.plugins.databases.DatabaseDriver; import com.facebook.flipper.plugins.databases.DatabaseDriver;
import net.sqlcipher.DatabaseUtils; import net.zetetic.database.DatabaseUtils;
import net.sqlcipher.database.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.sqlcipher.database.SQLiteStatement; import net.zetetic.database.sqlcipher.SQLiteStatement;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.util.Hex; import org.thoughtcrime.securesms.util.Hex;
import java.lang.reflect.Field; import java.lang.reflect.Field;
@@ -43,18 +42,17 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
@Override @Override
public List<Descriptor> getDatabases() { public List<Descriptor> getDatabases() {
try { try {
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper"); SignalDatabaseOpenHelper mainOpenHelper = Objects.requireNonNull(SignalDatabase.getInstance());
databaseHelperField.setAccessible(true); SignalDatabaseOpenHelper keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
SignalDatabaseOpenHelper megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
SignalDatabase mainOpenHelper = Objects.requireNonNull((SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()))); SignalDatabaseOpenHelper jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext()); SignalDatabaseOpenHelper metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
return Arrays.asList(new Descriptor(mainOpenHelper), return Arrays.asList(new Descriptor(mainOpenHelper),
new Descriptor(keyValueOpenHelper), new Descriptor(keyValueOpenHelper),
new Descriptor(megaphoneOpenHelper), new Descriptor(megaphoneOpenHelper),
new Descriptor(jobManagerOpenHelper)); new Descriptor(jobManagerOpenHelper),
new Descriptor(metricsOpenHelper));
} catch (Exception e) { } catch (Exception e) {
Log.i(TAG, "Unable to use reflection to access raw database.", e); Log.i(TAG, "Unable to use reflection to access raw database.", e);
} }
@@ -251,9 +249,9 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
} }
static class Descriptor implements DatabaseDescriptor { static class Descriptor implements DatabaseDescriptor {
private final SignalDatabase sqlCipherOpenHelper; private final SignalDatabaseOpenHelper sqlCipherOpenHelper;
Descriptor(@NonNull SignalDatabase sqlCipherOpenHelper) { Descriptor(@NonNull SignalDatabaseOpenHelper sqlCipherOpenHelper) {
this.sqlCipherOpenHelper = sqlCipherOpenHelper; this.sqlCipherOpenHelper = sqlCipherOpenHelper;
} }
+45 -43
View File
@@ -96,14 +96,22 @@
android:label="@string/app_name" android:label="@string/app_name"
android:supportsRtl="true" android:supportsRtl="true"
tools:replace="android:allowBackup" tools:replace="android:allowBackup"
android:resizeableActivity="true"
android:allowBackup="false" android:allowBackup="false"
android:theme="@style/TextSecure.LightTheme" android:theme="@style/TextSecure.LightTheme"
android:largeHeap="true"> android:largeHeap="true">
<meta-data
android:name="com.google.android.gms.wallet.api.enabled"
android:value="true" />
<meta-data <meta-data
android:name="com.google.android.geo.API_KEY" android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/> android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
<meta-data android:name="android.supports_size_changes"
android:value="true" />
<meta-data android:name="com.google.android.gms.version" <meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" /> android:value="@integer/google_play_services_version" />
@@ -117,16 +125,15 @@
<activity android:name=".WebRtcCallActivity" <activity android:name=".WebRtcCallActivity"
android:theme="@style/TextSecure.DarkTheme.WebRTCCall" android:theme="@style/TextSecure.DarkTheme.WebRTCCall"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:screenOrientation="portrait"
android:supportsPictureInPicture="true" android:supportsPictureInPicture="true"
android:windowSoftInputMode="stateAlwaysHidden" android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:taskAffinity=".calling" android:taskAffinity=".calling"
android:resizeableActivity="true"
android:launchMode="singleTask"/> android:launchMode="singleTask"/>
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity" <activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
android:theme="@style/TextSecure.DarkNoActionBar" android:theme="@style/TextSecure.DarkNoActionBar"
android:screenOrientation="portrait"
android:noHistory="true" android:noHistory="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -173,7 +180,6 @@
<activity android:name=".sharing.ShareActivity" <activity android:name=".sharing.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity="" android:taskAffinity=""
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -198,7 +204,7 @@
<meta-data <meta-data
android:name="android.service.chooser.chooser_target_service" android:name="android.service.chooser.chooser_target_service"
android:value=".service.DirectShareService" /> android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
</activity> </activity>
@@ -240,6 +246,9 @@
<meta-data android:name="com.sec.minimode.icon.landscape.normal" <meta-data android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher" /> android:resource="@mipmap/ic_launcher" />
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity-alias> </activity-alias>
<activity android:name=".deeplinks.DeepLinkEntryActivity" <activity android:name=".deeplinks.DeepLinkEntryActivity"
@@ -272,6 +281,16 @@
<data android:scheme="sgnl" <data android:scheme="sgnl"
android:host="signal.tube" /> android:host="signal.tube" />
</intent-filter> </intent-filter>
<intent-filter>
<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.me" />
<data android:scheme="sgnl"
android:host="signal.me" />
</intent-filter>
</activity> </activity>
<activity android:name=".conversation.ConversationActivity" <activity android:name=".conversation.ConversationActivity"
@@ -302,20 +321,13 @@
<activity android:name=".messagedetails.MessageDetailsActivity" <activity android:name=".messagedetails.MessageDetailsActivity"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity" <activity android:name=".groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" /> android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".recipients.ui.managerecipient.ManageRecipientActivity"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity" <activity android:name=".recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar" android:theme="@style/Signal.DayNight.NoActionBar"
@@ -327,7 +339,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".migrations.ApplicationMigrationActivity" <activity android:name=".migrations.ApplicationMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -358,17 +370,18 @@
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.MediaSendActivity" <activity android:name=".mediasend.v2.MediaSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia" android:theme="@style/TextSecure.FullScreenMedia"
android:windowSoftInputMode="stateHidden" android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:launchMode="singleTop" android:launchMode="singleTop"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseChangeActivity" <activity android:name=".PassphraseChangeActivity"
android:label="@string/AndroidManifest__change_passphrase" android:label="@string/AndroidManifest__change_passphrase"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".VerifyIdentityActivity" <activity android:name=".verify.VerifyIdentityActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".components.settings.app.AppSettingsActivity" <activity android:name=".components.settings.app.AppSettingsActivity"
@@ -381,6 +394,17 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".components.settings.app.changenumber.ChangeNumberLockActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.ConversationSettings"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".wallpaper.ChatWallpaperActivity" <activity android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="stateAlwaysHidden"> android:windowSoftInputMode="stateAlwaysHidden">
@@ -479,6 +503,7 @@
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare" <activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:permission="android.permission.CALL_PHONE"
android:theme="@style/NoAnimation.Theme.BlackScreen" android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask" android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"> android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -652,13 +677,6 @@
<meta-data android:name="android.provider.CONTACTS_STRUCTURE" android:resource="@xml/contactsformat" /> <meta-data android:name="android.provider.CONTACTS_STRUCTURE" android:resource="@xml/contactsformat" />
</service> </service>
<service android:name=".service.DirectShareService"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
<intent-filter>
<action android:name="android.service.chooser.ChooserTargetService" />
</intent-filter>
</service>
<service android:name=".service.GenericForegroundService"/> <service android:name=".service.GenericForegroundService"/>
<service android:name=".gcm.FcmFetchService" /> <service android:name=".gcm.FcmFetchService" />
@@ -752,22 +770,6 @@
</provider> </provider>
<provider android:name=".database.DatabaseContentProviders$Conversation"
android:authorities="${applicationId}.database.conversation"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Attachment"
android:authorities="${applicationId}.database.attachment"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$Sticker"
android:authorities="${applicationId}.database.sticker"
android:exported="false" />
<provider android:name=".database.DatabaseContentProviders$StickerPack"
android:authorities="${applicationId}.database.stickerpack"
android:exported="false" />
<receiver android:name=".service.BootReceiver"> <receiver android:name=".service.BootReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/> <action android:name="android.intent.action.BOOT_COMPLETED"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 239 KiB

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 91 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -60,6 +60,7 @@ import androidx.camera.core.impl.LensFacingConverter;
import androidx.camera.core.impl.utils.executor.CameraXExecutors; import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback; import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures; import androidx.camera.core.impl.utils.futures.Futures;
import androidx.core.util.Consumer;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData; import androidx.lifecycle.LiveData;
@@ -130,6 +131,11 @@ public final class SignalCameraView extends FrameLayout {
// For accessibility event // For accessibility event
private MotionEvent mUpEvent; private MotionEvent mUpEvent;
// BEGIN Custom Signal Code Block
private Consumer<Throwable> errorConsumer;
private Throwable pendingError;
// END Custom Signal Code Block
public SignalCameraView(@NonNull Context context) { public SignalCameraView(@NonNull Context context) {
this(context, null); this(context, null);
} }
@@ -167,14 +173,32 @@ public final class SignalCameraView extends FrameLayout {
* androidx.lifecycle.Lifecycle.State#DESTROYED} state. * androidx.lifecycle.Lifecycle.State#DESTROYED} state.
* @throws IllegalStateException if camera permissions are not granted. * @throws IllegalStateException if camera permissions are not granted.
*/ */
// BEGIN Custom Signal Code Block
@RequiresPermission(permission.CAMERA) @RequiresPermission(permission.CAMERA)
public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner) { public void bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner, Consumer<Throwable> errorConsumer) {
mCameraModule.bindToLifecycle(lifecycleOwner); mCameraModule.bindToLifecycle(lifecycleOwner);
this.errorConsumer = errorConsumer;
if (pendingError != null) {
errorConsumer.accept(pendingError);
}
} }
// END Custom Signal Code Block
private void init(Context context, @Nullable AttributeSet attrs) { private void init(Context context, @Nullable AttributeSet attrs) {
addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */); addView(mPreviewView = new PreviewView(getContext()), 0 /* view position */);
mCameraModule = new SignalCameraXModule(this);
// Begin custom signal code block
mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
mCameraModule = new SignalCameraXModule(this, error -> {
if (errorConsumer != null) {
errorConsumer.accept(error);
} else {
pendingError = error;
}
});
// End custom signal code block
if (attrs != null) { if (attrs != null) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CameraView);
@@ -46,6 +46,7 @@ import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.impl.utils.futures.FutureCallback; import androidx.camera.core.impl.utils.futures.FutureCallback;
import androidx.camera.core.impl.utils.futures.Futures; import androidx.camera.core.impl.utils.futures.Futures;
import androidx.camera.lifecycle.ProcessCameraProvider; import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.util.Consumer;
import androidx.core.util.Preconditions; import androidx.core.util.Preconditions;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver; import androidx.lifecycle.LifecycleObserver;
@@ -123,7 +124,9 @@ final class SignalCameraXModule {
@Nullable @Nullable
ProcessCameraProvider mCameraProvider; ProcessCameraProvider mCameraProvider;
SignalCameraXModule(SignalCameraView view) { // BEGIN Custom Signal Code Block
SignalCameraXModule(SignalCameraView view, Consumer<Throwable> errorConsumer) {
// END Custom Signal Code Block
mCameraView = view; mCameraView = view;
Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()), Futures.addCallback(ProcessCameraProvider.getInstance(view.getContext()),
@@ -141,7 +144,9 @@ final class SignalCameraXModule {
@Override @Override
public void onFailure(Throwable t) { public void onFailure(Throwable t) {
throw new RuntimeException("CameraX failed to initialize.", t); // BEGIN Custom Signal Code Block
errorConsumer.accept(t);
// END Custom Signal Code Block
} }
}, CameraXExecutors.mainThreadExecutor()); }, CameraXExecutors.mainThreadExecutor());
@@ -222,17 +227,10 @@ final class SignalCameraXModule {
// End Signal Custom Code Block // End Signal Custom Code Block
Rational targetAspectRatio; Rational targetAspectRatio;
if (getCaptureMode() == SignalCameraView.CaptureMode.IMAGE) { // Begin Signal Custom Code Block
// Begin Signal Custom Code Block mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_4_3, isDisplayPortrait)); targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
// End Signal Custom Code Block // End Signal Custom Code Block
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_3_4 : ASPECT_RATIO_4_3;
} else {
// Begin Signal Custom Code Block
mImageCaptureBuilder.setTargetResolution(CameraXUtil.buildResolutionForRatio(resolution, ASPECT_RATIO_16_9, isDisplayPortrait));
// End Signal Custom Code Block
targetAspectRatio = isDisplayPortrait ? ASPECT_RATIO_9_16 : ASPECT_RATIO_16_9;
}
// Begin Signal Custom Code Block // Begin Signal Custom Code Block
mImageCaptureBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode()); mImageCaptureBuilder.setCaptureMode(CameraXUtil.getOptimalCaptureMode());
@@ -512,7 +510,12 @@ final class SignalCameraXModule {
return rotationDegrees; return rotationDegrees;
} }
@SuppressLint("UnsafeExperimentalUsageError")
public void invalidateView() { public void invalidateView() {
if (mPreview != null) {
mPreview.setTargetRotation(getDisplaySurfaceRotation()); // Fixes issue #10940 (rotation not updated on phones using "Legacy API")
}
updateViewInfo(); updateViewInfo();
} }
@@ -8,15 +8,17 @@ public final class AppCapabilities {
private AppCapabilities() { private AppCapabilities() {
} }
private static final boolean UUID_CAPABLE = false; private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true; private static final boolean GV2_CAPABLE = true;
private static final boolean GV1_MIGRATION = true; private static final boolean GV1_MIGRATION = true;
private static final boolean ANNOUNCEMENT_GROUPS = true;
private static final boolean SENDER_KEY = true;
/** /**
* @param storageCapable Whether or not the user can use storage service. This is another way of * @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not. * asking if the user has set a Signal PIN or not.
*/ */
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) { public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, FeatureFlags.senderKey()); return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, SENDER_KEY, ANNOUNCEMENT_GROUPS, FeatureFlags.changeNumber());
} }
} }
@@ -28,16 +28,21 @@ import androidx.multidex.MultiDexApplication;
import com.google.android.gms.security.ProviderInstaller; import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.Conscrypt; import org.conscrypt.Conscrypt;
import org.greenrobot.eventbus.EventBus;
import org.signal.aesgcmprovider.AesGcmProvider; import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger; import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.core.util.logging.PersistentLogger;
import org.signal.core.util.tracing.Tracer; import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs; import org.signal.glide.SignalGlideCodecs;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.signal.ringrtc.CallManager; import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider; import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource; import org.thoughtcrime.securesms.emoji.EmojiSource;
@@ -48,18 +53,21 @@ import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob; import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob; import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger; import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.LogSecretProvider; import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver; import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil; import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationUtil; import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger; import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
@@ -73,18 +81,26 @@ import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler; import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker; import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper; import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils;
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider; import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.security.Security; import java.security.Security;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.exceptions.OnErrorNotImplementedException;
import io.reactivex.rxjava3.exceptions.UndeliverableException;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import io.reactivex.rxjava3.schedulers.Schedulers;
import rxdogtag2.RxDogTag;
/** /**
* Will be called once when the TextSecure process is created. * Will be called once when the TextSecure process is created.
* *
@@ -107,6 +123,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
public void onCreate() { public void onCreate() {
Tracer.getInstance().start("Application#onCreate()"); Tracer.getInstance().start("Application#onCreate()");
AppStartup.getInstance().onApplicationCreate(); AppStartup.getInstance().onApplicationCreate();
SignalLocalMetrics.ColdStart.start();
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@@ -117,12 +134,19 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
super.onCreate(); super.onCreate();
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider) AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
.addBlocking("sqlcipher-init", () -> {
SqlCipherLibraryLoader.load();
SignalDatabase.init(this,
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
})
.addBlocking("logging", () -> { .addBlocking("logging", () -> {
initializeLogging(); initializeLogging();
Log.i(TAG, "onCreate()"); Log.i(TAG, "onCreate()");
}) })
.addBlocking("crash-handling", this::initializeCrashHandling) .addBlocking("crash-handling", this::initializeCrashHandling)
.addBlocking("eat-db", () -> DatabaseFactory.getInstance(this)) .addBlocking("rx-init", this::initializeRx)
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
.addBlocking("app-dependencies", this::initializeAppDependencies) .addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("notification-channels", () -> NotificationChannels.create(this)) .addBlocking("notification-channels", () -> NotificationChannels.create(this))
.addBlocking("first-launch", this::initializeFirstEverAppLaunch) .addBlocking("first-launch", this::initializeFirstEverAppLaunch)
@@ -145,9 +169,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}) })
.addBlocking("blob-provider", this::initializeBlobProvider) .addBlocking("blob-provider", this::initializeBlobProvider)
.addBlocking("feature-flags", FeatureFlags::init) .addBlocking("feature-flags", FeatureFlags::init)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager) .addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager) .addNonBlocking(this::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeGcmCheck) .addNonBlocking(this::initializeFcmCheck)
.addNonBlocking(this::initializeSignedPreKeyCheck) .addNonBlocking(this::initializeSignedPreKeyCheck)
.addNonBlocking(this::initializePeriodicTasks) .addNonBlocking(this::initializePeriodicTasks)
.addNonBlocking(this::initializeCircumvention) .addNonBlocking(this::initializeCircumvention)
@@ -158,14 +184,18 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync) .addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop()) .addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
.addNonBlocking(EmojiSource::refresh) .addNonBlocking(EmojiSource::refresh)
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(this::ensureProfileUploaded)
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this)) .addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager) .addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this))) .addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this)) .addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary) .addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.execute(); .execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
SignalLocalMetrics.ColdStart.onApplicationCreateFinished();
Tracer.getInstance().end("Application#onCreate()"); Tracer.getInstance().end("Application#onCreate()");
} }
@@ -174,14 +204,15 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
Log.i(TAG, "App is now visible."); Log.i(TAG, "App is now visible.");
ApplicationDependencies.getFrameRateTracker().begin(); ApplicationDependencies.getFrameRateTracker().start();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded(); ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
ApplicationDependencies.getDeadlockDetector().start();
SubscriptionKeepAliveJob.launchSubscriberIdKeepAliveJobIfNecessary();
SignalExecutors.BOUNDED.execute(() -> { SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary(); FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp(); ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this); RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
executePendingContactSync(); executePendingContactSync();
KeyCachingService.onAppForegrounded(this); KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getShakeToReport().enable(); ApplicationDependencies.getShakeToReport().enable();
@@ -196,8 +227,9 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
Log.i(TAG, "App is no longer visible."); Log.i(TAG, "App is no longer visible.");
KeyCachingService.onAppBackgrounded(this); KeyCachingService.onAppBackgrounded(this);
ApplicationDependencies.getMessageNotifier().clearVisibleThread(); ApplicationDependencies.getMessageNotifier().clearVisibleThread();
ApplicationDependencies.getFrameRateTracker().end(); ApplicationDependencies.getFrameRateTracker().stop();
ApplicationDependencies.getShakeToReport().disable(); ApplicationDependencies.getShakeToReport().disable();
ApplicationDependencies.getDeadlockDetector().stop();
} }
public PersistentLogger getPersistentLogger() { public PersistentLogger getPersistentLogger() {
@@ -236,10 +268,15 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
} }
private void initializeLogging() { private void initializeLogging() {
persistentLogger = new PersistentLogger(this, LogSecretProvider.getOrCreateAttachmentSecret(this), BuildConfig.VERSION_NAME); persistentLogger = new PersistentLogger(this);
org.signal.core.util.logging.Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), persistentLogger); org.signal.core.util.logging.Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), persistentLogger);
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger()); SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
SignalExecutors.UNBOUNDED.execute(() -> {
Log.blockUntilAllWritesFinished();
LogDatabase.getInstance(this).trimToSize();
});
} }
private void initializeCrashHandling() { private void initializeCrashHandling() {
@@ -247,6 +284,30 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
Thread.setDefaultUncaughtExceptionHandler(new SignalUncaughtExceptionHandler(originalHandler)); Thread.setDefaultUncaughtExceptionHandler(new SignalUncaughtExceptionHandler(originalHandler));
} }
private void initializeRx() {
RxDogTag.install();
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
RxJavaPlugins.setErrorHandler(e -> {
boolean wasWrapped = false;
while ((e instanceof UndeliverableException || e instanceof AssertionError || e instanceof OnErrorNotImplementedException) && e.getCause() != null) {
wasWrapped = true;
e = e.getCause();
}
if (wasWrapped && (e instanceof SocketException || e instanceof SocketTimeoutException || e instanceof InterruptedException)) {
return;
}
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = Thread.currentThread().getUncaughtExceptionHandler();
if (uncaughtExceptionHandler == null) {
uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
}
uncaughtExceptionHandler.uncaughtException(Thread.currentThread(), e);
});
}
private void initializeApplicationMigrations() { private void initializeApplicationMigrations() {
ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager()); ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager());
} }
@@ -261,7 +322,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void initializeFirstEverAppLaunch() { private void initializeFirstEverAppLaunch() {
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) { if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
if (!SQLCipherOpenHelper.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) { if (!SignalDatabase.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
Log.i(TAG, "First ever app launch!"); Log.i(TAG, "First ever app launch!");
AppInitialization.onFirstEverAppLaunch(this); AppInitialization.onFirstEverAppLaunch(this);
} }
@@ -277,11 +338,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
} }
} }
private void initializeGcmCheck() { private void initializeFcmCheck() {
if (TextSecurePreferences.isPushRegistered(this)) { if (SignalStore.account().isRegistered()) {
long nextSetTime = TextSecurePreferences.getFcmTokenLastSetTime(this) + TimeUnit.HOURS.toMillis(6); long nextSetTime = SignalStore.account().getFcmTokenLastSetTime() + TimeUnit.HOURS.toMillis(6);
if (TextSecurePreferences.getFcmToken(this) == null || nextSetTime <= System.currentTimeMillis()) { if (SignalStore.account().getFcmToken() == null || nextSetTime <= System.currentTimeMillis()) {
ApplicationDependencies.getJobManager().add(new FcmRefreshJob()); ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
} }
} }
@@ -319,14 +380,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void initializeRingRtc() { private void initializeRingRtc() {
try { try {
if (RtcDeviceLists.hardwareAECBlocked()) {
WebRtcAudioUtils.setWebRtcBasedAcousticEchoCanceler(true);
}
if (!RtcDeviceLists.openSLESAllowed()) {
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
}
CallManager.initialize(this, new RingRtcLogger()); CallManager.initialize(this, new RingRtcLogger());
} catch (UnsatisfiedLinkError e) { } catch (UnsatisfiedLinkError e) {
throw new AssertionError("Unable to load ringrtc library", e); throw new AssertionError("Unable to load ringrtc library", e);
@@ -335,7 +388,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
@WorkerThread @WorkerThread
private void initializeCircumvention() { private void initializeCircumvention() {
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) { if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) {
try { try {
ProviderInstaller.installIfNeeded(ApplicationContext.this); ProviderInstaller.installIfNeeded(ApplicationContext.this);
} catch (Throwable t) { } catch (Throwable t) {
@@ -344,6 +397,13 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
} }
} }
private void ensureProfileUploaded() {
if (SignalStore.account().isRegistered() && !SignalStore.registrationValues().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
Log.w(TAG, "User has a profile, but has not uploaded one. Uploading now.");
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
}
}
private void executePendingContactSync() { private void executePendingContactSync() {
if (TextSecurePreferences.needsFullContactSync(this)) { if (TextSecurePreferences.needsFullContactSync(this)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true)); ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
@@ -367,9 +427,14 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
BlobProvider.getInstance().initialize(this); BlobProvider.getInstance().initialize(this);
} }
@WorkerThread
private void cleanAvatarStorage() {
AvatarPickerStorage.cleanOrphans(this);
}
@WorkerThread @WorkerThread
private void initializeCleanup() { private void initializeCleanup() {
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments(); int deleted = SignalDatabase.attachments().deleteAbandonedPreuploadedAttachments();
Log.i(TAG, "Deleted " + deleted + " abandoned attachments."); Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
} }
@@ -27,6 +27,7 @@ import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.request.transition.Transition;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
@@ -71,12 +72,14 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set)); getWindow().setSharedElementReturnTransition(inflater.inflateTransition(R.transition.full_screen_avatar_image_return_transition_set));
} }
Toolbar toolbar = findViewById(R.id.toolbar); Toolbar toolbar = findViewById(R.id.toolbar);
ImageView avatar = findViewById(R.id.avatar); EmojiTextView title = findViewById(R.id.title);
ImageView avatar = findViewById(R.id.avatar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
requireSupportActionBar().setDisplayHomeAsUpEnabled(true); requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
requireSupportActionBar().setDisplayShowTitleEnabled(false);
Context context = getApplicationContext(); Context context = getApplicationContext();
RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA)); RecipientId recipientId = RecipientId.from(getIntent().getStringExtra(RECIPIENT_ID_EXTRA));
@@ -122,7 +125,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
} }
}); });
toolbar.setTitle(recipient.getDisplayName(context)); title.setText(recipient.getDisplayName(context));
}); });
FullscreenHelper fullscreenHelper = new FullscreenHelper(this); FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
@@ -86,7 +86,8 @@ public abstract class BaseActivity extends AppCompatActivity {
int appCompatNightMode = getDelegate().getLocalNightMode() != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED ? getDelegate().getLocalNightMode() int appCompatNightMode = getDelegate().getLocalNightMode() != AppCompatDelegate.MODE_NIGHT_UNSPECIFIED ? getDelegate().getLocalNightMode()
: AppCompatDelegate.getDefaultNightMode(); : AppCompatDelegate.getDefaultNightMode();
configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode); configuration.uiMode = (configuration.uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | mapNightModeToConfigurationUiMode(newBase, appCompatNightMode);
configuration.orientation = Configuration.ORIENTATION_UNDEFINED;
applyOverrideConfiguration(configuration); applyOverrideConfiguration(configuration);
} }
@@ -13,6 +13,8 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationMessage; import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.conversation.colors.Colorizable; import org.thoughtcrime.securesms.conversation.colors.Colorizable;
import org.thoughtcrime.securesms.conversation.colors.Colorizer; import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselectable;
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord; import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
@@ -24,31 +26,29 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable { public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable, Multiselectable {
void bind(@NonNull LifecycleOwner lifecycleOwner, void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage messageRecord, @NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord, @NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests, @NonNull GlideRequests glideRequests,
@NonNull Locale locale, @NonNull Locale locale,
@NonNull Set<ConversationMessage> batchSelected, @NonNull Set<MultiselectPart> batchSelected,
@NonNull Recipient recipients, @NonNull Recipient recipients,
@Nullable String searchQuery, @Nullable String searchQuery,
boolean pulseMention, boolean pulseMention,
boolean hasWallpaper, boolean hasWallpaper,
boolean isMessageRequestAccepted, boolean isMessageRequestAccepted,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean canPlayInline, boolean canPlayInline,
@NonNull Colorizer colorizer); @NonNull Colorizer colorizer);
ConversationMessage getConversationMessage(); @NonNull ConversationMessage getConversationMessage();
void setEventListener(@Nullable EventListener listener); void setEventListener(@Nullable EventListener listener);
@@ -56,6 +56,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
// Intentionally Blank. // Intentionally Blank.
} }
default void updateContactNameColor() {
// Intentionally Blank.
}
interface EventListener { interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord); void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview); void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
@@ -66,15 +70,17 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onAddToContactsClicked(@NonNull Contact contact); void onAddToContactsClicked(@NonNull Contact contact);
void onMessageSharedContactClicked(@NonNull List<Recipient> choices); void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
void onInviteSharedContactClicked(@NonNull List<Recipient> choices); void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms); void onReactionClicked(@NonNull MultiselectPart multiselectPart, long messageId, boolean isMms);
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId); void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord); void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord); void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
void onIncomingIdentityMismatchClicked(@NonNull RecipientId recipientId);
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver); void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver); void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onVoiceNotePause(@NonNull Uri uri); void onVoiceNotePause(@NonNull Uri uri);
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position); void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
void onVoiceNoteSeekTo(@NonNull Uri uri, double position); void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange); void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
void onChatSessionRefreshLearnMoreClicked(); void onChatSessionRefreshLearnMoreClicked();
void onBadDecryptLearnMoreClicked(@NonNull RecipientId author); void onBadDecryptLearnMoreClicked(@NonNull RecipientId author);
@@ -85,6 +91,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onPlayInlineContent(ConversationMessage conversationMessage); void onPlayInlineContent(ConversationMessage conversationMessage);
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord); void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted); void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
/** @return true if handled, false if you want to let the normal url handling continue */ /** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url); boolean onUrlClicked(@NonNull String url);
@@ -2,6 +2,8 @@ package org.thoughtcrime.securesms;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.mms.GlideRequests;
@@ -13,8 +15,8 @@ public interface BindableConversationListItem extends Unbindable {
void bind(@NonNull ThreadRecord thread, void bind(@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests, @NonNull Locale locale, @NonNull GlideRequests glideRequests, @NonNull Locale locale,
@NonNull Set<Long> typingThreads, @NonNull Set<Long> typingThreads,
@NonNull Set<Long> selectedThreads, boolean batchMode); @NonNull ConversationSet selectedConversations);
void setBatchMode(boolean batchMode); void setSelectedConversations(@NonNull ConversationSet conversations);
void updateTypingIndicator(@NonNull Set<Long> typingThreads); void updateTypingIndicator(@NonNull Set<Long> typingThreads);
} }
@@ -11,7 +11,7 @@ import androidx.lifecycle.Lifecycle;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@@ -65,7 +65,7 @@ public final class BlockUnblockDialog {
Resources resources = context.getResources(); Resources resources = context.getResources();
if (recipient.isGroup()) { if (recipient.isGroup()) {
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) { if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context))); builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates); builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run())); builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
@@ -104,7 +104,7 @@ public final class BlockUnblockDialog {
Resources resources = context.getResources(); Resources resources = context.getResources();
if (recipient.isGroup()) { if (recipient.isGroup()) {
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) { if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context))); builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you); builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run())); builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
@@ -1,177 +0,0 @@
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
import org.thoughtcrime.securesms.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MessageDatabase;
import org.thoughtcrime.securesms.database.PushDatabase;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.PushDecryptMessageJob;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.VerifySpan;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
import java.io.IOException;
public class ConfirmIdentityDialog extends AlertDialog {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(ConfirmIdentityDialog.class);
private OnClickListener callback;
public ConfirmIdentityDialog(Context context,
MessageRecord messageRecord,
IdentityKeyMismatch mismatch)
{
super(context);
Recipient recipient = Recipient.resolved(mismatch.getRecipientId(context));
String name = recipient.getDisplayName(context);
String introduction = context.getString(R.string.ConfirmIdentityDialog_your_safety_number_with_s_has_changed, name, name);
SpannableString spannableString = new SpannableString(introduction + " " +
context.getString(R.string.ConfirmIdentityDialog_you_may_wish_to_verify_your_safety_number_with_this_contact));
spannableString.setSpan(new VerifySpan(context, mismatch),
introduction.length()+1, spannableString.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
setTitle(name);
setMessage(spannableString);
setButton(AlertDialog.BUTTON_POSITIVE, context.getString(R.string.ConfirmIdentityDialog_accept), new AcceptListener(messageRecord, mismatch, recipient.getId()));
setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(android.R.string.cancel), new CancelListener());
}
@Override
public void show() {
super.show();
((TextView)this.findViewById(android.R.id.message))
.setMovementMethod(LinkMovementMethod.getInstance());
}
public void setCallback(OnClickListener callback) {
this.callback = callback;
}
private class AcceptListener implements OnClickListener {
private final MessageRecord messageRecord;
private final IdentityKeyMismatch mismatch;
private final RecipientId recipientId;
private AcceptListener(MessageRecord messageRecord, IdentityKeyMismatch mismatch, RecipientId recipientId) {
this.messageRecord = messageRecord;
this.mismatch = mismatch;
this.recipientId = recipientId;
}
@SuppressLint("StaticFieldLeak")
@Override
public void onClick(DialogInterface dialog, int which) {
new AsyncTask<Void, Void, Void>()
{
@Override
protected Void doInBackground(Void... params) {
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
SignalProtocolAddress mismatchAddress = new SignalProtocolAddress(Recipient.resolved(recipientId).requireServiceId(), 1);
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(getContext());
identityKeyStore.saveIdentity(mismatchAddress, mismatch.getIdentityKey(), true);
}
processMessageRecord(messageRecord);
return null;
}
private void processMessageRecord(MessageRecord messageRecord) {
if (messageRecord.isOutgoing()) processOutgoingMessageRecord(messageRecord);
else processIncomingMessageRecord(messageRecord);
}
private void processOutgoingMessageRecord(MessageRecord messageRecord) {
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
MessageDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(getContext());
if (messageRecord.isMms()) {
mmsDatabase.removeMismatchedIdentity(messageRecord.getId(),
mismatch.getRecipientId(getContext()),
mismatch.getIdentityKey());
if (messageRecord.getRecipient().isPushGroup()) {
MessageSender.resendGroupMessage(getContext(), messageRecord, Recipient.resolved(mismatch.getRecipientId(getContext())).getId());
} else {
MessageSender.resend(getContext(), messageRecord);
}
} else {
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
mismatch.getRecipientId(getContext()),
mismatch.getIdentityKey());
MessageSender.resend(getContext(), messageRecord);
}
}
private void processIncomingMessageRecord(MessageRecord messageRecord) {
try {
MessageDatabase smsDatabase = DatabaseFactory.getSmsDatabase(getContext());
smsDatabase.removeMismatchedIdentity(messageRecord.getId(),
mismatch.getRecipientId(getContext()),
mismatch.getIdentityKey());
boolean legacy = !messageRecord.isContentBundleKeyExchange();
SignalServiceEnvelope envelope = new SignalServiceEnvelope(SignalServiceProtos.Envelope.Type.PREKEY_BUNDLE_VALUE,
Optional.of(RecipientUtil.toSignalServiceAddress(getContext(), messageRecord.getIndividualRecipient())),
messageRecord.getRecipientDeviceId(),
messageRecord.getDateSent(),
legacy ? Base64.decode(messageRecord.getBody()) : null,
!legacy ? Base64.decode(messageRecord.getBody()) : null,
0,
0,
null);
ApplicationDependencies.getJobManager().add(new PushDecryptMessageJob(getContext(), envelope, messageRecord.getId()));
} catch (IOException e) {
throw new AssertionError(e);
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
if (callback != null) callback.onClick(null, 0);
}
}
private class CancelListener implements OnClickListener {
@Override
public void onClick(DialogInterface dialog, int which) {
if (callback != null) callback.onClick(null, 0);
}
}
}
@@ -20,13 +20,13 @@ import android.content.Context;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import androidx.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterToolbar; import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -36,6 +36,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.function.Consumer;
/** /**
* Base activity container for selecting a list of contacts. * Base activity container for selecting a list of contacts.
@@ -56,7 +57,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
protected ContactSelectionListFragment contactsFragment; protected ContactSelectionListFragment contactsFragment;
private ContactFilterToolbar toolbar; private Toolbar toolbar;
private ContactFilterView contactFilterView;
@Override @Override
protected void onPreCreate() { protected void onPreCreate() {
@@ -73,6 +75,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
setContentView(getIntent().getIntExtra(EXTRA_LAYOUT_RES_ID, R.layout.contact_selection_activity)); setContentView(getIntent().getIntExtra(EXTRA_LAYOUT_RES_ID, R.layout.contact_selection_activity));
initializeContactFilterView();
initializeToolbar(); initializeToolbar();
initializeResources(); initializeResources();
initializeSearch(); initializeSearch();
@@ -84,16 +87,23 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
dynamicTheme.onResume(this); dynamicTheme.onResume(this);
} }
protected ContactFilterToolbar getToolbar() { protected Toolbar getToolbar() {
return toolbar; return toolbar;
} }
protected ContactFilterView getContactFilterView() {
return contactFilterView;
}
private void initializeContactFilterView() {
this.contactFilterView = findViewById(R.id.contact_filter_edit_text);
}
private void initializeToolbar() { private void initializeToolbar() {
this.toolbar = findViewById(R.id.toolbar); this.toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(false); getSupportActionBar().setDisplayHomeAsUpEnabled(false);
getSupportActionBar().setDisplayShowTitleEnabled(false);
getSupportActionBar().setIcon(null); getSupportActionBar().setIcon(null);
getSupportActionBar().setLogo(null); getSupportActionBar().setLogo(null);
} }
@@ -104,7 +114,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
} }
private void initializeSearch() { private void initializeSearch() {
toolbar.setOnFilterChangedListener(filter -> contactsFragment.setQueryFilter(filter)); contactFilterView.setOnFilterChangedListener(filter -> contactsFragment.setQueryFilter(filter));
} }
@Override @Override
@@ -113,8 +123,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
} }
@Override @Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) { public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
return true; callback.accept(true);
} }
@Override @Override
@@ -155,7 +165,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
ContactSelectionActivity activity = this.activity.get(); ContactSelectionActivity activity = this.activity.get();
if (activity != null && !activity.isFinishing()) { if (activity != null && !activity.isFinishing()) {
activity.toolbar.clear(); activity.contactFilterView.clear();
activity.contactsFragment.resetQueryFilter(); activity.contactsFragment.resetQueryFilter();
} }
} }
@@ -37,6 +37,7 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet; import androidx.constraintlayout.widget.ConstraintSet;
@@ -53,17 +54,19 @@ import androidx.transition.TransitionManager;
import com.annimon.stream.Collectors; import com.annimon.stream.Collectors;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.google.android.material.chip.ChipGroup; import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.pnikosis.materialishprogress.ProgressWheel; import com.pnikosis.materialishprogress.ProgressWheel;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller; import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.components.emoji.WarningTextView; import org.thoughtcrime.securesms.components.recyclerview.ToolbarShadowAnimationHelper;
import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader; import org.thoughtcrime.securesms.contacts.AbstractContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactChip; import org.thoughtcrime.securesms.contacts.ContactChip;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter; import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem; import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
import org.thoughtcrime.securesms.contacts.SelectedContact; import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.groups.SelectionLimits; import org.thoughtcrime.securesms.groups.SelectionLimits;
@@ -74,7 +77,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
@@ -88,6 +90,7 @@ import java.io.IOException;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.function.Consumer;
/** /**
* Fragment for selecting a one or more contacts from a list. * Fragment for selecting a one or more contacts from a list.
@@ -131,9 +134,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
private ContactSelectionListAdapter cursorRecyclerViewAdapter; private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup; private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer; private HorizontalScrollView chipGroupScrollContainer;
private WarningTextView groupLimit;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener; private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider; private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
private View shadowView;
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper;
@Nullable private FixedViewsAdapter headerAdapter; @Nullable private FixedViewsAdapter headerAdapter;
@@ -175,12 +179,16 @@ public final class ContactSelectionListFragment extends LoggingFragment
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) context; onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) context;
} }
if (getParentFragment() instanceof OnSelectionLimitReachedListener) {
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) getParentFragment();
}
if (context instanceof AbstractContactsCursorLoaderFactoryProvider) { if (context instanceof AbstractContactsCursorLoaderFactoryProvider) {
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context; cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context;
} }
if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) { if (getParentFragment() instanceof AbstractContactsCursorLoaderFactoryProvider) {
cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) context; cursorFactoryProvider = (AbstractContactsCursorLoaderFactoryProvider) getParentFragment();
} }
} }
@@ -233,9 +241,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
showContactsProgress = view.findViewById(R.id.progress); showContactsProgress = view.findViewById(R.id.progress);
chipGroup = view.findViewById(R.id.chipGroup); chipGroup = view.findViewById(R.id.chipGroup);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer); chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
groupLimit = view.findViewById(R.id.group_limit);
constraintLayout = view.findViewById(R.id.container); constraintLayout = view.findViewById(R.id.container);
shadowView = view.findViewById(R.id.toolbar_shadow);
toolbarShadowAnimationHelper = new ToolbarShadowAnimationHelper(shadowView);
recyclerView.addOnScrollListener(toolbarShadowAnimationHelper);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setItemAnimator(new DefaultItemAnimator() { recyclerView.setItemAnimator(new DefaultItemAnimator() {
@Override @Override
@@ -256,7 +267,9 @@ public final class ContactSelectionListFragment extends LoggingFragment
recyclerView.setClipToPadding(recyclerViewClipping); recyclerView.setClipToPadding(recyclerViewClipping);
swipeRefresh.setEnabled(arguments.getBoolean(REFRESHABLE, intent.getBooleanExtra(REFRESHABLE, true))); boolean isRefreshable = arguments.getBoolean(REFRESHABLE, intent.getBooleanExtra(REFRESHABLE, true));
swipeRefresh.setNestedScrollingEnabled(isRefreshable);
swipeRefresh.setEnabled(isRefreshable);
hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false)); hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false));
selectionLimit = arguments.getParcelable(SELECTION_LIMITS); selectionLimit = arguments.getParcelable(SELECTION_LIMITS);
@@ -272,8 +285,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
currentSelection = getCurrentSelection(); currentSelection = getCurrentSelection();
updateGroupLimit(getChipCount());
return view; return view;
} }
@@ -281,13 +292,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
return getArguments() != null ? getArguments() : new Bundle(); return getArguments() != null ? getArguments() : new Bundle();
} }
private void updateGroupLimit(int chipCount) {
int members = currentSelection.size() + chipCount;
groupLimit.setText(getResources().getQuantityString(R.plurals.ContactSelectionListFragment_d_members, members, members));
groupLimit.setVisibility(isMulti && !hideCount ? View.VISIBLE : View.GONE);
groupLimit.setWarning(selectionWarningLimitExceeded());
}
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@@ -309,6 +313,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
return cursorRecyclerViewAdapter.getSelectedContactsCount(); return cursorRecyclerViewAdapter.getSelectedContactsCount();
} }
public int getTotalMemberCount() {
if (cursorRecyclerViewAdapter == null) {
return 0;
}
return cursorRecyclerViewAdapter.getSelectedContactsCount() + cursorRecyclerViewAdapter.getCurrentContactsCount();
}
private Set<RecipientId> getCurrentSelection() { private Set<RecipientId> getCurrentSelection() {
List<RecipientId> currentSelection = safeArguments().getParcelableArrayList(CURRENT_SELECTION); List<RecipientId> currentSelection = safeArguments().getParcelableArrayList(CURRENT_SELECTION);
if (currentSelection == null) { if (currentSelection == null) {
@@ -349,8 +361,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
concatenateAdapter.addAdapter(footerAdapter); concatenateAdapter.addAdapter(footerAdapter);
} }
recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
recyclerView.setAdapter(concatenateAdapter); recyclerView.setAdapter(concatenateAdapter);
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true, 0));
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override @Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
@@ -361,6 +373,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
} }
}); });
if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged();
}
}
private boolean hideLetterHeaders() {
return hasQueryFilter() || shouldDisplayRecents();
} }
private View createInviteActionView(@NonNull ListCallback listCallback) { private View createInviteActionView(@NonNull ListCallback listCallback) {
@@ -425,11 +445,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
} }
public void setRecyclerViewPaddingBottom(@Px int paddingBottom) {
ViewUtil.setPaddingBottom(recyclerView, paddingBottom);
}
@Override @Override
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) { public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
FragmentActivity activity = requireActivity(); FragmentActivity activity = requireActivity();
int displayMode = safeArguments().getInt(DISPLAY_MODE, activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL)); int displayMode = safeArguments().getInt(DISPLAY_MODE, activity.getIntent().getIntExtra(DISPLAY_MODE, DisplayMode.FLAG_ALL));
boolean displayRecents = safeArguments().getBoolean(RECENTS, activity.getIntent().getBooleanExtra(RECENTS, false)); boolean displayRecents = shouldDisplayRecents();
if (cursorFactoryProvider != null) { if (cursorFactoryProvider != null) {
return cursorFactoryProvider.get().create(); return cursorFactoryProvider.get().create();
@@ -475,6 +499,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
fastScroller.setVisibility(View.GONE); fastScroller.setVisibility(View.GONE);
} }
private boolean shouldDisplayRecents() {
return safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
}
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private void handleContactPermissionGranted() { private void handleContactPermissionGranted() {
final Context context = requireContext(); final Context context = requireContext();
@@ -543,7 +571,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext()); AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> { SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
return UsernameUtil.fetchUuidForUsername(requireContext(), contact.getNumber()); return UsernameUtil.fetchAciForUsername(requireContext(), contact.getNumber());
}, uuid -> { }, uuid -> {
loadingDialog.dismiss(); loadingDialog.dismiss();
if (uuid.isPresent()) { if (uuid.isPresent()) {
@@ -551,28 +579,32 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber()); SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
if (onContactSelectedListener != null) { if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) { onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
markContactSelected(selected); if (allowed) {
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE); markContactSelected(selected);
} cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
});
} else { } else {
markContactSelected(selected); markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE); cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
} }
} else { } else {
new AlertDialog.Builder(requireContext()) new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.ContactSelectionListFragment_username_not_found) .setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber())) .setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss()) .setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show(); .show();
} }
}); });
} else { } else {
if (onContactSelectedListener != null) { if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) { onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber(), allowed -> {
markContactSelected(selectedContact); if (allowed) {
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE); markContactSelected(selectedContact);
} cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
});
} else { } else {
markContactSelected(selectedContact); markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE); cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
@@ -606,12 +638,19 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (isMulti) { if (isMulti) {
addChipForSelectedContact(selectedContact); addChipForSelectedContact(selectedContact);
} }
if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged();
}
} }
private void markContactUnselected(@NonNull SelectedContact selectedContact) { private void markContactUnselected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact); cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE); cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
removeChipForContact(selectedContact); removeChipForContact(selectedContact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged();
}
} }
private void removeChipForContact(@NonNull SelectedContact contact) { private void removeChipForContact(@NonNull SelectedContact contact) {
@@ -622,8 +661,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
} }
updateGroupLimit(getChipCount());
if (getChipCount() == 0) { if (getChipCount() == 0) {
setChipGroupVisibility(ConstraintSet.GONE); setChipGroupVisibility(ConstraintSet.GONE);
} }
@@ -660,6 +697,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
@Override @Override
public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) { public void endTransition(LayoutTransition transition, ViewGroup container, View view, int transitionType) {
if (getView() == null || !requireView().isAttachedToWindow()) {
Log.w(TAG, "Fragment's view was detached before the animation completed.");
return;
}
if (view == chip && transitionType == LayoutTransition.APPEARING) { if (view == chip && transitionType == LayoutTransition.APPEARING) {
chipGroup.getLayoutTransition().removeTransitionListener(this); chipGroup.getLayoutTransition().removeTransitionListener(this);
registerChipRecipientObserver(chip, recipient.live()); registerChipRecipientObserver(chip, recipient.live());
@@ -673,7 +715,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void addChip(@NonNull ContactChip chip) { private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip); chipGroup.addView(chip);
updateGroupLimit(getChipCount());
if (selectionWarningLimitReachedExactly()) { if (selectionWarningLimitReachedExactly()) {
if (onSelectionLimitReachedListener != null) { if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit()); onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
@@ -723,9 +764,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
} }
public interface OnContactSelectedListener { public interface OnContactSelectedListener {
/** @return True if the contact is allowed to be selected, otherwise false. */ /** Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it. */
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number); void onBeforeContactSelected(Optional<RecipientId> recipientId, @Nullable String number, Consumer<Boolean> callback);
void onContactDeselected(Optional<RecipientId> recipientId, String number); void onContactDeselected(Optional<RecipientId> recipientId, @Nullable String number);
void onSelectionChanged();
} }
public interface OnSelectionLimitReachedListener { public interface OnSelectionLimitReachedListener {
@@ -120,7 +120,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
} }
@Override @Override
public void onQrDataFound(final String data) { public void onQrDataFound(@NonNull final String data) {
ThreadUtil.runOnMain(() -> { ThreadUtil.runOnMain(() -> {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50); ((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
Uri uri = Uri.parse(data); Uri uri = Uri.parse(data);
@@ -150,6 +150,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
}); });
} }
@SuppressLint("MissingSuperCall")
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@@ -190,7 +191,6 @@ public class DeviceActivity extends PassphraseRequiredActivity
Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext())); Optional<byte[]> profileKey = Optional.of(ProfileKeyUtil.getProfileKey(getContext()));
TextSecurePreferences.setMultiDevice(DeviceActivity.this, true); TextSecurePreferences.setMultiDevice(DeviceActivity.this, true);
TextSecurePreferences.setIsUnidentifiedDeliveryEnabled(context, false);
accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode); accountManager.addDevice(ephemeralId, publicKey, identityKeyPair, profileKey, verificationCode);
return SUCCESS; return SUCCESS;
@@ -35,6 +35,7 @@ public final class GroupMembersDialog {
.show(); .show();
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members); GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
memberListView.initializeAdapter(fragmentActivity);
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId()); LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
LiveData<List<GroupMemberEntry.FullMember>> fullMembers = liveGroup.getFullMembers(); LiveData<List<GroupMemberEntry.FullMember>> fullMembers = liveGroup.getFullMembers();
@@ -3,12 +3,8 @@ package org.thoughtcrime.securesms;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.graphics.PorterDuff;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -16,19 +12,19 @@ import android.view.animation.Animation;
import android.view.animation.AnimationUtils; import android.view.animation.AnimationUtils;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.AnimRes; import androidx.annotation.AnimRes;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar; import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import org.thoughtcrime.securesms.components.ContactFilterToolbar; import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener; import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode; import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact; import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.SelectionLimits; import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -36,27 +32,25 @@ import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask; import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged; import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener { public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
private ContactSelectionListFragment contactsFragment; private ContactSelectionListFragment contactsFragment;
private EditText inviteText; private EditText inviteText;
private ViewGroup smsSendFrame; private ViewGroup smsSendFrame;
private Button smsSendButton; private Button smsSendButton;
private Animation slideInAnimation; private Animation slideInAnimation;
private Animation slideOutAnimation; private Animation slideOutAnimation;
private DynamicTheme dynamicTheme = new DynamicNoActionBarInviteTheme(); private final DynamicTheme dynamicTheme = new DynamicNoActionBarInviteTheme();
private Toolbar primaryToolbar;
@Override @Override
protected void onPreCreate() { protected void onPreCreate() {
@@ -84,7 +78,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
} }
private void initializeAppBar() { private void initializeAppBar() {
primaryToolbar = findViewById(R.id.toolbar); final Toolbar primaryToolbar = findViewById(R.id.toolbar);
setSupportActionBar(primaryToolbar); setSupportActionBar(primaryToolbar);
assert getSupportActionBar() != null; assert getSupportActionBar() != null;
@@ -98,9 +92,10 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
slideOutAnimation = loadAnimation(R.anim.slide_to_bottom); slideOutAnimation = loadAnimation(R.anim.slide_to_bottom);
View shareButton = findViewById(R.id.share_button); View shareButton = findViewById(R.id.share_button);
Button smsButton = findViewById(R.id.sms_button); TextView shareText = findViewById(R.id.share_text);
View smsButton = findViewById(R.id.sms_button);
Button smsCancelButton = findViewById(R.id.cancel_sms_button); Button smsCancelButton = findViewById(R.id.cancel_sms_button);
ContactFilterToolbar contactFilter = findViewById(R.id.contact_filter); ContactFilterView contactFilter = findViewById(R.id.contact_filter_edit_text);
inviteText = findViewById(R.id.invite_text); inviteText = findViewById(R.id.invite_text);
smsSendFrame = findViewById(R.id.sms_send_frame); smsSendFrame = findViewById(R.id.sms_send_frame);
@@ -121,15 +116,14 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
smsCancelButton.setOnClickListener(new SmsCancelClickListener()); smsCancelButton.setOnClickListener(new SmsCancelClickListener());
smsSendButton.setOnClickListener(new SmsSendClickListener()); smsSendButton.setOnClickListener(new SmsSendClickListener());
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener()); contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
contactFilter.setNavigationIcon(R.drawable.ic_search_conversation_24);
if (Util.isDefaultSmsProvider(this)) { if (Util.isDefaultSmsProvider(this)) {
shareButton.setOnClickListener(new ShareClickListener()); shareButton.setOnClickListener(new ShareClickListener());
smsButton.setOnClickListener(new SmsClickListener()); smsButton.setOnClickListener(new SmsClickListener());
} else { } else {
shareButton.setVisibility(View.GONE); smsButton.setVisibility(View.GONE);
smsButton.setOnClickListener(new ShareClickListener()); shareText.setText(R.string.InviteActivity_share);
smsButton.setText(R.string.InviteActivity_share); shareButton.setOnClickListener(new ShareClickListener());
} }
} }
@@ -140,9 +134,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
} }
@Override @Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) { public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1); updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
return true; callback.accept(true);
} }
@Override @Override
@@ -150,6 +144,10 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
updateSmsButtonText(contactsFragment.getSelectedContacts().size()); updateSmsButtonText(contactsFragment.getSelectedContacts().size());
} }
@Override
public void onSelectionChanged() {
}
private void sendSmsInvites() { private void sendSmsInvites() {
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString()) new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
@@ -158,9 +156,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
} }
private void updateSmsButtonText(int count) { private void updateSmsButtonText(int count) {
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends, smsSendButton.setText(getResources().getString(R.string.InviteActivity_send_sms, count));
count,
count));
smsSendButton.setEnabled(count > 0); smsSendButton.setEnabled(count > 0);
} }
@@ -172,43 +168,21 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
} }
} }
@Override public boolean onSupportNavigateUp() {
if (smsSendFrame.getVisibility() == View.VISIBLE) {
cancelSmsSelection();
return false;
} else {
return super.onSupportNavigateUp();
}
}
private void cancelSmsSelection() { private void cancelSmsSelection() {
setPrimaryColorsToolbarNormal();
contactsFragment.reset(); contactsFragment.reset();
updateSmsButtonText(contactsFragment.getSelectedContacts().size()); updateSmsButtonText(contactsFragment.getSelectedContacts().size());
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE); ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
} }
private void setPrimaryColorsToolbarNormal() {
primaryToolbar.setBackgroundColor(0);
primaryToolbar.getNavigationIcon().setColorFilter(null);
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_primary));
if (Build.VERSION.SDK_INT >= 23) {
WindowUtil.setStatusBarColor(getWindow(), ThemeUtil.getThemedColor(this, android.R.attr.statusBarColor));
getWindow().setNavigationBarColor(ThemeUtil.getThemedColor(this, android.R.attr.navigationBarColor));
WindowUtil.setLightStatusBarFromTheme(this);
}
WindowUtil.setLightNavigationBarFromTheme(this);
}
private void setPrimaryColorsToolbarForSms() {
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine));
primaryToolbar.getNavigationIcon().setColorFilter(ContextCompat.getColor(this, R.color.signal_text_toolbar_subtitle), PorterDuff.Mode.SRC_IN);
primaryToolbar.setTitleTextColor(ContextCompat.getColor(this, R.color.signal_text_toolbar_title));
if (Build.VERSION.SDK_INT >= 23) {
WindowUtil.setStatusBarColor(getWindow(), ContextCompat.getColor(this, R.color.core_ultramarine));
WindowUtil.clearLightStatusBar(getWindow());
}
if (Build.VERSION.SDK_INT >= 27) {
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
WindowUtil.clearLightNavigationBar(getWindow());
}
}
private class ShareClickListener implements OnClickListener { private class ShareClickListener implements OnClickListener {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
@@ -227,7 +201,6 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
private class SmsClickListener implements OnClickListener { private class SmsClickListener implements OnClickListener {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
setPrimaryColorsToolbarForSms();
ViewUtil.animateIn(smsSendFrame, slideInAnimation); ViewUtil.animateIn(smsSendFrame, slideInAnimation);
} }
} }
@@ -279,10 +252,10 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
Recipient recipient = Recipient.resolved(recipientId); Recipient recipient = Recipient.resolved(recipientId);
int subscriptionId = recipient.getDefaultSubscriptionId().or(-1); int subscriptionId = recipient.getDefaultSubscriptionId().or(-1);
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null); MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null, null);
if (recipient.getContactUri() != null) { if (recipient.getContactUri() != null) {
DatabaseFactory.getRecipientDatabase(context).setHasSentInvite(recipient.getId()); SignalDatabase.recipients().setHasSentInvite(recipient.getId());
} }
} }
@@ -9,6 +9,8 @@ import android.os.Bundle;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog; import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferLockedDialog;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.AppStartup; import org.thoughtcrime.securesms.util.AppStartup;
@@ -17,13 +19,15 @@ import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
public class MainActivity extends PassphraseRequiredActivity { public class MainActivity extends PassphraseRequiredActivity implements VoiceNoteMediaControllerOwner {
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901; public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme(); private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final MainNavigator navigator = new MainNavigator(this); private final MainNavigator navigator = new MainNavigator(this);
private VoiceNoteMediaController mediaController;
public static @NonNull Intent clearTop(@NonNull Context context) { public static @NonNull Intent clearTop(@NonNull Context context) {
Intent intent = new Intent(context, MainActivity.class); Intent intent = new Intent(context, MainActivity.class);
@@ -40,10 +44,12 @@ public class MainActivity extends PassphraseRequiredActivity {
super.onCreate(savedInstanceState, ready); super.onCreate(savedInstanceState, ready);
setContentView(R.layout.main_activity); setContentView(R.layout.main_activity);
mediaController = new VoiceNoteMediaController(this);
navigator.onCreate(savedInstanceState); navigator.onCreate(savedInstanceState);
handleGroupLinkInIntent(getIntent()); handleGroupLinkInIntent(getIntent());
handleProxyInIntent(getIntent()); handleProxyInIntent(getIntent());
handleSignalMeIntent(getIntent());
CachedInflater.from(this).clear(); CachedInflater.from(this).clear();
} }
@@ -60,6 +66,7 @@ public class MainActivity extends PassphraseRequiredActivity {
super.onNewIntent(intent); super.onNewIntent(intent);
handleGroupLinkInIntent(intent); handleGroupLinkInIntent(intent);
handleProxyInIntent(intent); handleProxyInIntent(intent);
handleSignalMeIntent(intent);
} }
@Override @Override
@@ -109,4 +116,16 @@ public class MainActivity extends PassphraseRequiredActivity {
CommunicationActions.handlePotentialProxyLinkUrl(this, data.toString()); CommunicationActions.handlePotentialProxyLinkUrl(this, data.toString());
} }
} }
private void handleSignalMeIntent(Intent intent) {
Uri data = intent.getData();
if (data != null) {
CommunicationActions.handlePotentialSignalMeUrl(this, data.toString());
}
}
@Override
public @NonNull VoiceNoteMediaController getVoiceNoteMediaController() {
return mediaController;
}
} }
@@ -73,6 +73,7 @@ import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil; import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FullscreenHelper; import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.StorageUtil;
@@ -171,6 +172,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
initializeObservers(); initializeObservers();
} }
@SuppressLint("MissingSuperCall")
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@@ -533,7 +535,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
} }
public static boolean isContentTypeSupported(final String contentType) { public static boolean isContentTypeSupported(final String contentType) {
return contentType != null && (contentType.startsWith("image/") || contentType.startsWith("video/")); return MediaUtil.isImageType(contentType) || MediaUtil.isVideoType(contentType);
} }
@Override @Override
@@ -567,26 +569,11 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (item == 0) { if (item == 0) {
viewPagerListener.onPageSelected(0); viewPagerListener.onPageSelected(0);
} }
cursor.registerContentObserver(new ContentObserver(new Handler(getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
onMediaChange();
}
});
} else { } else {
mediaNotAvailable(); mediaNotAvailable();
} }
} }
private void onMediaChange() {
MediaItemAdapter adapter = (MediaItemAdapter) mediaPager.getAdapter();
if (adapter != null) {
adapter.checkMedia(mediaPager.getCurrentItem());
}
}
@Override @Override
public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) { public void onLoaderReset(@NonNull Loader<Pair<Cursor, Integer>> loader) {
@@ -614,7 +601,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (adapter != null) { if (adapter != null) {
MediaItem item = adapter.getMediaItemFor(position); MediaItem item = adapter.getMediaItemFor(position);
if (item.recipient != null) item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar()); if (item != null && item.recipient != null) {
item.recipient.live().observe(MediaPreviewActivity.this, r -> initializeActionBar());
}
viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position); viewModel.setActiveAlbumRailItem(MediaPreviewActivity.this, position);
initializeActionBar(); initializeActionBar();
} }
@@ -627,7 +617,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (adapter != null) { if (adapter != null) {
MediaItem item = adapter.getMediaItemFor(position); MediaItem item = adapter.getMediaItemFor(position);
if (item.recipient != null) item.recipient.live().removeObservers(MediaPreviewActivity.this); if (item != null && item.recipient != null) {
item.recipient.live().removeObservers(MediaPreviewActivity.this);
}
adapter.pause(position); adapter.pause(position);
} }
@@ -677,7 +669,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
} }
@Override @Override
public MediaItem getMediaItemFor(int position) { public @Nullable MediaItem getMediaItemFor(int position) {
return new MediaItem(null, null, null, uri, mediaType, -1, true); return new MediaItem(null, null, null, uri, mediaType, -1, true);
} }
@@ -700,11 +692,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
public boolean hasFragmentFor(int position) { public boolean hasFragmentFor(int position) {
return mediaPreviewFragment != null; return mediaPreviewFragment != null;
} }
@Override
public void checkMedia(int currentItem) {
}
} }
private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) { private static void anchorMarginsToBottomInsets(@NonNull View viewToAnchor) {
@@ -788,8 +775,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
super.destroyItem(container, position, object); super.destroyItem(container, position, object);
} }
public MediaItem getMediaItemFor(int position) { public @Nullable MediaItem getMediaItemFor(int position) {
cursor.moveToPosition(getCursorPosition(position)); int cursorPosition = getCursorPosition(position);
if (cursor.isClosed() || cursorPosition < 0) {
Log.w(TAG, "Invalid cursor state! Closed: " + cursor.isClosed() + " Position: " + cursorPosition);
return null;
}
cursor.moveToPosition(cursorPosition);
MediaRecord mediaRecord = MediaRecord.from(context, cursor); MediaRecord mediaRecord = MediaRecord.from(context, cursor);
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment()); DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
@@ -823,14 +817,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
return mediaFragments.containsKey(position); return mediaFragments.containsKey(position);
} }
@Override
public void checkMedia(int position) {
MediaPreviewFragment fragment = mediaFragments.get(position);
if (fragment != null) {
fragment.checkMediaStillAvailable();
}
}
private int getCursorPosition(int position) { private int getCursorPosition(int position) {
if (leftIsRecent) return position; if (leftIsRecent) return position;
else return cursor.getCount() - 1 - position; else return cursor.getCount() - 1 - position;
@@ -865,10 +851,9 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
} }
interface MediaItemAdapter { interface MediaItemAdapter {
MediaItem getMediaItemFor(int position); @Nullable MediaItem getMediaItemFor(int position);
void pause(int position); void pause(int position);
@Nullable View getPlaybackControls(int position); @Nullable View getPlaybackControls(int position);
boolean hasFragmentFor(int position); boolean hasFragmentFor(int position);
void checkMedia(int currentItem);
} }
} }
@@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
public class MuteDialog extends AlertDialog { public class MuteDialog extends AlertDialog {
@@ -29,24 +31,21 @@ public class MuteDialog extends AlertDialog {
} }
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) { public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
AlertDialog.Builder builder = new AlertDialog.Builder(context); AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
builder.setTitle(R.string.MuteDialog_mute_notifications); builder.setTitle(R.string.MuteDialog_mute_notifications);
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() { builder.setItems(R.array.mute_durations, (dialog, which) -> {
@Override final long muteUntil;
public void onClick(DialogInterface dialog, final int which) {
final long muteUntil;
switch (which) { switch (which) {
case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break; case 0: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break; case 1: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(8); break;
case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break; case 2: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1); break;
case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break; case 3: muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7); break;
case 4: muteUntil = Long.MAX_VALUE; break; case 4: muteUntil = Long.MAX_VALUE; break;
default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break; default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1); break;
}
listener.onMuted(muteUntil);
} }
listener.onMuted(muteUntil);
}); });
if (cancelListener != null) { if (cancelListener != null) {
@@ -26,17 +26,18 @@ import androidx.appcompat.app.AlertDialog;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper; import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity; import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException; import java.io.IOException;
import java.util.function.Consumer;
/** /**
* Activity container for starting a new conversation. * Activity container for starting a new conversation.
@@ -56,16 +57,17 @@ public class NewConversationActivity extends ContactSelectionActivity
super.onCreate(bundle, ready); super.onCreate(bundle, ready);
assert getSupportActionBar() != null; assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.NewConversationActivity__new_message);
} }
@Override @Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) { public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
if (recipientId.isPresent()) { if (recipientId.isPresent()) {
launch(Recipient.resolved(recipientId.get())); launch(Recipient.resolved(recipientId.get()));
} else { } else {
Log.i(TAG, "[onContactSelected] Maybe creating a new recipient."); Log.i(TAG, "[onContactSelected] Maybe creating a new recipient.");
if (TextSecurePreferences.isPushRegistered(this) && NetworkConstraint.isMet(this)) { if (SignalStore.account().isRegistered() && NetworkConstraint.isMet(this)) {
Log.i(TAG, "[onContactSelected] Doing contact refresh."); Log.i(TAG, "[onContactSelected] Doing contact refresh.");
AlertDialog progress = SimpleProgressDialog.show(this); AlertDialog progress = SimpleProgressDialog.show(this);
@@ -73,7 +75,7 @@ public class NewConversationActivity extends ContactSelectionActivity
SimpleTask.run(getLifecycle(), () -> { SimpleTask.run(getLifecycle(), () -> {
Recipient resolved = Recipient.external(this, number); Recipient resolved = Recipient.external(this, number);
if (!resolved.isRegistered() || !resolved.hasUuid()) { if (!resolved.isRegistered() || !resolved.hasAci()) {
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh."); Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
try { try {
DirectoryHelper.refreshDirectoryFor(this, resolved, false); DirectoryHelper.refreshDirectoryFor(this, resolved, false);
@@ -93,11 +95,15 @@ public class NewConversationActivity extends ContactSelectionActivity
} }
} }
return true; callback.accept(true);
}
@Override
public void onSelectionChanged() {
} }
private void launch(Recipient recipient) { private void launch(Recipient recipient) {
long existingThread = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId()); long existingThread = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
Intent intent = ConversationIntents.createBuilder(this, recipient.getId(), existingThread) Intent intent = ConversationIntents.createBuilder(this, recipient.getId(), existingThread)
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT)) .withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
.withDataUri(getIntent().getData()) .withDataUri(getIntent().getData())
@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms
import android.graphics.Bitmap
import android.graphics.Canvas
import androidx.annotation.ColorInt
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest
/**
* BitmapTransformation which overlays the given bitmap with the given color.
*/
class OverlayTransformation(
@ColorInt private val color: Int
) : BitmapTransformation() {
private val id = "${OverlayTransformation::class.java.name}$color"
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(id.toByteArray(CHARSET))
}
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(outBitmap)
canvas.drawBitmap(toTransform, 0f, 0f, null)
canvas.drawColor(color)
return outBitmap
}
override fun equals(other: Any?): Boolean {
return (other as? OverlayTransformation)?.color == color
}
override fun hashCode(): Int {
return id.hashCode()
}
}
@@ -35,7 +35,6 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.WindowManager;
import android.view.animation.Animation; import android.view.animation.Animation;
import android.view.animation.BounceInterpolator; import android.view.animation.BounceInterpolator;
import android.view.animation.TranslateAnimation; import android.view.animation.TranslateAnimation;
@@ -51,6 +50,7 @@ import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricManager.Authenticators; import androidx.biometric.BiometricManager.Authenticators;
import androidx.biometric.BiometricPrompt; import androidx.biometric.BiometricPrompt;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener; import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.AnimatingToggle; import org.thoughtcrime.securesms.components.AnimatingToggle;
@@ -98,13 +98,16 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private boolean hadFailure; private boolean hadFailure;
private boolean alreadyShown; private boolean alreadyShown;
private final Runnable resumeScreenLockRunnable = () -> {
resumeScreenLock(!alreadyShown);
alreadyShown = true;
};
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate()"); Log.i(TAG, "onCreate()");
dynamicTheme.onCreate(this); dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this); dynamicLanguage.onCreate(this);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.prompt_passphrase_activity); setContentView(R.layout.prompt_passphrase_activity);
@@ -129,11 +132,20 @@ public class PassphrasePromptActivity extends PassphraseActivity {
setLockTypeVisibility(); setLockTypeVisibility();
if (TextSecurePreferences.isScreenLockEnabled(this) && !authenticated && !hadFailure) { if (TextSecurePreferences.isScreenLockEnabled(this) && !authenticated && !hadFailure) {
resumeScreenLock(!alreadyShown); ThreadUtil.postToMain(resumeScreenLockRunnable);
alreadyShown = true;
} }
hadFailure = false; hadFailure = false;
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN);
}
@Override
public void onPause() {
super.onPause();
ThreadUtil.cancelRunnableOnMain(resumeScreenLockRunnable);
biometricPrompt.cancelAuthentication();
} }
@Override @Override
@@ -388,9 +400,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
@Override @Override
public void onAnimationEnd(Animator animation) { public void onAnimationEnd(Animator animation) {
handleAuthenticated(); handleAuthenticated();
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
} }
}).start(); }).start();
} }
@@ -412,7 +421,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
@Override @Override
public void onAnimationEnd(Animation animation) { public void onAnimationEnd(Animation animation) {
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp); fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN); fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN);
} }
@Override @Override
@@ -15,6 +15,7 @@ import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer; import org.signal.core.util.tracing.Tracer;
import org.signal.devicetransfer.TransferStatus; import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity; import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
@@ -50,6 +51,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_CREATE_SIGNAL_PIN = 7; private static final int STATE_CREATE_SIGNAL_PIN = 7;
private static final int STATE_TRANSFER_ONGOING = 8; private static final int STATE_TRANSFER_ONGOING = 8;
private static final int STATE_TRANSFER_LOCKED = 9; private static final int STATE_TRANSFER_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private SignalServiceNetworkAccess networkAccess; private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver; private BroadcastReceiver clearKeyReceiver;
@@ -58,7 +60,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
protected final void onCreate(Bundle savedInstanceState) { protected final void onCreate(Bundle savedInstanceState) {
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()"); Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
AppStartup.getInstance().onCriticalRenderEventStart(); AppStartup.getInstance().onCriticalRenderEventStart();
this.networkAccess = new SignalServiceNetworkAccess(this); this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
onPreCreate(); onPreCreate();
final boolean locked = KeyCachingService.isLocked(this); final boolean locked = KeyCachingService.isLocked(this);
@@ -82,7 +84,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
if (networkAccess.isCensored(this)) { if (networkAccess.isCensored()) {
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob()); ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
} }
} }
@@ -153,6 +155,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent(); case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent(); case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent(); case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
default: return null; default: return null;
} }
} }
@@ -176,6 +179,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_TRANSFER_ONGOING; return STATE_TRANSFER_ONGOING;
} else if (SignalStore.misc().isOldDeviceTransferLocked()) { } else if (SignalStore.misc().isOldDeviceTransferLocked()) {
return STATE_TRANSFER_LOCKED; return STATE_TRANSFER_LOCKED;
} else if (SignalStore.misc().isChangeNumberLocked() && getClass() != ChangeNumberLockActivity.class) {
return STATE_CHANGE_NUMBER_LOCK;
} else { } else {
return STATE_NORMAL; return STATE_NORMAL;
} }
@@ -243,6 +248,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return MainActivity.clearTop(this); return MainActivity.clearTop(this);
} }
private Intent getChangeNumberLockIntent() {
return ChangeNumberLockActivity.createIntent(this);
}
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) { private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
final Intent intent = new Intent(this, destination); final Intent intent = new Intent(this, destination);
if (nextIntent != null) intent.putExtra("next_intent", nextIntent); if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
@@ -65,4 +65,8 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
setResult(RESULT_OK, resultIntent); setResult(RESULT_OK, resultIntent);
finish(); finish();
} }
@Override
public void onSelectionChanged() {
}
} }
@@ -1,48 +0,0 @@
package org.thoughtcrime.securesms;
import android.os.Build;
import java.util.HashSet;
import java.util.Set;
/**
* Device hardware capability lists.
* <p>
* Moved outside of ApplicationContext as the indirection was important for API19 support with desugaring: https://issuetracker.google.com/issues/183419297
*/
final class RtcDeviceLists {
private RtcDeviceLists() {}
static Set<String> hardwareAECBlockList() {
return new HashSet<String>() {{
add("Pixel");
add("Pixel XL");
add("Moto G5");
add("Moto G (5S) Plus");
add("Moto G4");
add("TA-1053");
add("Mi A1");
add("Mi A2");
add("E5823"); // Sony z5 compact
add("Redmi Note 5");
add("FP2"); // Fairphone FP2
add("MI 5");
}};
}
static Set<String> openSlEsAllowList() {
return new HashSet<String>() {{
add("Pixel");
add("Pixel XL");
}};
}
static boolean hardwareAECBlocked() {
return hardwareAECBlockList().contains(Build.MODEL);
}
static boolean openSLESAllowed() {
return openSlEsAllowList().contains(Build.MODEL);
}
}
@@ -12,7 +12,7 @@ import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.conversation.ConversationIntents; import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Rfc5724Uri; import org.thoughtcrime.securesms.util.Rfc5724Uri;
@@ -48,7 +48,7 @@ public class SmsSendtoActivity extends Activity {
Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.ConversationActivity_specify_recipient, Toast.LENGTH_LONG).show();
} else { } else {
Recipient recipient = Recipient.external(this, destination.getDestination()); Recipient recipient = Recipient.external(this, destination.getDestination());
long threadId = DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId()); long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipient.getId());
nextIntent = ConversationIntents.createBuilder(this, recipient.getId(), threadId) nextIntent = ConversationIntents.createBuilder(this, recipient.getId(), threadId)
.withDraftText(destination.getBody()) .withDraftText(destination.getBody())
@@ -1,693 +0,0 @@
/*
* Copyright (C) 2016-2017 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
import android.Manifest;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.PorterDuff;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Vibrator;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.crypto.DatabaseSessionLock;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.QrCode;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.qr.ScanningThread;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.fingerprint.Fingerprint;
import org.whispersystems.libsignal.fingerprint.FingerprintParsingException;
import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException;
import org.whispersystems.libsignal.fingerprint.NumericFingerprintGenerator;
import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.Locale;
/**
* Activity for verifying identity keys.
*
* @author Moxie Marlinspike
*/
@SuppressLint("StaticFieldLeak")
public class VerifyIdentityActivity extends PassphraseRequiredActivity implements ScanListener, View.OnClickListener {
private static final String TAG = Log.tag(VerifyIdentityActivity.class);
private static final String RECIPIENT_EXTRA = "recipient_id";
private static final String IDENTITY_EXTRA = "recipient_identity";
private static final String VERIFIED_EXTRA = "verified_state";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityDatabase.IdentityRecord identityRecord)
{
return newIntent(context,
identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
identityRecord.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
}
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityDatabase.IdentityRecord identityRecord,
boolean verified)
{
return newIntent(context,
identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
verified);
}
public static Intent newIntent(@NonNull Context context,
@NonNull RecipientId recipientId,
@NonNull IdentityKey identityKey,
boolean verified)
{
Intent intent = new Intent(context, VerifyIdentityActivity.class);
intent.putExtra(RECIPIENT_EXTRA, recipientId);
intent.putExtra(IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey));
intent.putExtra(VERIFIED_EXTRA, verified);
return intent;
}
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle state, boolean ready) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_number);
Bundle extras = new Bundle();
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
extras.putParcelable(VerifyDisplayFragment.LOCAL_IDENTITY, new IdentityKeyParcelable(IdentityKeyUtil.getIdentityKey(this)));
extras.putString(VerifyDisplayFragment.LOCAL_NUMBER, TextSecurePreferences.getLocalNumber(this));
extras.putBoolean(VerifyDisplayFragment.VERIFIED_STATE, getIntent().getBooleanExtra(VERIFIED_EXTRA, false));
scanFragment.setScanListener(this);
displayFragment.setClickListener(this);
initFragment(android.R.id.content, displayFragment, Locale.getDefault(), extras);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
}
return false;
}
@Override
public void onQrDataFound(final String data) {
ThreadUtil.runOnMain(() -> {
((Vibrator)getSystemService(Context.VIBRATOR_SERVICE)).vibrate(50);
getSupportFragmentManager().popBackStack();
displayFragment.setScannedFingerprint(data);
});
}
@Override
public void onClick(View v) {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied))
.onAllGranted(() -> {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_top);
transaction.replace(android.R.id.content, scanFragment)
.addToBackStack(null)
.commitAllowingStateLoss();
})
.onAnyDenied(() -> Toast.makeText(this, R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show())
.execute();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
public static final String RECIPIENT_ID = "recipient_id";
public static final String REMOTE_NUMBER = "remote_number";
public static final String REMOTE_IDENTITY = "remote_identity";
public static final String LOCAL_IDENTITY = "local_identity";
public static final String LOCAL_NUMBER = "local_number";
public static final String VERIFIED_STATE = "verified_state";
private LiveRecipient recipient;
private IdentityKey localIdentity;
private IdentityKey remoteIdentity;
private Fingerprint fingerprint;
private View container;
private View numbersContainer;
private ImageView qrCode;
private ImageView qrVerified;
private TextView tapLabel;
private TextView description;
private View.OnClickListener clickListener;
private SwitchCompat verified;
private TextView[] codes = new TextView[12];
private boolean animateSuccessOnDraw = false;
private boolean animateFailureOnDraw = false;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
this.numbersContainer = container.findViewById(R.id.number_table);
this.qrCode = container.findViewById(R.id.qr_code);
this.verified = container.findViewById(R.id.verified_switch);
this.qrVerified = container.findViewById(R.id.qr_verified);
this.description = container.findViewById(R.id.description);
this.tapLabel = container.findViewById(R.id.tap_label);
this.codes[0] = container.findViewById(R.id.code_first);
this.codes[1] = container.findViewById(R.id.code_second);
this.codes[2] = container.findViewById(R.id.code_third);
this.codes[3] = container.findViewById(R.id.code_fourth);
this.codes[4] = container.findViewById(R.id.code_fifth);
this.codes[5] = container.findViewById(R.id.code_sixth);
this.codes[6] = container.findViewById(R.id.code_seventh);
this.codes[7] = container.findViewById(R.id.code_eighth);
this.codes[8] = container.findViewById(R.id.code_ninth);
this.codes[9] = container.findViewById(R.id.code_tenth);
this.codes[10] = container.findViewById(R.id.code_eleventh);
this.codes[11] = container.findViewById(R.id.code_twelth);
this.qrCode.setOnClickListener(clickListener);
this.registerForContextMenu(numbersContainer);
this.verified.setChecked(getArguments().getBoolean(VERIFIED_STATE, false));
this.verified.setOnCheckedChangeListener(this);
return container;
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
RecipientId recipientId = getArguments().getParcelable(RECIPIENT_ID);
IdentityKeyParcelable localIdentityParcelable = getArguments().getParcelable(LOCAL_IDENTITY);
IdentityKeyParcelable remoteIdentityParcelable = getArguments().getParcelable(REMOTE_IDENTITY);
if (recipientId == null) throw new AssertionError("RecipientId required");
if (localIdentityParcelable == null) throw new AssertionError("local identity required");
if (remoteIdentityParcelable == null) throw new AssertionError("remote identity required");
this.localIdentity = localIdentityParcelable.get();
this.recipient = Recipient.live(recipientId);
this.remoteIdentity = remoteIdentityParcelable.get();
int version;
byte[] localId;
byte[] remoteId;
//noinspection WrongThread
Recipient resolved = recipient.resolve();
if (FeatureFlags.verifyV2() && resolved.getUuid().isPresent()) {
Log.i(TAG, "Using UUID (version 2).");
version = 2;
localId = UuidUtil.toByteArray(TextSecurePreferences.getLocalUuid(requireContext()));
remoteId = UuidUtil.toByteArray(resolved.getUuid().get());
} else if (!FeatureFlags.verifyV2() && resolved.getE164().isPresent()) {
Log.i(TAG, "Using E164 (version 1).");
version = 1;
localId = TextSecurePreferences.getLocalNumber(requireContext()).getBytes();
remoteId = resolved.requireE164().getBytes();
} else {
Log.w(TAG, String.format(Locale.ENGLISH, "Could not show proper verification! verifyV2: %s, hasUuid: %s, hasE164: %s", FeatureFlags.verifyV2(), resolved.getUuid().isPresent(), resolved.getE164().isPresent()));
new AlertDialog.Builder(requireContext())
.setMessage(getString(R.string.VerifyIdentityActivity_you_must_first_exchange_messages_in_order_to_view, resolved.getDisplayName(requireContext())))
.setPositiveButton(android.R.string.ok, (dialog, which) -> requireActivity().finish())
.setOnDismissListener(dialog -> requireActivity().finish())
.show();
return;
}
this.recipient.observe(this, this::setRecipientText);
new AsyncTask<Void, Void, Fingerprint>() {
@Override
protected Fingerprint doInBackground(Void... params) {
return new NumericFingerprintGenerator(5200).createFor(version,
localId, localIdentity,
remoteId, remoteIdentity);
}
@Override
protected void onPostExecute(Fingerprint fingerprint) {
VerifyDisplayFragment.this.fingerprint = fingerprint;
setFingerprintViews(fingerprint, true);
getActivity().supportInvalidateOptionsMenu();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
setHasOptionsMenu(true);
}
@Override
public void onResume() {
super.onResume();
setRecipientText(recipient.get());
if (fingerprint != null) {
setFingerprintViews(fingerprint, false);
}
if (animateSuccessOnDraw) {
animateSuccessOnDraw = false;
animateVerifiedSuccess();
} else if (animateFailureOnDraw) {
animateFailureOnDraw = false;
animateVerifiedFailure();
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View view,
ContextMenuInfo menuInfo)
{
super.onCreateContextMenu(menu, view, menuInfo);
if (fingerprint != null) {
MenuInflater inflater = getActivity().getMenuInflater();
inflater.inflate(R.menu.verify_display_fragment_context_menu, menu);
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
if (fingerprint == null) return super.onContextItemSelected(item);
switch (item.getItemId()) {
case R.id.menu_copy: handleCopyToClipboard(fingerprint, codes.length); return true;
case R.id.menu_compare: handleCompareWithClipboard(fingerprint); return true;
default: return super.onContextItemSelected(item);
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
if (fingerprint != null) {
inflater.inflate(R.menu.verify_identity, menu);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.verify_identity__share: handleShare(fingerprint, codes.length); return true;
}
return false;
}
public void setScannedFingerprint(String scanned) {
try {
if (fingerprint.getScannableFingerprint().compareTo(scanned.getBytes("ISO-8859-1"))) {
this.animateSuccessOnDraw = true;
} else {
this.animateFailureOnDraw = true;
}
} catch (FingerprintVersionMismatchException e) {
Log.w(TAG, e);
if (e.getOurVersion() < e.getTheirVersion()) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_a_newer_version_of_Signal, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
Log.w(TAG, e);
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_the_scanned_qr_code_is_not_a_correctly_formatted_safety_number, Toast.LENGTH_LONG).show();
}
}
public void setClickListener(View.OnClickListener listener) {
this.clickListener = listener;
}
private @NonNull String getFormattedSafetyNumbers(@NonNull Fingerprint fingerprint, int segmentCount) {
String[] segments = getSegments(fingerprint, segmentCount);
StringBuilder result = new StringBuilder();
for (int i = 0; i < segments.length; i++) {
result.append(segments[i]);
if (i != segments.length - 1) {
if (((i+1) % 4) == 0) result.append('\n');
else result.append(' ');
}
}
return result.toString();
}
private void handleCopyToClipboard(Fingerprint fingerprint, int segmentCount) {
Util.writeTextToClipboard(getActivity(), getFormattedSafetyNumbers(fingerprint, segmentCount));
}
private void handleCompareWithClipboard(Fingerprint fingerprint) {
String clipboardData = Util.readTextFromClipboard(getActivity());
if (clipboardData == null) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
return;
}
String numericClipboardData = clipboardData.replaceAll("\\D", "");
if (TextUtils.isEmpty(numericClipboardData) || numericClipboardData.length() != 60) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_safety_number_to_compare_was_found_in_the_clipboard, Toast.LENGTH_LONG).show();
return;
}
if (fingerprint.getDisplayableFingerprint().getDisplayText().equals(numericClipboardData)) {
animateVerifiedSuccess();
} else {
animateVerifiedFailure();
}
}
private void handleShare(@NonNull Fingerprint fingerprint, int segmentCount) {
String shareString =
getString(R.string.VerifyIdentityActivity_our_signal_safety_number) + "\n" +
getFormattedSafetyNumbers(fingerprint, segmentCount) + "\n";
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_TEXT, shareString);
intent.setType("text/plain");
try {
startActivity(Intent.createChooser(intent, getString(R.string.VerifyIdentityActivity_share_safety_number_via)));
} catch (ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_no_app_to_share_to, Toast.LENGTH_LONG).show();
}
}
private void setRecipientText(Recipient recipient) {
description.setText(Html.fromHtml(String.format(getActivity().getString(R.string.verify_display_fragment__if_you_wish_to_verify_the_security_of_your_end_to_end_encryption_with_s), recipient.getDisplayName(getContext()))));
description.setMovementMethod(LinkMovementMethod.getInstance());
}
private void setFingerprintViews(Fingerprint fingerprint, boolean animate) {
String[] segments = getSegments(fingerprint, codes.length);
for (int i=0;i<codes.length;i++) {
if (animate) setCodeSegment(codes[i], segments[i]);
else codes[i].setText(segments[i]);
}
byte[] qrCodeData = fingerprint.getScannableFingerprint().getSerialized();
String qrCodeString = new String(qrCodeData, Charset.forName("ISO-8859-1"));
Bitmap qrCodeBitmap = QrCode.create(qrCodeString);
qrCode.setImageBitmap(qrCodeBitmap);
if (animate) {
ViewUtil.fadeIn(qrCode, 1000);
ViewUtil.fadeIn(tapLabel, 1000);
} else {
qrCode.setVisibility(View.VISIBLE);
tapLabel.setVisibility(View.VISIBLE);
}
}
private void setCodeSegment(final TextView codeView, String segment) {
ValueAnimator valueAnimator = new ValueAnimator();
valueAnimator.setObjectValues(0, Integer.parseInt(segment));
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int value = (int) animation.getAnimatedValue();
codeView.setText(String.format(Locale.getDefault(), "%05d", value));
}
});
valueAnimator.setEvaluator(new TypeEvaluator<Integer>() {
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
return Math.round(startValue + (endValue - startValue) * fraction);
}
});
valueAnimator.setDuration(1000);
valueAnimator.start();
}
private String[] getSegments(Fingerprint fingerprint, int segmentCount) {
String[] segments = new String[segmentCount];
String digits = fingerprint.getDisplayableFingerprint().getDisplayText();
int partSize = digits.length() / segmentCount;
for (int i=0;i<segmentCount;i++) {
segments[i] = digits.substring(i * partSize, (i * partSize) + partSize);
}
return segments;
}
private Bitmap createVerifiedBitmap(int width, int height, @DrawableRes int id) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Bitmap check = BitmapFactory.decodeResource(getResources(), id);
float offset = (width - check.getWidth()) / 2;
canvas.drawBitmap(check, offset, offset, null);
return bitmap;
}
private void animateVerifiedSuccess() {
Bitmap qrBitmap = ((BitmapDrawable)qrCode.getDrawable()).getBitmap();
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_check_white_48dp);
qrVerified.setImageBitmap(qrSuccess);
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.MULTIPLY);
animateVerified();
}
private void animateVerifiedFailure() {
Bitmap qrBitmap = ((BitmapDrawable)qrCode.getDrawable()).getBitmap();
Bitmap qrSuccess = createVerifiedBitmap(qrBitmap.getWidth(), qrBitmap.getHeight(), R.drawable.ic_close_white_48dp);
qrVerified.setImageBitmap(qrSuccess);
qrVerified.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.MULTIPLY);
animateVerified();
}
private void animateVerified() {
ScaleAnimation scaleAnimation = new ScaleAnimation(0, 1, 0, 1,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setInterpolator(new OvershootInterpolator());
scaleAnimation.setDuration(800);
scaleAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
qrVerified.postDelayed(new Runnable() {
@Override
public void run() {
ScaleAnimation scaleAnimation = new ScaleAnimation(1, 0, 1, 0,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f,
ScaleAnimation.RELATIVE_TO_SELF, 0.5f);
scaleAnimation.setInterpolator(new AnticipateInterpolator());
scaleAnimation.setDuration(500);
ViewUtil.animateOut(qrVerified, scaleAnimation, View.GONE);
}
}, 2000);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
ViewUtil.animateIn(qrVerified, scaleAnimation);
}
@Override
public void onCheckedChanged(CompoundButton buttonView, final boolean isChecked) {
final Recipient recipient = this.recipient.get();
final RecipientId recipientId = recipient.getId();
SignalExecutors.BOUNDED.execute(() -> {
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
if (isChecked) {
Log.i(TAG, "Saving identity: " + recipientId);
DatabaseFactory.getIdentityDatabase(getActivity())
.saveIdentity(recipientId,
remoteIdentity,
VerifiedStatus.VERIFIED, false,
System.currentTimeMillis(), true);
} else {
DatabaseFactory.getIdentityDatabase(getActivity())
.setVerified(recipientId,
remoteIdentity,
VerifiedStatus.DEFAULT);
}
ApplicationDependencies.getJobManager()
.add(new MultiDeviceVerifiedUpdateJob(recipientId,
remoteIdentity,
isChecked ? VerifiedStatus.VERIFIED
: VerifiedStatus.DEFAULT));
StorageSyncHelper.scheduleSyncForDataChange();
IdentityUtil.markIdentityVerified(getActivity(), recipient, isChecked, false);
}
});
}
}
public static class VerifyScanFragment extends Fragment {
private View container;
private CameraView cameraView;
private ScanningThread scanningThread;
private ScanListener scanListener;
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
this.cameraView = container.findViewById(R.id.scanner);
return container;
}
@Override
public void onResume() {
super.onResume();
this.scanningThread = new ScanningThread();
this.scanningThread.setScanListener(scanListener);
this.scanningThread.setCharacterSet("ISO-8859-1");
this.cameraView.onResume();
this.cameraView.setPreviewCallback(scanningThread);
this.scanningThread.start();
}
@Override
public void onPause() {
super.onPause();
this.cameraView.onPause();
this.scanningThread.stopScanning();
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
this.cameraView.onPause();
this.cameraView.onResume();
this.cameraView.setPreviewCallback(scanningThread);
}
public void setScanListener(ScanListener listener) {
if (this.scanningThread != null) scanningThread.setScanListener(listener);
this.scanListener = listener;
}
}
}
@@ -18,27 +18,36 @@
package org.thoughtcrime.securesms; package org.thoughtcrime.securesms;
import android.Manifest; import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PictureInPictureParams; import android.app.PictureInPictureParams;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Rect;
import android.media.AudioManager; import android.media.AudioManager;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.util.Rational; import android.util.Rational;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate; import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.lifecycle.ViewModelProviders; import androidx.lifecycle.ViewModelProviders;
import androidx.window.DisplayFeature;
import androidx.window.FoldingFeature;
import androidx.window.WindowLayoutInfo;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode; import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.TooltipPopup; import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor; import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
@@ -49,6 +58,7 @@ import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeN
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput; import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel; import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog; import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog; import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -60,15 +70,20 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.webrtc.SignalCallManager; import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter; import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.FullscreenHelper; import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage; import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE; import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
@@ -87,11 +102,14 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private CallParticipantsListUpdatePopupWindow participantUpdateWindow; private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
private DeviceOrientationMonitor deviceOrientationMonitor; private DeviceOrientationMonitor deviceOrientationMonitor;
private FullscreenHelper fullscreenHelper; private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen; private WebRtcCallView callScreen;
private TooltipPopup videoTooltip; private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel; private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable; private boolean enableVideoIfAvailable;
private androidx.window.WindowManager windowManager;
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
private ThrottledDebouncer requestNewSizesThrottle;
@Override @Override
protected void attachBaseContext(@NonNull Context newBase) { protected void attachBaseContext(@NonNull Context newBase) {
@@ -99,6 +117,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
super.attachBaseContext(newBase); super.attachBaseContext(newBase);
} }
@SuppressLint("SourceLockedOrientationActivity")
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate()"); Log.i(TAG, "onCreate()");
@@ -106,6 +125,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
boolean isLandscapeEnabled = getResources().getConfiguration().smallestScreenWidthDp >= 480;
if (!isLandscapeEnabled) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity); setContentView(R.layout.webrtc_call_activity);
@@ -114,12 +138,19 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL); setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
initializeResources(); initializeResources();
initializeViewModel(); initializeViewModel(isLandscapeEnabled);
processIntent(getIntent()); processIntent(getIntent());
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false); enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE); getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
windowManager = new androidx.window.WindowManager(this);
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
windowManager.registerLayoutChangeCallback(SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
} }
@Override @Override
@@ -164,6 +195,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
if (!isInPipMode() || isFinishing()) { if (!isInPipMode() || isFinishing()) {
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
requestNewSizesThrottle.clear();
} }
if (!viewModel.isCallStarting()) { if (!viewModel.isCallStarting()) {
@@ -177,9 +209,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
windowManager.unregisterLayoutChangeCallback(windowLayoutInfoConsumer);
EventBus.getDefault().unregister(this); EventBus.getDefault().unregister(this);
} }
@SuppressLint("MissingSuperCall")
@Override @Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@@ -206,8 +240,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private boolean enterPipModeIfPossible() { private boolean enterPipModeIfPossible() {
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) { if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder() PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(9, 16)) .setAspectRatio(new Rational(9, 16))
.build(); .build();
enterPictureInPictureMode(params); enterPictureInPictureMode(params);
CallParticipantsListDialog.dismiss(getSupportFragmentManager()); CallParticipantsListDialog.dismiss(getSupportFragmentManager());
@@ -222,7 +256,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void processIntent(@NonNull Intent intent) { private void processIntent(@NonNull Intent intent) {
if (ANSWER_ACTION.equals(intent.getAction())) { if (ANSWER_ACTION.equals(intent.getAction())) {
viewModel.setRecipient(EventBus.getDefault().getStickyEvent(WebRtcViewModel.class).getRecipient());
handleAnswerWithAudio(); handleAnswerWithAudio();
} else if (DENY_ACTION.equals(intent.getAction())) { } else if (DENY_ACTION.equals(intent.getAction())) {
handleDenyCall(); handleDenyCall();
@@ -246,53 +279,46 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen); participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen);
} }
private void initializeViewModel() { private void initializeViewModel(boolean isLandscapeEnabled) {
deviceOrientationMonitor = new DeviceOrientationMonitor(this); deviceOrientationMonitor = new DeviceOrientationMonitor(this);
getLifecycle().addObserver(deviceOrientationMonitor); getLifecycle().addObserver(deviceOrientationMonitor);
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor); WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor);
viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class); viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class);
viewModel.setIsLandscapeEnabled(isLandscapeEnabled);
viewModel.setIsInPipMode(isInPipMode()); viewModel.setIsInPipMode(isInPipMode());
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled); viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls); viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent); viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime); viewModel.getCallTime().observe(this, this::handleCallTime);
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(), viewModel.getOrientation(), (s, o) -> new Pair<>(s, o == PORTRAIT_BOTTOM_EDGE))
.observe(this, p -> callScreen.updateCallParticipants(p.first(), p.second())); LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
viewModel.getOrientationAndLandscapeEnabled(),
(s, o) -> new CallParticipantsViewState(s, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
.observe(this, p -> callScreen.updateCallParticipants(p));
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate); viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent); viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall()); viewModel.getGroupMembersChanged().observe(this, unused -> updateGroupMembersForGroupCall());
viewModel.getGroupMemberCount().observe(this, this::handleGroupMemberCountChange);
viewModel.shouldShowSpeakerHint().observe(this, this::updateSpeakerHint); viewModel.shouldShowSpeakerHint().observe(this, this::updateSpeakerHint);
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> { callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue(); CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null) { if (state != null) {
if (state.needsNewRequestSizes()) { if (state.needsNewRequestSizes()) {
ApplicationDependencies.getSignalCallManager().updateRenderedResolutions(); requestNewSizesThrottle.publish(() -> ApplicationDependencies.getSignalCallManager().updateRenderedResolutions());
} }
} }
}); });
viewModel.getOrientation().observe(this, orientation -> { viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
ApplicationDependencies.getSignalCallManager().orientationChanged(orientation.getDegrees()); viewModel.getControlsRotation().observe(this, callScreen::rotateControls);
switch (orientation) {
case LANDSCAPE_LEFT_EDGE:
callScreen.rotateControls(90);
break;
case LANDSCAPE_RIGHT_EDGE:
callScreen.rotateControls(-90);
break;
case PORTRAIT_BOTTOM_EDGE:
callScreen.rotateControls(0);
}
});
} }
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) { private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
if (event instanceof WebRtcCallViewModel.Event.StartCall) { if (event instanceof WebRtcCallViewModel.Event.StartCall) {
startCall(((WebRtcCallViewModel.Event.StartCall)event).isVideoCall()); startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall());
return; return;
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) { } else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords()); SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords());
@@ -340,15 +366,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
} }
private void handleSetAudioHandset() { private void handleSetAudioHandset() {
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(false); ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.EARPIECE);
} }
private void handleSetAudioSpeaker() { private void handleSetAudioSpeaker() {
ApplicationDependencies.getSignalCallManager().setAudioSpeaker(true); ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.SPEAKER_PHONE);
} }
private void handleSetAudioBluetooth() { private void handleSetAudioBluetooth() {
ApplicationDependencies.getSignalCallManager().setAudioBluetooth(true); ApplicationDependencies.getSignalCallManager().selectAudioDevice(SignalAudioManager.AudioDevice.BLUETOOTH);
} }
private void handleSetMuteAudio(boolean enabled) { private void handleSetMuteAudio(boolean enabled) {
@@ -376,24 +402,19 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
} }
private void handleAnswerWithAudio() { private void handleAnswerWithAudio() {
Recipient recipient = viewModel.getRecipient().get(); Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone),
R.drawable.ic_mic_solid_24)
.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)) { ApplicationDependencies.getSignalCallManager().acceptCall(false);
Permissions.with(this) })
.request(Manifest.permission.RECORD_AUDIO) .onAnyDenied(this::handleDenyCall)
.ifNecessary() .execute();
.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)
.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(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
}
} }
private void handleAnswerWithVideo() { private void handleAnswerWithVideo() {
@@ -404,7 +425,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) .request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary() .ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)), .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) 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)) .withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> { .onAllGranted(() -> {
callScreen.setRecipient(recipient); callScreen.setRecipient(recipient);
@@ -457,6 +478,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
delayedFinish(); delayedFinish();
} }
private void handleGlare(@NonNull Recipient recipient) {
Log.i(TAG, "handleGlare: " + recipient.getId());
callScreen.setStatus("");
}
private void handleCallRinging() { private void handleCallRinging() {
callScreen.setStatus(getString(R.string.RedPhone_ringing)); callScreen.setStatus(getString(R.string.RedPhone_ringing));
} }
@@ -488,13 +515,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) { private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle(R.string.RedPhone_number_not_registered) .setTitle(R.string.RedPhone_number_not_registered)
.setIcon(R.drawable.ic_warning) .setIcon(R.drawable.ic_warning)
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice) .setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
.setCancelable(true) .setCancelable(true)
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL)) .setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL)) .setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.show(); .show();
} }
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) { private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
@@ -524,6 +551,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
ApplicationDependencies.getSignalCallManager().requestUpdateGroupMembers(); ApplicationDependencies.getSignalCallManager().requestUpdateGroupMembers();
} }
public void handleGroupMemberCountChange(int count) {
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize() && FeatureFlags.groupCallRinging();
callScreen.enableRingGroup(canRing);
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
}
private void updateSpeakerHint(boolean showSpeakerHint) { private void updateSpeakerHint(boolean showSpeakerHint) {
if (showSpeakerHint) { if (showSpeakerHint) {
callScreen.showSpeakerViewHint(); callScreen.showSpeakerViewHint();
@@ -586,20 +619,36 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
callScreen.setRecipient(event.getRecipient()); callScreen.setRecipient(event.getRecipient());
switch (event.getState()) { switch (event.getState()) {
case CALL_PRE_JOIN: handleCallPreJoin(event); break; case CALL_PRE_JOIN:
case CALL_CONNECTED: handleCallConnected(event); break; handleCallPreJoin(event); break;
case NETWORK_FAILURE: handleServerFailure(); break; case CALL_CONNECTED:
case CALL_RINGING: handleCallRinging(); break; handleCallConnected(event); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break; case NETWORK_FAILURE:
case CALL_ACCEPTED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break; handleServerFailure(); break;
case CALL_DECLINED_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break; case CALL_RINGING:
case CALL_ONGOING_ELSEWHERE: handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break; handleCallRinging(); break;
case CALL_NEEDS_PERMISSION: handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break; case CALL_DISCONNECTED:
case NO_SUCH_USER: handleNoSuchUser(event); break; handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(); break; case CALL_DISCONNECTED_GLARE:
case CALL_OUTGOING: handleOutgoingCall(event); break; handleGlare(event.getRecipient()); break;
case CALL_BUSY: handleCallBusy(); break; case CALL_ACCEPTED_ELSEWHERE:
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break; handleTerminate(event.getRecipient(), HangupMessage.Type.ACCEPTED); break;
case CALL_DECLINED_ELSEWHERE:
handleTerminate(event.getRecipient(), HangupMessage.Type.DECLINED); break;
case CALL_ONGOING_ELSEWHERE:
handleTerminate(event.getRecipient(), HangupMessage.Type.BUSY); break;
case CALL_NEEDS_PERMISSION:
handleTerminate(event.getRecipient(), HangupMessage.Type.NEED_PERMISSION); break;
case NO_SUCH_USER:
handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE:
handleRecipientUnavailable(); break;
case CALL_OUTGOING:
handleOutgoingCall(event); break;
case CALL_BUSY:
handleCallBusy(); break;
case UNTRUSTED_IDENTITY:
handleUntrustedIdentity(event); break;
} }
boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable; boolean enableVideo = event.getLocalParticipant().getCameraState().getCameraCount() > 0 && enableVideoIfAvailable;
@@ -615,6 +664,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void handleCallPreJoin(@NonNull WebRtcViewModel event) { private void handleCallPreJoin(@NonNull WebRtcViewModel event) {
if (event.getGroupState().isNotIdle()) { if (event.getGroupState().isNotIdle()) {
callScreen.setStatusFromGroupCallState(event.getGroupState()); callScreen.setStatusFromGroupCallState(event.getGroupState());
callScreen.setRingGroup(event.shouldRingGroup());
if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) {
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
}
} }
} }
@@ -729,5 +783,38 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public void onLocalPictureInPictureClicked() { public void onLocalPictureInPictureClicked() {
viewModel.onLocalPictureInPictureClicked(); viewModel.onLocalPictureInPictureClicked();
} }
@Override
public void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed) {
if (ringingAllowed) {
ApplicationDependencies.getSignalCallManager().setRingGroup(ringGroup);
} else {
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
Toast.makeText(WebRtcCallActivity.this, R.string.WebRtcCallActivity__group_is_too_large_to_ring_the_participants, Toast.LENGTH_SHORT).show();
}
}
}
private class WindowLayoutInfoConsumer implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo windowLayoutInfo) {
Log.d(TAG, "On WindowLayoutInfo accepted: " + windowLayoutInfo.toString());
Optional<DisplayFeature> feature = windowLayoutInfo.getDisplayFeatures().stream().filter(f -> f instanceof FoldingFeature).findFirst();
viewModel.setIsLandscapeEnabled(feature.isPresent());
setRequestedOrientation(feature.isPresent() ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
if (feature.isPresent()) {
FoldingFeature foldingFeature = (FoldingFeature) feature.get();
Rect bounds = foldingFeature.getBounds();
if (foldingFeature.getState() == FoldingFeature.State.HALF_OPENED && bounds.top == bounds.bottom) {
Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in table-top display mode");
viewModel.setFoldableState(WebRtcControls.FoldableState.folded(bounds.top));
} else {
Log.d(TAG, "OnWindowLayoutInfo accepted: ensure call view is in flat display mode");
viewModel.setFoldableState(WebRtcControls.FoldableState.flat());
}
}
}
} }
} }
@@ -0,0 +1,96 @@
package org.thoughtcrime.securesms.animation.transitions
import android.animation.Animator
import android.animation.ObjectAnimator
import android.animation.PropertyValuesHolder
import android.animation.TypeEvaluator
import android.content.Context
import android.transition.Transition
import android.transition.TransitionValues
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
import androidx.annotation.RequiresApi
private const val POSITION_ON_SCREEN = "signal.circleavatartransition.positiononscreen"
private const val WIDTH = "signal.circleavatartransition.width"
private const val HEIGHT = "signal.circleavatartransition.height"
/**
* Custom transition for Circular avatars, because once you have multiple things animating stuff was getting broken and weird.
*/
@RequiresApi(21)
class CircleAvatarTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
override fun captureStartValues(transitionValues: TransitionValues) {
captureValues(transitionValues)
}
override fun captureEndValues(transitionValues: TransitionValues) {
captureValues(transitionValues)
}
private fun captureValues(transitionValues: TransitionValues) {
val view: View = transitionValues.view
if (view.transitionName == "avatar") {
val topLeft = intArrayOf(0, 0)
view.getLocationOnScreen(topLeft)
transitionValues.values[POSITION_ON_SCREEN] = topLeft
transitionValues.values[WIDTH] = view.measuredWidth
transitionValues.values[HEIGHT] = view.measuredHeight
}
}
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues == null || endValues == null) {
return null
}
val view: View = endValues.view
if (view.transitionName != "avatar") {
return null
}
val startCoords: IntArray = startValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
val endCoords: IntArray = endValues.values[POSITION_ON_SCREEN] as? IntArray ?: intArrayOf(0, 0).apply { view.getLocationOnScreen(this) }
val startWidth: Int = startValues.values[WIDTH] as? Int ?: view.measuredWidth
val endWidth: Int = endValues.values[WIDTH] as? Int ?: view.measuredWidth
val startHeight: Int = startValues.values[HEIGHT] as? Int ?: view.measuredHeight
val endHeight: Int = endValues.values[HEIGHT] as? Int ?: view.measuredHeight
val startHeightOffset = (endHeight - startHeight) / 2f
val startWidthOffset = (endWidth - startWidth) / 2f
val translateXHolder = PropertyValuesHolder.ofFloat("translationX", startCoords[0] - endCoords[0] - startWidthOffset, 0f).apply {
setEvaluator(FloatInterpolatorEvaluator(DecelerateInterpolator()))
}
val translateYHolder = PropertyValuesHolder.ofFloat("translationY", startCoords[1] - endCoords[1] - startHeightOffset, 0f).apply {
setEvaluator(FloatInterpolatorEvaluator(AccelerateInterpolator()))
}
val widthRatio = startWidth.toFloat() / endWidth
val scaleXHolder = PropertyValuesHolder.ofFloat("scaleX", widthRatio, 1f)
val heightRatio = startHeight.toFloat() / endHeight
val scaleYHolder = PropertyValuesHolder.ofFloat("scaleY", heightRatio, 1f)
return ObjectAnimator.ofPropertyValuesHolder(view, translateXHolder, translateYHolder, scaleXHolder, scaleYHolder)
}
private class FloatInterpolatorEvaluator(
private val interpolator: Interpolator
) : TypeEvaluator<Float> {
override fun evaluate(fraction: Float, startValue: Float, endValue: Float): Float {
val interpolatedFraction = interpolator.getInterpolation(fraction)
val delta = endValue - startValue
return delta * interpolatedFraction + startValue
}
}
}
@@ -0,0 +1,63 @@
package org.thoughtcrime.securesms.animation.transitions
import android.animation.Animator
import android.animation.ObjectAnimator
import android.animation.RectEvaluator
import android.content.Context
import android.graphics.Rect
import android.transition.Transition
import android.transition.TransitionValues
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.core.animation.addListener
import androidx.fragment.app.FragmentContainerView
private const val BOUNDS = "signal.wipedowntransition.bottom"
/**
* WipeDownTransition will animate the bottom position of a view such that it "wipes" down the screen to a final position.
*/
@RequiresApi(21)
class WipeDownTransition(context: Context, attrs: AttributeSet?) : Transition(context, attrs) {
override fun captureStartValues(transitionValues: TransitionValues) {
captureValues(transitionValues)
}
override fun captureEndValues(transitionValues: TransitionValues) {
captureValues(transitionValues)
}
private fun captureValues(transitionValues: TransitionValues) {
val view: View = transitionValues.view
if (view is ViewGroup) {
val rect = Rect()
view.getLocalVisibleRect(rect)
transitionValues.values[BOUNDS] = rect
}
}
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues == null || endValues == null) {
return null
}
val view: View = endValues.view
if (view !is FragmentContainerView) {
return null
}
val startBottom: Rect = startValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
val endBottom: Rect = endValues.values[BOUNDS] as? Rect ?: Rect().apply { view.getLocalVisibleRect(this) }
return ObjectAnimator.ofObject(view, "clipBounds", RectEvaluator(), startBottom, endBottom).apply {
addListener(
onEnd = {
view.clipBounds = null
}
)
}
}
}
@@ -1,13 +1,12 @@
package org.thoughtcrime.securesms.audio; package org.thoughtcrime.securesms.audio;
import android.annotation.TargetApi;
import android.media.AudioFormat; import android.media.AudioFormat;
import android.media.AudioRecord; import android.media.AudioRecord;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.media.MediaRecorder; import android.media.MediaRecorder;
import android.os.Build; import android.os.ParcelFileDescriptor;
import org.signal.core.util.StreamUtil; import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
@@ -17,8 +16,7 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN) public class AudioCodec implements Recorder {
public class AudioCodec {
private static final String TAG = Log.tag(AudioCodec.class); private static final String TAG = Log.tag(AudioCodec.class);
@@ -32,6 +30,7 @@ public class AudioCodec {
private final AudioRecord audioRecord; private final AudioRecord audioRecord;
private boolean running = true; private boolean running = true;
private boolean failed = false;
private boolean finished = false; private boolean finished = false;
public AudioCodec() throws IOException { public AudioCodec() throws IOException {
@@ -50,12 +49,19 @@ public class AudioCodec {
} }
} }
@Override
public void start(ParcelFileDescriptor fileDescriptor) {
Log.i(TAG, "Recording voice note using AudioCodec.");
start(new ParcelFileDescriptor.AutoCloseOutputStream(fileDescriptor));
}
@Override
public synchronized void stop() { public synchronized void stop() {
running = false; running = false;
while (!finished) Util.wait(this, 0); while (!finished) Util.wait(this, 0);
} }
public void start(final OutputStream outputStream) { private void start(final OutputStream outputStream) {
new Thread(new Runnable() { new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -76,10 +82,25 @@ public class AudioCodec {
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
} finally { } finally {
mediaCodec.stop();
audioRecord.stop();
mediaCodec.release(); try {
mediaCodec.stop();
} catch (IllegalStateException ise) {
Log.w(TAG, "mediaCodec stop failed.", ise);
}
try {
audioRecord.stop();
} catch (IllegalStateException ise) {
Log.w(TAG, "audioRecord stop failed.", ise);
}
try {
mediaCodec.release();
} catch (IllegalStateException ise) {
Log.w(TAG, "mediaCodec release failed. Probably already released.", ise);
}
audioRecord.release(); audioRecord.release();
StreamUtil.close(outputStream); StreamUtil.close(outputStream);
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.audio; package org.thoughtcrime.securesms.audio;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
@@ -11,16 +10,16 @@ import androidx.annotation.NonNull;
import org.signal.core.util.ThreadUtil; import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture; import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.Pair;
import java.io.IOException; import java.io.IOException;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class AudioRecorder { public class AudioRecorder {
private static final String TAG = Log.tag(AudioRecorder.class); private static final String TAG = Log.tag(AudioRecorder.class);
@@ -29,8 +28,8 @@ public class AudioRecorder {
private final Context context; private final Context context;
private AudioCodec audioCodec; private Recorder recorder;
private Uri captureUri; private Uri captureUri;
public AudioRecorder(@NonNull Context context) { public AudioRecorder(@NonNull Context context) {
this.context = context; this.context = context;
@@ -42,7 +41,7 @@ public class AudioRecorder {
executor.execute(() -> { executor.execute(() -> {
Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId()); Log.i(TAG, "Running startRecording() + " + Thread.currentThread().getId());
try { try {
if (audioCodec != null) { if (recorder != null) {
throw new AssertionError("We can only record once at a time."); throw new AssertionError("We can only record once at a time.");
} }
@@ -51,38 +50,38 @@ public class AudioRecorder {
captureUri = BlobProvider.getInstance() captureUri = BlobProvider.getInstance()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0) .forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType(MediaUtil.AUDIO_AAC) .withMimeType(MediaUtil.AUDIO_AAC)
.createForSingleSessionOnDiskAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e)); .createForDraftAttachmentAsync(context, () -> Log.i(TAG, "Write successful."), e -> Log.w(TAG, "Error during recording", e));
audioCodec = new AudioCodec();
audioCodec.start(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1])); recorder = Build.VERSION.SDK_INT >= 26 && FeatureFlags.voiceNoteRecordingV2() ? new MediaRecorderWrapper() : new AudioCodec();
recorder.start(fds[1]);
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
} }
}); });
} }
public @NonNull ListenableFuture<Pair<Uri, Long>> stopRecording() { public @NonNull ListenableFuture<VoiceNoteDraft> stopRecording() {
Log.i(TAG, "stopRecording()"); Log.i(TAG, "stopRecording()");
final SettableFuture<Pair<Uri, Long>> future = new SettableFuture<>(); final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
executor.execute(() -> { executor.execute(() -> {
if (audioCodec == null) { if (recorder == null) {
sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!")); sendToFuture(future, new IOException("MediaRecorder was never initialized successfully!"));
return; return;
} }
audioCodec.stop(); recorder.stop();
try { try {
long size = MediaUtil.getMediaSize(context, captureUri); long size = MediaUtil.getMediaSize(context, captureUri);
sendToFuture(future, new Pair<>(captureUri, size)); sendToFuture(future, new VoiceNoteDraft(captureUri, size));
} catch (IOException ioe) { } catch (IOException ioe) {
Log.w(TAG, ioe); Log.w(TAG, ioe);
sendToFuture(future, ioe); sendToFuture(future, ioe);
} }
audioCodec = null; recorder = null;
captureUri = null; captureUri = null;
}); });
@@ -22,7 +22,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData; import org.thoughtcrime.securesms.database.model.databaseprotos.AudioWaveFormData;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput; import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
import org.thoughtcrime.securesms.media.MediaInput; import org.thoughtcrime.securesms.media.MediaInput;
@@ -65,12 +65,6 @@ public final class AudioWaveForm {
return; return;
} }
if (!(attachment instanceof DatabaseAttachment)) {
Log.i(TAG, "Not yet in database");
ThreadUtil.runOnMain(onFailure);
return;
}
String cacheKey = uri.toString(); String cacheKey = uri.toString();
AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey); AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
if (cached != null) { if (cached != null) {
@@ -104,26 +98,46 @@ public final class AudioWaveForm {
} }
} }
try { if (attachment instanceof DatabaseAttachment) {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); try {
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment; AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
long startTime = System.currentTimeMillis(); DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
long startTime = System.currentTimeMillis();
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance()); attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), AudioWaveFormData.getDefaultInstance());
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey)); Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
AudioFileInfo fileInfo = generateWaveForm(uri); AudioFileInfo fileInfo = generateWaveForm(uri);
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey)); Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf()); attachmentDatabase.writeAudioHash(dbAttachment.getAttachmentId(), fileInfo.toDatabaseProtobuf());
WAVE_FORM_CACHE.put(cacheKey, fileInfo); WAVE_FORM_CACHE.put(cacheKey, fileInfo);
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo)); ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (Throwable e) { } catch (Throwable e) {
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e); Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
ThreadUtil.runOnMain(onFailure); ThreadUtil.runOnMain(onFailure);
}
} else {
try {
Log.i(TAG, "Not in database and not cached. Generating wave form on-the-fly.");
long startTime = System.currentTimeMillis();
Log.i(TAG, String.format("Starting wave form generation (%s)", cacheKey));
AudioFileInfo fileInfo = generateWaveForm(uri);
Log.i(TAG, String.format(Locale.US, "Audio wave form generation time %d ms (%s)", System.currentTimeMillis() - startTime, cacheKey));
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (IOException e) {
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
ThreadUtil.runOnMain(onFailure);
}
} }
}); });
} }
@@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.audio;
import android.media.MediaRecorder;
import android.os.ParcelFileDescriptor;
import org.signal.core.util.logging.Log;
import java.io.IOException;
/**
* Wrap Android's {@link MediaRecorder} for use with voice notes.
*/
public class MediaRecorderWrapper implements Recorder {
private static final String TAG = Log.tag(MediaRecorderWrapper.class);
private static final int SAMPLE_RATE = 44100;
private static final int CHANNELS = 1;
private static final int BIT_RATE = 32000;
private MediaRecorder recorder = null;
@Override
public void start(ParcelFileDescriptor fileDescriptor) throws IOException {
Log.i(TAG, "Recording voice note using MediaRecorderWrapper.");
recorder = new MediaRecorder();
recorder.setAudioSource(MediaRecorder.AudioSource.MIC);
recorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);
recorder.setOutputFile(fileDescriptor.getFileDescriptor());
recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
recorder.setAudioSamplingRate(SAMPLE_RATE);
recorder.setAudioEncodingBitRate(BIT_RATE);
recorder.setAudioChannels(CHANNELS);
recorder.prepare();
recorder.start();
}
@Override
public void stop() {
try {
recorder.stop();
} catch (RuntimeException e) {
if (e.getClass() != RuntimeException.class) {
throw e;
} else {
Log.d(TAG, "Recording stopped with no data captured.");
}
} finally {
recorder.release();
recorder = null;
}
}
}
@@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.audio;
import android.os.ParcelFileDescriptor;
import java.io.IOException;
/**
* Simple abstraction of the interface for the original voice note recording and the new.
*/
public interface Recorder {
void start(ParcelFileDescriptor fileDescriptor) throws IOException;
void stop();
}
@@ -0,0 +1,79 @@
package org.thoughtcrime.securesms.avatar
import android.net.Uri
import org.thoughtcrime.securesms.R
/**
* Represents an Avatar which the user can choose, edit, and render into a bitmap via the renderer.
*/
sealed class Avatar(
open val databaseId: DatabaseId
) {
data class Resource(
val resourceId: Int,
val color: Avatars.ColorPair
) : Avatar(DatabaseId.DoNotPersist) {
override fun isSameAs(other: Avatar): Boolean {
return other is Resource && other.resourceId == resourceId
}
}
data class Text(
val text: String,
val color: Avatars.ColorPair,
override val databaseId: DatabaseId,
) : Avatar(databaseId) {
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
return copy(databaseId = databaseId)
}
override fun isSameAs(other: Avatar): Boolean {
return other is Text && other.databaseId == databaseId
}
}
data class Vector(
val key: String,
val color: Avatars.ColorPair,
override val databaseId: DatabaseId,
) : Avatar(databaseId) {
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
return copy(databaseId = databaseId)
}
override fun isSameAs(other: Avatar): Boolean {
return other is Vector && other.key == key
}
}
data class Photo(
val uri: Uri,
val size: Long,
override val databaseId: DatabaseId
) : Avatar(databaseId) {
override fun withDatabaseId(databaseId: DatabaseId): Avatar {
return copy(databaseId = databaseId)
}
override fun isSameAs(other: Avatar): Boolean {
return other is Photo && databaseId == other.databaseId
}
}
open fun withDatabaseId(databaseId: DatabaseId): Avatar {
throw UnsupportedOperationException()
}
abstract fun isSameAs(other: Avatar): Boolean
companion object {
fun getDefaultForSelf(): Resource = Resource(R.drawable.ic_profile_outline_40, Avatars.colors.random())
fun getDefaultForGroup(): Resource = Resource(R.drawable.ic_group_outline_40, Avatars.colors.random())
}
sealed class DatabaseId {
object DoNotPersist : DatabaseId()
object NotSet : DatabaseId()
data class Saved(val id: Long) : DatabaseId()
}
}
@@ -0,0 +1,68 @@
package org.thoughtcrime.securesms.avatar
import android.os.Bundle
/**
* Utility class which encapsulates reading and writing Avatar objects to and from Bundles.
*/
object AvatarBundler {
private const val TEXT = "org.thoughtcrime.securesms.avatar.TEXT"
private const val COLOR = "org.thoughtcrime.securesms.avatar.COLOR"
private const val URI = "org.thoughtcrime.securesms.avatar.URI"
private const val KEY = "org.thoughtcrime.securesms.avatar.KEY"
private const val DATABASE_ID = "org.thoughtcrime.securesms.avatar.DATABASE_ID"
private const val SIZE = "org.thoughtcrime.securesms.avatar.SIZE"
fun bundleText(text: Avatar.Text): Bundle = Bundle().apply {
putString(TEXT, text.text)
putString(COLOR, text.color.code)
putDatabaseId(DATABASE_ID, text.databaseId)
}
fun extractText(bundle: Bundle): Avatar.Text = Avatar.Text(
text = requireNotNull(bundle.getString(TEXT)),
color = Avatars.colorMap[bundle.getString(COLOR)] ?: throw IllegalStateException(),
databaseId = bundle.getDatabaseId()
)
fun bundlePhoto(photo: Avatar.Photo): Bundle = Bundle().apply {
putParcelable(URI, photo.uri)
putLong(SIZE, photo.size)
putDatabaseId(DATABASE_ID, photo.databaseId)
}
fun extractPhoto(bundle: Bundle): Avatar.Photo = Avatar.Photo(
uri = requireNotNull(bundle.getParcelable(URI)),
size = bundle.getLong(SIZE),
databaseId = bundle.getDatabaseId()
)
fun bundleVector(vector: Avatar.Vector): Bundle = Bundle().apply {
putString(KEY, vector.key)
putString(COLOR, vector.color.code)
putDatabaseId(DATABASE_ID, vector.databaseId)
}
fun extractVector(bundle: Bundle): Avatar.Vector = Avatar.Vector(
key = requireNotNull(bundle.getString(KEY)),
color = Avatars.colorMap[bundle.getString(COLOR)] ?: throw IllegalStateException(),
databaseId = bundle.getDatabaseId()
)
private fun Bundle.getDatabaseId(): Avatar.DatabaseId {
val id = getLong(DATABASE_ID, -1L)
return if (id == -1L) {
Avatar.DatabaseId.NotSet
} else {
Avatar.DatabaseId.Saved(id)
}
}
private fun Bundle.putDatabaseId(key: String, databaseId: Avatar.DatabaseId) {
if (databaseId is Avatar.DatabaseId.Saved) {
putLong(key, databaseId.id)
}
}
}
@@ -0,0 +1,43 @@
package org.thoughtcrime.securesms.avatar
import android.view.View
import android.widget.ImageView
import com.airbnb.lottie.SimpleColorFilter
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
typealias OnAvatarColorClickListener = (Avatars.ColorPair) -> Unit
/**
* Selectable color item for choosing colors when editing a Text or Vector avatar.
*/
data class AvatarColorItem(
val colors: Avatars.ColorPair,
val selected: Boolean
) {
companion object {
fun registerViewHolder(adapter: MappingAdapter, onAvatarColorClickListener: OnAvatarColorClickListener) {
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onAvatarColorClickListener) }, R.layout.avatar_color_item))
}
}
class Model(val colorItem: AvatarColorItem) : MappingModel<Model> {
override fun areItemsTheSame(newItem: Model): Boolean = newItem.colorItem.colors == colorItem.colors
override fun areContentsTheSame(newItem: Model): Boolean = newItem.colorItem == colorItem
}
private class ViewHolder(itemView: View, private val onAvatarColorClickListener: OnAvatarColorClickListener) : MappingViewHolder<Model>(itemView) {
private val imageView: ImageView = findViewById(R.id.avatar_color_item)
override fun bind(model: Model) {
itemView.setOnClickListener { onAvatarColorClickListener(model.colorItem.colors) }
imageView.background.colorFilter = SimpleColorFilter(model.colorItem.colors.backgroundColor)
imageView.isSelected = model.colorItem.selected
}
}
}
@@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.avatar
import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.storage.FileStorage
import java.io.InputStream
object AvatarPickerStorage {
private const val DIRECTORY = "avatar_picker"
private const val FILENAME_BASE = "avatar"
@JvmStatic
fun read(context: Context, fileName: String) = FileStorage.read(context, DIRECTORY, fileName)
fun save(context: Context, media: Media): Uri {
val fileName = FileStorage.save(context, PartAuthority.getAttachmentStream(context, media.uri), DIRECTORY, FILENAME_BASE, MediaUtil.getExtension(context, media.uri) ?: "")
return PartAuthority.getAvatarPickerUri(fileName)
}
fun save(context: Context, inputStream: InputStream): Uri {
val fileName = FileStorage.save(context, inputStream, DIRECTORY, FILENAME_BASE, MimeTypeMap.getSingleton().getExtensionFromMimeType(MediaUtil.IMAGE_JPEG) ?: "")
return PartAuthority.getAvatarPickerUri(fileName)
}
@JvmStatic
fun cleanOrphans(context: Context) {
val avatarFiles = FileStorage.getAllFiles(context, DIRECTORY, FILENAME_BASE)
val database = SignalDatabase.avatarPicker
val photoAvatars = database
.getAllAvatars()
.filterIsInstance<Avatar.Photo>()
val inDatabaseFileNames = photoAvatars.map { PartAuthority.getAvatarPickerFilename(it.uri) }
val onDiskFileNames = avatarFiles.map { it.name }
val inDatabaseButNotOnDisk = inDatabaseFileNames - onDiskFileNames
val onDiskButNotInDatabase = onDiskFileNames - inDatabaseFileNames
avatarFiles
.filter { onDiskButNotInDatabase.contains(it.name) }
.forEach { it.delete() }
photoAvatars
.filter { inDatabaseButNotOnDisk.contains(PartAuthority.getAvatarPickerFilename(it.uri)) }
.forEach { database.deleteAvatar(it) }
}
}
@@ -0,0 +1,133 @@
package org.thoughtcrime.securesms.avatar
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.appcompat.content.res.AppCompatResources
import com.airbnb.lottie.SimpleColorFilter
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.MediaUtil
import org.whispersystems.libsignal.util.guava.Optional
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import javax.annotation.meta.Exhaustive
/**
* Renders Avatar objects into Media objects. This can involve creating a Bitmap, depending on the
* type of Avatar passed to `renderAvatar`
*/
object AvatarRenderer {
val DIMENSIONS = AvatarHelper.AVATAR_DIMENSIONS
fun getTypeface(context: Context): Typeface {
return Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
}
fun renderAvatar(context: Context, avatar: Avatar, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
@Exhaustive
when (avatar) {
is Avatar.Resource -> renderResource(context, avatar, onAvatarRendered, onRenderFailed)
is Avatar.Vector -> renderVector(context, avatar, onAvatarRendered, onRenderFailed)
is Avatar.Photo -> renderPhoto(context, avatar, onAvatarRendered)
is Avatar.Text -> renderText(context, avatar, onAvatarRendered, onRenderFailed)
}
}
@JvmStatic
fun createTextDrawable(
context: Context,
avatar: Avatar.Text,
inverted: Boolean = false,
size: Int = DIMENSIONS,
synchronous: Boolean = false
): Drawable {
return TextAvatarDrawable(context, avatar, inverted, size, synchronous)
}
private fun renderVector(context: Context, avatar: Avatar.Vector, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas ->
val drawableResourceId = Avatars.getDrawableResource(avatar.key) ?: return@renderInBackground Result.failure(Exception("Drawable resource for key ${avatar.key} does not exist."))
val vector: Drawable = requireNotNull(AppCompatResources.getDrawable(context, drawableResourceId))
vector.setBounds(0, 0, DIMENSIONS, DIMENSIONS)
canvas.drawColor(avatar.color.backgroundColor)
vector.draw(canvas)
Result.success(Unit)
}
}
private fun renderText(context: Context, avatar: Avatar.Text, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas ->
val textDrawable = createTextDrawable(context, avatar, synchronous = true)
canvas.drawColor(avatar.color.backgroundColor)
textDrawable.draw(canvas)
Result.success(Unit)
}
}
private fun renderPhoto(context: Context, avatar: Avatar.Photo, onAvatarRendered: (Media) -> Unit) {
SignalExecutors.BOUNDED.execute {
val blob = BlobProvider.getInstance()
.forData(AvatarPickerStorage.read(context, PartAuthority.getAvatarPickerFilename(avatar.uri)), avatar.size)
.createForSingleSessionOnDisk(context)
onAvatarRendered(createMedia(blob, avatar.size))
}
}
private fun renderResource(context: Context, avatar: Avatar.Resource, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit) {
renderInBackground(context, onAvatarRendered, onRenderFailed) { canvas ->
val resource: Drawable = requireNotNull(AppCompatResources.getDrawable(context, avatar.resourceId))
resource.colorFilter = SimpleColorFilter(avatar.color.foregroundColor)
val padding = (DIMENSIONS * 0.2).toInt()
resource.setBounds(0 + padding, 0 + padding, DIMENSIONS - padding, DIMENSIONS - padding)
canvas.drawColor(avatar.color.backgroundColor)
resource.draw(canvas)
Result.success(Unit)
}
}
private fun renderInBackground(context: Context, onAvatarRendered: (Media) -> Unit, onRenderFailed: (Throwable?) -> Unit, drawAvatar: (Canvas) -> Result<Unit>) {
SignalExecutors.BOUNDED.execute {
val canvasBitmap = Bitmap.createBitmap(DIMENSIONS, DIMENSIONS, Bitmap.Config.ARGB_8888)
val canvas = Canvas(canvasBitmap)
val drawResult = drawAvatar(canvas)
if (drawResult.isFailure) {
canvasBitmap.recycle()
onRenderFailed(drawResult.exceptionOrNull())
}
val outStream = ByteArrayOutputStream()
val compressed = canvasBitmap.compress(Bitmap.CompressFormat.JPEG, 80, outStream)
canvasBitmap.recycle()
if (!compressed) {
onRenderFailed(IOException("Failed to compress bitmap"))
return@execute
}
val bytes = outStream.toByteArray()
val inStream = ByteArrayInputStream(bytes)
val uri = BlobProvider.getInstance().forData(inStream, bytes.size.toLong()).createForSingleSessionOnDisk(context)
onAvatarRendered(createMedia(uri, bytes.size.toLong()))
}
}
private fun createMedia(uri: Uri, size: Long): Media {
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.absent(), Optional.absent(), Optional.absent())
}
}
@@ -0,0 +1,153 @@
package org.thoughtcrime.securesms.avatar
import android.content.Context
import android.graphics.Paint
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.Px
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import kotlin.math.abs
import kotlin.math.min
object Avatars {
/**
* Enum class mirroring AvatarColors codes but utilizing foreground colors for text or icon tinting.
*/
enum class ForegroundColor(private val code: String, @ColorInt val colorInt: Int) {
A100("A100", 0xFF3838F5.toInt()),
A110("A110", 0xFF1251D3.toInt()),
A120("A120", 0xFF086DA0.toInt()),
A130("A130", 0xFF067906.toInt()),
A140("A140", 0xFF661AFF.toInt()),
A150("A150", 0xFF9F00F0.toInt()),
A160("A160", 0xFFB8057C.toInt()),
A170("A170", 0xFFBE0404.toInt()),
A180("A180", 0xFF836B01.toInt()),
A190("A190", 0xFF7D6F40.toInt()),
A200("A200", 0xFF4F4F6D.toInt()),
A210("A210", 0xFF5C5C5C.toInt());
fun deserialize(code: String): ForegroundColor {
return values().find { it.code == code } ?: throw IllegalArgumentException()
}
fun serialize(): String = code
}
/**
* Mapping which associates color codes to ColorPair objects containing background and foreground colors.
*/
val colorMap: Map<String, ColorPair> = ForegroundColor.values().map {
ColorPair(AvatarColor.deserialize(it.serialize()), it)
}.associateBy {
it.code
}
val colors: List<ColorPair> = colorMap.values.toList()
val defaultAvatarsForSelf = linkedMapOf(
"avatar_abstract_01" to DefaultAvatar(R.drawable.ic_avatar_abstract_01, "A130"),
"avatar_abstract_02" to DefaultAvatar(R.drawable.ic_avatar_abstract_02, "A120"),
"avatar_abstract_03" to DefaultAvatar(R.drawable.ic_avatar_abstract_03, "A170"),
"avatar_cat" to DefaultAvatar(R.drawable.ic_avatar_cat, "A190"),
"avatar_dog" to DefaultAvatar(R.drawable.ic_avatar_dog, "A140"),
"avatar_fox" to DefaultAvatar(R.drawable.ic_avatar_fox, "A190"),
"avatar_tucan" to DefaultAvatar(R.drawable.ic_avatar_tucan, "A120"),
"avatar_sloth" to DefaultAvatar(R.drawable.ic_avatar_sloth, "A160"),
"avatar_dinosaur" to DefaultAvatar(R.drawable.ic_avatar_dinosour, "A130"),
"avatar_pig" to DefaultAvatar(R.drawable.ic_avatar_pig, "A180"),
"avatar_incognito" to DefaultAvatar(R.drawable.ic_avatar_incognito, "A220"),
"avatar_ghost" to DefaultAvatar(R.drawable.ic_avatar_ghost, "A100")
)
val defaultAvatarsForGroup = linkedMapOf(
"avatar_heart" to DefaultAvatar(R.drawable.ic_avatar_heart, "A180"),
"avatar_house" to DefaultAvatar(R.drawable.ic_avatar_house, "A120"),
"avatar_melon" to DefaultAvatar(R.drawable.ic_avatar_melon, "A110"),
"avatar_drink" to DefaultAvatar(R.drawable.ic_avatar_drink, "A170"),
"avatar_celebration" to DefaultAvatar(R.drawable.ic_avatar_celebration, "A100"),
"avatar_balloon" to DefaultAvatar(R.drawable.ic_avatar_balloon, "A220"),
"avatar_book" to DefaultAvatar(R.drawable.ic_avatar_book, "A100"),
"avatar_briefcase" to DefaultAvatar(R.drawable.ic_avatar_briefcase, "A180"),
"avatar_sunset" to DefaultAvatar(R.drawable.ic_avatar_sunset, "A120"),
"avatar_surfboard" to DefaultAvatar(R.drawable.ic_avatar_surfboard, "A110"),
"avatar_soccerball" to DefaultAvatar(R.drawable.ic_avatar_soccerball, "A130"),
"avatar_football" to DefaultAvatar(R.drawable.ic_avatar_football, "A220"),
)
@DrawableRes
fun getDrawableResource(key: String): Int? {
val defaultAvatar = defaultAvatarsForSelf.getOrDefault(key, defaultAvatarsForGroup[key])
return defaultAvatar?.vectorDrawableId
}
private fun textPaint(context: Context) = Paint().apply {
isAntiAlias = true
typeface = AvatarRenderer.getTypeface(context)
textSize = 1f
}
/**
* Calculate the text size for a give string using a maximum desired width and a maximum desired font size.
*/
@JvmStatic
fun getTextSizeForLength(context: Context, text: String, @Px maxWidth: Float, @Px maxSize: Float): Float {
val paint = textPaint(context)
return branchSizes(0f, maxWidth / 2, maxWidth, maxSize, paint, text)
}
/**
* Uses binary search to determine optimal font size to within 1% given the input parameters.
*/
private fun branchSizes(@Px lastFontSize: Float, @Px fontSize: Float, @Px target: Float, @Px maxFontSize: Float, paint: Paint, text: String): Float {
paint.textSize = fontSize
val textWidth = paint.measureText(text)
val delta = abs(lastFontSize - fontSize) / 2f
val isWithinThreshold = abs(1f - (textWidth / target)) <= 0.01f
if (textWidth == 0f) {
return maxFontSize
}
if (delta == 0f) {
return min(maxFontSize, fontSize)
}
return when {
fontSize >= maxFontSize -> {
maxFontSize
}
isWithinThreshold -> {
fontSize
}
textWidth > target -> {
branchSizes(fontSize, fontSize - delta, target, maxFontSize, paint, text)
}
else -> {
branchSizes(fontSize, fontSize + delta, target, maxFontSize, paint, text)
}
}
}
@JvmStatic
fun getForegroundColor(avatarColor: AvatarColor): ForegroundColor {
return ForegroundColor.values().firstOrNull { it.serialize() == avatarColor.serialize() } ?: ForegroundColor.A210
}
data class DefaultAvatar(
@DrawableRes val vectorDrawableId: Int,
val colorCode: String
)
data class ColorPair(
val backgroundAvatarColor: AvatarColor,
val foregroundAvatarColor: ForegroundColor
) {
@ColorInt val backgroundColor: Int = backgroundAvatarColor.colorInt()
@ColorInt val foregroundColor: Int = foregroundAvatarColor.colorInt
val code: String = backgroundAvatarColor.serialize()
}
}
@@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.avatar
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.text.Layout
import android.text.SpannableString
import android.text.StaticLayout
import android.text.TextPaint
import androidx.core.graphics.withTranslation
import org.thoughtcrime.securesms.components.emoji.EmojiProvider
class TextAvatarDrawable(
private val context: Context,
private val avatar: Avatar.Text,
inverted: Boolean = false,
private val size: Int = AvatarRenderer.DIMENSIONS,
private val synchronous: Boolean = false
) : Drawable() {
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
init {
textPaint.typeface = AvatarRenderer.getTypeface(context)
textPaint.color = if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor
textPaint.density = context.resources.displayMetrics.density
setBounds(0, 0, size, size)
}
override fun draw(canvas: Canvas) {
val textSize = Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f)
val width = bounds.width()
val candidates = EmojiProvider.getCandidates(avatar.text)
var hasEmoji = false
textPaint.textSize = textSize
val newText = if (candidates == null || candidates.size() == 0) {
SpannableString(avatar.text)
} else {
EmojiProvider.emojify(context, candidates, avatar.text, textPaint, synchronous)
}
if (newText == null) return
val layout = StaticLayout(SpannableString(newText), textPaint, width, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true)
layout.draw(canvas, getStartX(layout), ((bounds.height() / 2) - ((layout.height / 2))).toFloat())
}
private fun getStartX(layout: StaticLayout): Float {
val direction = layout.getParagraphDirection(0)
val lineWidth = layout.getLineWidth(0)
val width = bounds.width()
val xPos = (width - lineWidth) / 2
return if (direction == Layout.DIR_LEFT_TO_RIGHT) xPos else -xPos
}
override fun setAlpha(alpha: Int) = Unit
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
override fun getOpacity(): Int = PixelFormat.OPAQUE
private fun Layout.draw(canvas: Canvas, x: Float, y: Float) {
canvas.withTranslation(x, y) {
draw(canvas)
}
}
}
@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.avatar.photo
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.fragment.app.setFragmentResult
import androidx.navigation.Navigation
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.AvatarBundler
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), ImageEditorFragment.Controller {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val args = PhotoEditorFragmentArgs.fromBundle(requireArguments())
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
val imageEditorFragment = ImageEditorFragment.newInstanceForAvatarEdit(photo.uri)
childFragmentManager.commit {
add(R.id.fragment_container, imageEditorFragment, IMAGE_EDITOR)
}
}
override fun onTouchEventsNeeded(needed: Boolean) {
}
override fun onRequestFullScreen(fullScreen: Boolean, hideKeyboard: Boolean) {
}
override fun onDoneEditing() {
val args = PhotoEditorFragmentArgs.fromBundle(requireArguments())
val applicationContext = requireContext().applicationContext
val imageEditorFragment: ImageEditorFragment = childFragmentManager.findFragmentByTag(IMAGE_EDITOR) as ImageEditorFragment
SignalExecutors.BOUNDED.execute {
val editedImageUri = imageEditorFragment.renderToSingleUseBlob()
val size = BlobProvider.getFileSize(editedImageUri) ?: 0
val inputStream = BlobProvider.getInstance().getStream(applicationContext, editedImageUri)
val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream)
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
val database = SignalDatabase.avatarPicker
val newPhoto = photo.copy(uri = onDiskUri, size = size)
database.update(newPhoto)
BlobProvider.getInstance().delete(requireContext(), photo.uri)
ThreadUtil.runOnMain {
setFragmentResult(REQUEST_KEY_EDIT, AvatarBundler.bundlePhoto(newPhoto))
Navigation.findNavController(requireView()).popBackStack()
}
}
}
override fun onCancelEditing() {
Navigation.findNavController(requireView()).popBackStack()
}
override fun onMainImageLoaded() {
}
override fun onMainImageFailedToLoad() {
}
companion object {
const val REQUEST_KEY_EDIT = "org.thoughtcrime.securesms.avatar.photo.EDIT"
private const val IMAGE_EDITOR = "image_editor"
}
}
@@ -0,0 +1,247 @@
package org.thoughtcrime.securesms.avatar.picker
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.View
import android.widget.PopupMenu
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarBundler
import org.thoughtcrime.securesms.avatar.photo.PhotoEditorFragment
import org.thoughtcrime.securesms.avatar.text.TextAvatarCreationFragment
import org.thoughtcrime.securesms.avatar.vector.VectorAvatarCreationFragment
import org.thoughtcrime.securesms.components.ButtonStripItemView
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
import org.thoughtcrime.securesms.groups.ParcelableGroupId
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.visible
/**
* Primary Avatar picker fragment, displays current user avatar and a list of recently used avatars and defaults.
*/
class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
companion object {
const val REQUEST_KEY_SELECT_AVATAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR"
const val SELECT_AVATAR_MEDIA = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_MEDIA"
const val SELECT_AVATAR_CLEAR = "org.thoughtcrime.securesms.avatar.picker.SELECT_AVATAR_CLEAR"
private const val REQUEST_CODE_SELECT_IMAGE = 1
}
private val viewModel: AvatarPickerViewModel by viewModels(factoryProducer = this::createFactory)
private lateinit var recycler: RecyclerView
private fun createFactory(): AvatarPickerViewModel.Factory {
val args = AvatarPickerFragmentArgs.fromBundle(requireArguments())
val groupId = ParcelableGroupId.get(args.groupId)
return AvatarPickerViewModel.Factory(AvatarPickerRepository(requireContext()), groupId, args.isNewGroup, args.groupAvatarMedia)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar = view.findViewById(R.id.avatar_picker_toolbar)
val cameraButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_camera)
val photoButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_photo)
val textButton: ButtonStripItemView = view.findViewById(R.id.avatar_picker_text)
val saveButton: View = view.findViewById(R.id.avatar_picker_save)
val clearButton: View = view.findViewById(R.id.avatar_picker_clear)
recycler = view.findViewById(R.id.avatar_picker_recycler)
recycler.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(16)))
val adapter = MappingAdapter()
AvatarPickerItem.register(adapter, this::onAvatarClick, this::onAvatarLongClick)
recycler.adapter = adapter
val avatarViewHolder = AvatarPickerItem.ViewHolder(view)
viewModel.state.observe(viewLifecycleOwner) { state ->
if (state.currentAvatar != null) {
avatarViewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false))
}
clearButton.visible = state.canClear
val wasEnabled = saveButton.isEnabled
saveButton.isEnabled = state.canSave
if (wasEnabled != state.canSave) {
val alpha = if (state.canSave) 1f else 0.5f
saveButton.animate().cancel()
saveButton.animate().alpha(alpha)
}
val items = state.selectableAvatars.map { AvatarPickerItem.Model(it, it == state.currentAvatar) }
val selectedPosition = items.indexOfFirst { it.isSelected }
adapter.submitList(items) {
if (selectedPosition > -1)
recycler.smoothScrollToPosition(selectedPosition)
}
}
toolbar.setNavigationOnClickListener { Navigation.findNavController(it).popBackStack() }
cameraButton.setOnIconClickedListener { openCameraCapture() }
photoButton.setOnIconClickedListener { openGallery() }
textButton.setOnIconClickedListener { openTextEditor(null) }
saveButton.setOnClickListener { v ->
viewModel.save(
{
setFragmentResult(
REQUEST_KEY_SELECT_AVATAR,
Bundle().apply {
putParcelable(SELECT_AVATAR_MEDIA, it)
}
)
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
},
{
setFragmentResult(
REQUEST_KEY_SELECT_AVATAR,
Bundle().apply {
putBoolean(SELECT_AVATAR_CLEAR, true)
}
)
ThreadUtil.runOnMain { Navigation.findNavController(v).popBackStack() }
}
)
}
clearButton.setOnClickListener { viewModel.clear() }
setFragmentResultListener(TextAvatarCreationFragment.REQUEST_KEY_TEXT) { _, bundle ->
val text = AvatarBundler.extractText(bundle)
viewModel.onAvatarEditCompleted(text)
}
setFragmentResultListener(VectorAvatarCreationFragment.REQUEST_KEY_VECTOR) { _, bundle ->
val vector = AvatarBundler.extractVector(bundle)
viewModel.onAvatarEditCompleted(vector)
}
setFragmentResultListener(PhotoEditorFragment.REQUEST_KEY_EDIT) { _, bundle ->
val photo = AvatarBundler.extractPhoto(bundle)
viewModel.onAvatarEditCompleted(photo)
}
}
override fun onResume() {
super.onResume()
ViewUtil.hideKeyboard(requireContext(), requireView())
}
@Suppress("DEPRECATION")
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.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA))
viewModel.onAvatarPhotoSelectionCompleted(media)
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun onAvatarClick(avatar: Avatar, isSelected: Boolean) {
if (isSelected) {
openEditor(avatar)
} else {
viewModel.onAvatarSelectedFromGrid(avatar)
}
}
private fun onAvatarLongClick(anchorView: View, avatar: Avatar): Boolean {
val menuRes = when (avatar) {
is Avatar.Photo -> R.menu.avatar_picker_context
is Avatar.Text -> R.menu.avatar_picker_context
is Avatar.Vector -> return true
is Avatar.Resource -> return true
}
val popup = PopupMenu(context, anchorView, Gravity.TOP)
popup.menuInflater.inflate(menuRes, popup.menu)
popup.setOnMenuItemClickListener { menuItem ->
when (menuItem.itemId) {
R.id.action_delete -> viewModel.delete(avatar)
}
true
}
popup.show()
return true
}
fun openEditor(avatar: Avatar) {
when (avatar) {
is Avatar.Photo -> openPhotoEditor(avatar)
is Avatar.Resource -> throw UnsupportedOperationException()
is Avatar.Text -> openTextEditor(avatar)
is Avatar.Vector -> openVectorEditor(avatar)
}
}
private fun openPhotoEditor(photo: Avatar.Photo) {
Navigation.findNavController(requireView())
.safeNavigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo)))
}
private fun openVectorEditor(vector: Avatar.Vector) {
Navigation.findNavController(requireView())
.safeNavigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector)))
}
private fun openTextEditor(text: Avatar.Text?) {
val bundle = if (text != null) AvatarBundler.bundleText(text) else null
Navigation.findNavController(requireView())
.safeNavigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle))
}
@Suppress("DEPRECATION")
private fun openCameraCapture() {
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.onAllGranted {
val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
}
.onAnyDenied {
Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT)
.show()
}
.execute()
}
@Suppress("DEPRECATION")
private fun openGallery() {
Permissions.with(this)
.request(Manifest.permission.READ_EXTERNAL_STORAGE)
.ifNecessary()
.onAllGranted {
val intent = AvatarSelectionActivity.getIntentForGallery(requireContext())
startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE)
}
.onAnyDenied {
Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__viewing_your_gallery_requires_the_storage_permission, Toast.LENGTH_SHORT)
.show()
}
.execute()
}
}
@@ -0,0 +1,148 @@
package org.thoughtcrime.securesms.avatar.picker
import android.util.TypedValue
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.setPadding
import com.airbnb.lottie.SimpleColorFilter
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import org.thoughtcrime.securesms.util.visible
typealias OnAvatarClickListener = (Avatar, Boolean) -> Unit
typealias OnAvatarLongClickListener = (View, Avatar) -> Boolean
object AvatarPickerItem {
private val SELECTION_CHANGED = Any()
fun register(adapter: MappingAdapter, onAvatarClickListener: OnAvatarClickListener, onAvatarLongClickListener: OnAvatarLongClickListener) {
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onAvatarClickListener, onAvatarLongClickListener) }, R.layout.avatar_picker_item))
}
class Model(val avatar: Avatar, val isSelected: Boolean) : MappingModel<Model> {
override fun areItemsTheSame(newItem: Model): Boolean = avatar.isSameAs(newItem.avatar)
override fun areContentsTheSame(newItem: Model): Boolean = avatar == newItem.avatar && isSelected == newItem.isSelected
override fun getChangePayload(newItem: Model): Any? {
return if (newItem.avatar == avatar && isSelected != newItem.isSelected) {
SELECTION_CHANGED
} else {
null
}
}
}
class ViewHolder(
itemView: View,
private val onAvatarClickListener: OnAvatarClickListener? = null,
private val onAvatarLongClickListener: OnAvatarLongClickListener? = null
) : MappingViewHolder<Model>(itemView) {
private val imageView: ImageView = itemView.findViewById(R.id.avatar_picker_item_image)
private val textView: TextView = itemView.findViewById(R.id.avatar_picker_item_text)
private val selectedFader: View? = itemView.findViewById(R.id.avatar_picker_item_fader)
private val selectedOverlay: View? = itemView.findViewById(R.id.avatar_picker_item_selection_overlay)
init {
textView.typeface = AvatarRenderer.getTypeface(context)
textView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
updateFontSize(textView.text.toString())
}
}
private fun updateFontSize(text: String) {
val textSize = Avatars.getTextSizeForLength(context, text, textView.measuredWidth * 0.8f, textView.measuredHeight * 0.45f)
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize)
if (textView !is EditText) {
textView.text = text
}
}
override fun bind(model: Model) {
val alpha = if (model.isSelected) 1f else 0f
val scale = if (model.isSelected) 0.9f else 1f
imageView.animate().cancel()
textView.animate().cancel()
selectedOverlay?.animate()?.cancel()
selectedFader?.animate()?.cancel()
itemView.setOnLongClickListener {
onAvatarLongClickListener?.invoke(itemView, model.avatar) ?: false
}
itemView.setOnClickListener { onAvatarClickListener?.invoke(model.avatar, model.isSelected) }
if (payload.isNotEmpty() && payload.contains(SELECTION_CHANGED)) {
imageView.animate().scaleX(scale).scaleY(scale)
textView.animate().scaleX(scale).scaleY(scale)
selectedOverlay?.animate()?.alpha(alpha)
selectedFader?.animate()?.alpha(alpha)
return
}
imageView.scaleX = scale
imageView.scaleY = scale
textView.scaleX = scale
textView.scaleY = scale
selectedFader?.alpha = alpha
selectedOverlay?.alpha = alpha
imageView.clearColorFilter()
imageView.setPadding(0)
when (model.avatar) {
is Avatar.Text -> {
textView.visible = true
updateFontSize(model.avatar.text)
if (textView.text.toString() != model.avatar.text) {
textView.text = model.avatar.text
}
imageView.setImageDrawable(null)
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
textView.setTextColor(model.avatar.color.foregroundColor)
}
is Avatar.Vector -> {
textView.visible = false
val drawableId = Avatars.getDrawableResource(model.avatar.key)
if (drawableId == null) {
imageView.setImageDrawable(null)
} else {
imageView.setImageDrawable(AppCompatResources.getDrawable(context, drawableId))
}
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
}
is Avatar.Photo -> {
textView.visible = false
GlideApp.with(imageView).load(DecryptableStreamUriLoader.DecryptableUri(model.avatar.uri)).into(imageView)
}
is Avatar.Resource -> {
imageView.setPadding((imageView.width * 0.2).toInt())
textView.visible = false
GlideApp.with(imageView).clear(imageView)
imageView.setImageResource(model.avatar.resourceId)
imageView.colorFilter = SimpleColorFilter(model.avatar.color.foregroundColor)
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
}
}
}
}
}
@@ -0,0 +1,189 @@
package org.thoughtcrime.securesms.avatar.picker
import android.content.Context
import android.net.Uri
import android.widget.Toast
import io.reactivex.rxjava3.core.Single
import org.signal.core.util.StreamUtil
import org.signal.core.util.ThreadUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.NameUtil
import org.whispersystems.signalservice.api.util.StreamDetails
import java.io.IOException
private val TAG = Log.tag(AvatarPickerRepository::class.java)
class AvatarPickerRepository(context: Context) {
private val applicationContext = context.applicationContext
fun getAvatarForSelf(): Single<Avatar> = Single.fromCallable {
val details: StreamDetails? = AvatarHelper.getSelfProfileAvatarStream(applicationContext)
if (details != null) {
try {
val bytes = StreamUtil.readFully(details.stream)
Avatar.Photo(
BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(),
details.length,
Avatar.DatabaseId.DoNotPersist
)
} catch (e: IOException) {
Log.w(TAG, "Failed to read avatar!")
getDefaultAvatarForSelf()
}
} else {
getDefaultAvatarForSelf()
}
}
fun getAvatarForGroup(groupId: GroupId): Single<Avatar> = Single.fromCallable {
val recipient = Recipient.externalGroupExact(applicationContext, groupId)
if (AvatarHelper.hasAvatar(applicationContext, recipient.id)) {
try {
val bytes = AvatarHelper.getAvatarBytes(applicationContext, recipient.id)
Avatar.Photo(
BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(),
AvatarHelper.getAvatarLength(applicationContext, recipient.id),
Avatar.DatabaseId.DoNotPersist
)
} catch (e: IOException) {
Log.w(TAG, "Failed to read group avatar!")
getDefaultAvatarForGroup(recipient.avatarColor)
}
} else {
getDefaultAvatarForGroup(recipient.avatarColor)
}
}
fun getPersistedAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
SignalDatabase.avatarPicker.getAvatarsForSelf()
}
fun getPersistedAvatarsForGroup(groupId: GroupId): Single<List<Avatar>> = Single.fromCallable {
SignalDatabase.avatarPicker.getAvatarsForGroup(groupId)
}
fun getDefaultAvatarsForSelf(): Single<List<Avatar>> = Single.fromCallable {
Avatars.defaultAvatarsForSelf.entries.mapIndexed { index, entry ->
Avatar.Vector(entry.key, color = Avatars.colors[index % Avatars.colors.size], Avatar.DatabaseId.NotSet)
}
}
fun getDefaultAvatarsForGroup(): Single<List<Avatar>> = Single.fromCallable {
Avatars.defaultAvatarsForGroup.entries.mapIndexed { index, entry ->
Avatar.Vector(entry.key, color = Avatars.colors[index % Avatars.colors.size], Avatar.DatabaseId.NotSet)
}
}
fun writeMediaToMultiSessionStorage(media: Media, onMediaWrittenToMultiSessionStorage: (Uri) -> Unit) {
SignalExecutors.BOUNDED.execute {
onMediaWrittenToMultiSessionStorage(AvatarPickerStorage.save(applicationContext, media))
}
}
fun persistAvatarForSelf(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
SignalExecutors.BOUNDED.execute {
val avatarDatabase = SignalDatabase.avatarPicker
val savedAvatar = avatarDatabase.saveAvatarForSelf(avatar)
avatarDatabase.markUsage(savedAvatar)
onPersisted(savedAvatar)
}
}
fun persistAvatarForGroup(avatar: Avatar, groupId: GroupId, onPersisted: (Avatar) -> Unit) {
SignalExecutors.BOUNDED.execute {
val avatarDatabase = SignalDatabase.avatarPicker
val savedAvatar = avatarDatabase.saveAvatarForGroup(avatar, groupId)
avatarDatabase.markUsage(savedAvatar)
onPersisted(savedAvatar)
}
}
fun persistAndCreateMediaForSelf(avatar: Avatar, onSaved: (Media) -> Unit) {
SignalExecutors.BOUNDED.execute {
if (avatar.databaseId !is Avatar.DatabaseId.DoNotPersist) {
persistAvatarForSelf(avatar) {
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
}
} else {
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
}
}
}
fun persistAndCreateMediaForGroup(avatar: Avatar, groupId: GroupId, onSaved: (Media) -> Unit) {
SignalExecutors.BOUNDED.execute {
if (avatar.databaseId !is Avatar.DatabaseId.DoNotPersist) {
persistAvatarForGroup(avatar, groupId) {
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
}
} else {
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
}
}
}
fun createMediaForNewGroup(avatar: Avatar, onSaved: (Media) -> Unit) {
SignalExecutors.BOUNDED.execute {
AvatarRenderer.renderAvatar(applicationContext, avatar, onSaved, this::handleRenderFailure)
}
}
fun handleRenderFailure(throwable: Throwable?) {
Log.w(TAG, "Failed to render avatar.", throwable)
ThreadUtil.postToMain {
Toast.makeText(applicationContext, R.string.AvatarPickerRepository__failed_to_save_avatar, Toast.LENGTH_SHORT).show()
}
}
fun getDefaultAvatarForSelf(): Avatar {
val initials = NameUtil.getAbbreviation(Recipient.self().getDisplayName(applicationContext))
return if (initials.isNullOrBlank()) {
Avatar.getDefaultForSelf()
} else {
Avatar.Text(initials, requireNotNull(Avatars.colorMap[Recipient.self().avatarColor.serialize()]), Avatar.DatabaseId.DoNotPersist)
}
}
fun getDefaultAvatarForGroup(groupId: GroupId): Avatar {
val recipient = Recipient.externalGroupExact(applicationContext, groupId)
return getDefaultAvatarForGroup(recipient.avatarColor)
}
fun getDefaultAvatarForGroup(color: AvatarColor?): Avatar {
val colorPair = Avatars.colorMap[color?.serialize()]
val defaultColor = Avatar.getDefaultForGroup()
return if (colorPair != null) {
defaultColor.copy(color = colorPair)
} else {
defaultColor
}
}
fun delete(avatar: Avatar, onDelete: () -> Unit) {
SignalExecutors.BOUNDED.execute {
if (avatar.databaseId is Avatar.DatabaseId.Saved) {
val avatarDatabase = SignalDatabase.avatarPicker
avatarDatabase.deleteAvatar(avatar)
}
onDelete()
}
}
}
@@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.avatar.picker
import org.thoughtcrime.securesms.avatar.Avatar
data class AvatarPickerState(
val currentAvatar: Avatar? = null,
val selectableAvatars: List<Avatar> = listOf(),
val canSave: Boolean = false,
val canClear: Boolean = false,
val isCleared: Boolean = false
)
@@ -0,0 +1,198 @@
package org.thoughtcrime.securesms.avatar.picker
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.util.livedata.Store
sealed class AvatarPickerViewModel(private val repository: AvatarPickerRepository) : ViewModel() {
private val disposables = CompositeDisposable()
private val store = Store(AvatarPickerState())
val state: LiveData<AvatarPickerState> = store.stateLiveData
protected abstract fun getAvatar(): Single<Avatar>
protected abstract fun getDefaultAvatarFromRepository(): Avatar
protected abstract fun getPersistedAvatars(): Single<List<Avatar>>
protected abstract fun getDefaultAvatars(): Single<List<Avatar>>
protected abstract fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit)
protected abstract fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit)
fun delete(avatar: Avatar) {
repository.delete(avatar) {
refreshAvatar()
refreshSelectableAvatars()
}
}
fun clear() {
store.update {
val avatar = getDefaultAvatarFromRepository()
it.copy(currentAvatar = avatar, canSave = true, canClear = false, isCleared = true)
}
}
fun save(onSaved: (Media) -> Unit, onCleared: () -> Unit) {
if (store.state.isCleared) {
onCleared()
} else {
val avatar = store.state.currentAvatar ?: throw AssertionError()
persistAndCreateMedia(avatar, onSaved)
}
}
fun onAvatarSelectedFromGrid(avatar: Avatar) {
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true, isCleared = false) }
}
fun onAvatarEditCompleted(avatar: Avatar) {
persistAvatar(avatar) { saved ->
store.update { it.copy(currentAvatar = saved, canSave = isSaveable(saved), canClear = true, isCleared = false) }
refreshSelectableAvatars()
}
}
fun onAvatarPhotoSelectionCompleted(media: Media) {
repository.writeMediaToMultiSessionStorage(media) { multiSessionUri ->
persistAvatar(Avatar.Photo(multiSessionUri, media.size, Avatar.DatabaseId.NotSet)) { avatar ->
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = true, isCleared = false) }
refreshSelectableAvatars()
}
}
}
protected fun refreshAvatar() {
disposables.add(
getAvatar().subscribeOn(Schedulers.io()).subscribe { avatar ->
store.update { it.copy(currentAvatar = avatar, canSave = isSaveable(avatar), canClear = avatar is Avatar.Photo && !isSaveable(avatar), isCleared = false) }
}
)
}
protected fun refreshSelectableAvatars() {
disposables.add(
Single.zip(getPersistedAvatars(), getDefaultAvatars()) { custom, def ->
val customKeys = custom.filterIsInstance(Avatar.Vector::class.java).map { it.key }
custom + def.filterNot {
it is Avatar.Vector && customKeys.contains(it.key)
}
}.subscribeOn(Schedulers.io()).subscribe { avatars ->
store.update { it.copy(selectableAvatars = avatars) }
}
)
}
private fun isSaveable(avatar: Avatar) = avatar.databaseId != Avatar.DatabaseId.DoNotPersist
override fun onCleared() {
disposables.dispose()
}
private class SelfAvatarPickerViewModel(private val repository: AvatarPickerRepository) : AvatarPickerViewModel(repository) {
init {
refreshAvatar()
refreshSelectableAvatars()
}
override fun getAvatar(): Single<Avatar> = repository.getAvatarForSelf()
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForSelf()
override fun getPersistedAvatars(): Single<List<Avatar>> = repository.getPersistedAvatarsForSelf()
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForSelf()
override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
repository.persistAvatarForSelf(avatar, onPersisted)
}
override fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) {
repository.persistAndCreateMediaForSelf(avatar, onSaved)
}
}
private class GroupAvatarPickerViewModel(
private val groupId: GroupId,
private val repository: AvatarPickerRepository,
groupAvatarMedia: Media?
) : AvatarPickerViewModel(repository) {
private val initialAvatar: Avatar? = groupAvatarMedia?.let { Avatar.Photo(it.uri, it.size, Avatar.DatabaseId.DoNotPersist) }
init {
refreshAvatar()
refreshSelectableAvatars()
}
override fun getAvatar(): Single<Avatar> {
return if (initialAvatar != null) {
Single.just(initialAvatar)
} else {
repository.getAvatarForGroup(groupId)
}
}
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup(groupId)
override fun getPersistedAvatars(): Single<List<Avatar>> = repository.getPersistedAvatarsForGroup(groupId)
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) {
repository.persistAvatarForGroup(avatar, groupId, onPersisted)
}
override fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) {
repository.persistAndCreateMediaForGroup(avatar, groupId, onSaved)
}
}
private class NewGroupAvatarPickerViewModel(
private val repository: AvatarPickerRepository,
initialMedia: Media?
) : AvatarPickerViewModel(repository) {
private val initialAvatar: Avatar? = initialMedia?.let { Avatar.Photo(it.uri, it.size, Avatar.DatabaseId.DoNotPersist) }
init {
refreshAvatar()
refreshSelectableAvatars()
}
override fun getAvatar(): Single<Avatar> {
return if (initialAvatar != null) {
Single.just(initialAvatar)
} else {
Single.fromCallable { getDefaultAvatarFromRepository() }
}
}
override fun getDefaultAvatarFromRepository(): Avatar = repository.getDefaultAvatarForGroup(null)
override fun getPersistedAvatars(): Single<List<Avatar>> = Single.just(listOf())
override fun getDefaultAvatars(): Single<List<Avatar>> = repository.getDefaultAvatarsForGroup()
override fun persistAvatar(avatar: Avatar, onPersisted: (Avatar) -> Unit) = onPersisted(avatar)
override fun persistAndCreateMedia(avatar: Avatar, onSaved: (Media) -> Unit) = repository.createMediaForNewGroup(avatar, onSaved)
}
class Factory(
private val repository: AvatarPickerRepository,
private val groupId: GroupId?,
private val isNewGroup: Boolean,
private val groupAvatarMedia: Media?
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
val viewModel = if (groupId == null && !isNewGroup) {
SelfAvatarPickerViewModel(repository)
} else if (groupId == null) {
NewGroupAvatarPickerViewModel(repository, groupAvatarMedia)
} else {
GroupAvatarPickerViewModel(groupId, repository, groupAvatarMedia)
}
return requireNotNull(modelClass.cast(viewModel))
}
}
}
@@ -0,0 +1,152 @@
package org.thoughtcrime.securesms.avatar.text
import android.os.Bundle
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.appcompat.widget.Toolbar
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.widget.doAfterTextChanged
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.AutoTransition
import androidx.transition.TransitionManager
import com.google.android.material.tabs.TabLayout
import org.signal.core.util.EditTextUtil
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarBundler
import org.thoughtcrime.securesms.avatar.AvatarColorItem
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerItem
import org.thoughtcrime.securesms.components.BoldSelectionTabItem
import org.thoughtcrime.securesms.components.ControllableTabLayout
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
/**
* Fragment to create an avatar based off of a Vector or Text (via a pager)
*/
class TextAvatarCreationFragment : Fragment(R.layout.text_avatar_creation_fragment) {
private val viewModel: TextAvatarCreationViewModel by viewModels(factoryProducer = this::createFactory)
private lateinit var textInput: EditText
private lateinit var recycler: RecyclerView
private lateinit var content: ConstraintLayout
private val withRecyclerSet = ConstraintSet()
private val withoutRecyclerSet = ConstraintSet()
private var hasBoundFromViewModel: Boolean = false
private fun createFactory(): TextAvatarCreationViewModel.Factory {
val args = TextAvatarCreationFragmentArgs.fromBundle(requireArguments())
val textBundle = args.textAvatar
val text = if (textBundle != null) {
AvatarBundler.extractText(textBundle)
} else {
Avatar.Text("", Avatars.colors.random(), Avatar.DatabaseId.NotSet)
}
return TextAvatarCreationViewModel.Factory(text)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar = view.findViewById(R.id.text_avatar_creation_toolbar)
val tabLayout: ControllableTabLayout = view.findViewById(R.id.text_avatar_creation_tabs)
val doneButton: View = view.findViewById(R.id.text_avatar_creation_done)
val keyboardAwareLayout: KeyboardAwareLinearLayout = view.findViewById(R.id.keyboard_aware_layout)
withRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content)
withoutRecyclerSet.load(requireContext(), R.layout.text_avatar_creation_fragment_content_hidden_recycler)
content = view.findViewById(R.id.content)
recycler = view.findViewById(R.id.text_avatar_creation_recycler)
textInput = view.findViewById(R.id.avatar_picker_item_text)
toolbar.setNavigationOnClickListener { Navigation.findNavController(it).popBackStack() }
BoldSelectionTabItem.registerListeners(tabLayout)
val onTabSelectedListener = OnTabSelectedListener()
tabLayout.addOnTabSelectedListener(onTabSelectedListener)
onTabSelectedListener.onTabSelected(requireNotNull(tabLayout.getTabAt(tabLayout.selectedTabPosition)))
val adapter = MappingAdapter()
recycler.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(16)))
AvatarColorItem.registerViewHolder(adapter) {
viewModel.setColor(it)
}
recycler.adapter = adapter
val viewHolder = AvatarPickerItem.ViewHolder(view)
viewModel.state.observe(viewLifecycleOwner) { state ->
EditTextUtil.setCursorColor(textInput, state.currentAvatar.color.foregroundColor)
viewHolder.bind(AvatarPickerItem.Model(state.currentAvatar, false))
adapter.submitList(state.colors().map { AvatarColorItem.Model(it) })
hasBoundFromViewModel = true
}
EditTextUtil.addGraphemeClusterLimitFilter(textInput, 3)
textInput.doAfterTextChanged {
if (it != null && hasBoundFromViewModel) {
viewModel.setText(it.toString())
}
}
doneButton.setOnClickListener { v ->
setFragmentResult(REQUEST_KEY_TEXT, AvatarBundler.bundleText(viewModel.getCurrentAvatar()))
Navigation.findNavController(v).popBackStack()
}
textInput.setOnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
tabLayout.getTabAt(1)?.select()
true
} else {
false
}
}
keyboardAwareLayout.addOnKeyboardHiddenListener {
if (tabLayout.selectedTabPosition == 1) {
val transition = AutoTransition().setStartDelay(250L)
TransitionManager.endTransitions(content)
withRecyclerSet.applyTo(content)
TransitionManager.beginDelayedTransition(content, transition)
}
}
}
private inner class OnTabSelectedListener : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
when (tab.position) {
0 -> {
textInput.isEnabled = true
ViewUtil.focusAndShowKeyboard(textInput)
withoutRecyclerSet.applyTo(content)
textInput.setSelection(textInput.length())
}
1 -> {
textInput.isEnabled = false
ViewUtil.hideKeyboard(requireContext(), textInput)
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabReselected(tab: TabLayout.Tab?) = Unit
}
companion object {
const val REQUEST_KEY_TEXT = "org.thoughtcrime.securesms.avatar.text.TEXT"
}
}
@@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.avatar.text
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarColorItem
import org.thoughtcrime.securesms.avatar.Avatars
data class TextAvatarCreationState(
val currentAvatar: Avatar.Text,
) {
fun colors(): List<AvatarColorItem> = Avatars.colors.map { AvatarColorItem(it, currentAvatar.color == it) }
}
@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.avatar.text
import androidx.lifecycle.LiveData
import androidx.lifecycle.Transformations
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.util.livedata.Store
class TextAvatarCreationViewModel(initialText: Avatar.Text) : ViewModel() {
private val store = Store(TextAvatarCreationState(initialText))
val state: LiveData<TextAvatarCreationState> = Transformations.distinctUntilChanged(store.stateLiveData)
fun setColor(colorPair: Avatars.ColorPair) {
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }
}
fun setText(text: String) {
store.update {
if (it.currentAvatar.text == text) {
it
} else {
it.copy(currentAvatar = it.currentAvatar.copy(text = text))
}
}
}
fun getCurrentAvatar(): Avatar.Text {
return store.state.currentAvatar
}
class Factory(private val initialText: Avatar.Text) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(TextAvatarCreationViewModel(initialText)))
}
}
}
@@ -0,0 +1,64 @@
package org.thoughtcrime.securesms.avatar.vector
import android.os.Bundle
import android.view.View
import android.widget.ImageView
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.fragment.app.setFragmentResult
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.recyclerview.widget.RecyclerView
import com.airbnb.lottie.SimpleColorFilter
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.AvatarBundler
import org.thoughtcrime.securesms.avatar.AvatarColorItem
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
/**
* Fragment to create an avatar based off a default vector.
*/
class VectorAvatarCreationFragment : Fragment(R.layout.vector_avatar_creation_fragment) {
private val viewModel: VectorAvatarCreationViewModel by viewModels(factoryProducer = this::createFactory)
private fun createFactory(): VectorAvatarCreationViewModel.Factory {
val args = VectorAvatarCreationFragmentArgs.fromBundle(requireArguments())
val vectorBundle = args.vectorAvatar
return VectorAvatarCreationViewModel.Factory(AvatarBundler.extractVector(vectorBundle))
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val toolbar: Toolbar = view.findViewById(R.id.vector_avatar_creation_toolbar)
val recycler: RecyclerView = view.findViewById(R.id.vector_avatar_creation_recycler)
val doneButton: View = view.findViewById(R.id.vector_avatar_creation_done)
val preview: ImageView = view.findViewById(R.id.vector_avatar_creation_image)
val adapter = MappingAdapter()
recycler.adapter = adapter
recycler.addItemDecoration(GridDividerDecoration(4, ViewUtil.dpToPx(16)))
AvatarColorItem.registerViewHolder(adapter) {
viewModel.setColor(it)
}
viewModel.state.observe(viewLifecycleOwner) { state ->
preview.background.colorFilter = SimpleColorFilter(state.currentAvatar.color.backgroundColor)
preview.setImageResource(requireNotNull(Avatars.getDrawableResource(state.currentAvatar.key)))
adapter.submitList(state.colors().map { AvatarColorItem.Model(it) })
}
toolbar.setNavigationOnClickListener { Navigation.findNavController(view).popBackStack() }
doneButton.setOnClickListener {
setFragmentResult(REQUEST_KEY_VECTOR, AvatarBundler.bundleVector(viewModel.getCurrentAvatar()))
Navigation.findNavController(it).popBackStack()
}
}
companion object {
const val REQUEST_KEY_VECTOR = "org.thoughtcrime.securesms.avatar.text.VECTOR"
}
}
@@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.avatar.vector
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarColorItem
import org.thoughtcrime.securesms.avatar.Avatars
data class VectorAvatarCreationState(
val currentAvatar: Avatar.Vector,
) {
fun colors(): List<AvatarColorItem> = Avatars.colors.map { AvatarColorItem(it, currentAvatar.color == it) }
}
@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms.avatar.vector
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.util.livedata.Store
class VectorAvatarCreationViewModel(initialAvatar: Avatar.Vector) : ViewModel() {
private val store = Store(VectorAvatarCreationState(initialAvatar))
val state: LiveData<VectorAvatarCreationState> = store.stateLiveData
fun setColor(colorPair: Avatars.ColorPair) {
store.update { it.copy(currentAvatar = it.currentAvatar.copy(color = colorPair)) }
}
fun getCurrentAvatar() = store.state.currentAvatar
class Factory(private val initialAvatar: Avatar.Vector) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(VectorAvatarCreationViewModel(initialAvatar)))
}
}
}
@@ -0,0 +1,30 @@
package org.thoughtcrime.securesms.backup
import org.thoughtcrime.securesms.database.AttachmentDatabase
import org.thoughtcrime.securesms.database.GroupReceiptDatabase
import org.thoughtcrime.securesms.database.MmsDatabase
import org.thoughtcrime.securesms.database.SmsDatabase
/**
* Queries used by backup exporter to estimate total counts for various complicated tables.
*/
object BackupCountQueries {
const val mmsCount: String = "SELECT COUNT(*) FROM ${MmsDatabase.TABLE_NAME} WHERE ${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.VIEW_ONCE} <= 0"
const val smsCount: String = "SELECT COUNT(*) FROM ${SmsDatabase.TABLE_NAME} WHERE ${SmsDatabase.EXPIRES_IN} <= 0"
@get:JvmStatic
val groupReceiptCount: String = """
SELECT COUNT(*) FROM ${GroupReceiptDatabase.TABLE_NAME}
INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${GroupReceiptDatabase.TABLE_NAME}.${GroupReceiptDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID}
WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0
""".trimIndent()
@get:JvmStatic
val attachmentCount: String = """
SELECT COUNT(*) FROM ${AttachmentDatabase.TABLE_NAME}
INNER JOIN ${MmsDatabase.TABLE_NAME} ON ${AttachmentDatabase.TABLE_NAME}.${AttachmentDatabase.MMS_ID} = ${MmsDatabase.TABLE_NAME}.${MmsDatabase.ID}
WHERE ${MmsDatabase.TABLE_NAME}.${MmsDatabase.EXPIRES_IN} <= 0 AND ${MmsDatabase.TABLE_NAME}.${MmsDatabase.VIEW_ONCE} <= 0
""".trimIndent()
}
@@ -13,13 +13,12 @@ import java.security.NoSuchAlgorithmException;
public abstract class FullBackupBase { public abstract class FullBackupBase {
@SuppressWarnings("unused") private static final int DIGEST_ROUNDS = 250_000;
private static final String TAG = Log.tag(FullBackupBase.class);
static class BackupStream { static class BackupStream {
static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) { static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
try { try {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0));
MessageDigest digest = MessageDigest.getInstance("SHA-512"); MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] input = passphrase.replace(" ", "").getBytes(); byte[] input = passphrase.replace(" ", "").getBytes();
@@ -27,8 +26,8 @@ public abstract class FullBackupBase {
if (salt != null) digest.update(salt); if (salt != null) digest.update(salt);
for (int i=0;i<250000;i++) { for (int i = 0; i < DIGEST_ROUNDS; i++) {
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0));
digest.update(hash); digest.update(hash);
hash = digest.digest(input); hash = digest.digest(input);
} }
@@ -47,20 +46,34 @@ public abstract class FullBackupBase {
} }
private final Type type; private final Type type;
private final int count; private final long count;
private final long estimatedTotalCount;
BackupEvent(Type type, int count) { BackupEvent(Type type, long count, long estimatedTotalCount) {
this.type = type; this.type = type;
this.count = count; this.count = count;
this.estimatedTotalCount = estimatedTotalCount;
} }
public Type getType() { public Type getType() {
return type; return type;
} }
public int getCount() { public long getCount() {
return count; return count;
} }
public long getEstimatedTotalCount() {
return estimatedTotalCount;
}
public double getCompletionPercentage() {
if (estimatedTotalCount == 0) {
return 0;
}
return Math.min(99.9f, (double) count * 100L / (double) estimatedTotalCount);
}
} }
} }
@@ -13,7 +13,7 @@ import androidx.documentfile.provider.DocumentFile;
import com.annimon.stream.function.Predicate; import com.annimon.stream.function.Predicate;
import com.google.protobuf.ByteString; import com.google.protobuf.ByteString;
import net.sqlcipher.database.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions; import org.signal.core.util.Conversions;
@@ -38,11 +38,11 @@ import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase; import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet; import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper; import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -75,6 +75,11 @@ public class FullBackupExporter extends FullBackupBase {
private static final String TAG = Log.tag(FullBackupExporter.class); private static final String TAG = Log.tag(FullBackupExporter.class);
private static final long DATABASE_VERSION_RECORD_COUNT = 1L;
private static final long TABLE_RECORD_COUNT_MULTIPLIER = 3L;
private static final long IDENTITY_KEY_BACKUP_RECORD_COUNT = 2L;
private static final long FINAL_MESSAGE_COUNT = 1L;
private static final Set<String> BLACKLISTED_TABLES = SetUtil.newHashSet( private static final Set<String> BLACKLISTED_TABLES = SetUtil.newHashSet(
SignedPreKeyDatabase.TABLE_NAME, SignedPreKeyDatabase.TABLE_NAME,
OneTimePreKeyDatabase.TABLE_NAME, OneTimePreKeyDatabase.TABLE_NAME,
@@ -84,7 +89,8 @@ public class FullBackupExporter extends FullBackupBase {
EmojiSearchDatabase.TABLE_NAME, EmojiSearchDatabase.TABLE_NAME,
SenderKeyDatabase.TABLE_NAME, SenderKeyDatabase.TABLE_NAME,
SenderKeySharedDatabase.TABLE_NAME, SenderKeySharedDatabase.TABLE_NAME,
PendingRetryReceiptDatabase.TABLE_NAME PendingRetryReceiptDatabase.TABLE_NAME,
AvatarPickerDatabase.TABLE_NAME
); );
public static void export(@NonNull Context context, public static void export(@NonNull Context context,
@@ -133,58 +139,62 @@ public class FullBackupExporter extends FullBackupBase {
@NonNull BackupCancellationSignal cancellationSignal) @NonNull BackupCancellationSignal cancellationSignal)
throws IOException throws IOException
{ {
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase); BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
int count = 0; int count = 0;
long estimatedCountOutside = 0L;
try { try {
outputStream.writeDatabaseVersion(input.getVersion()); outputStream.writeDatabaseVersion(input.getVersion());
count++; count++;
List<String> tables = exportSchema(input, outputStream); List<String> tables = exportSchema(input, outputStream);
count += tables.size() * 3; count += tables.size() * TABLE_RECORD_COUNT_MULTIPLIER;
final long estimatedCount = calculateCount(context, input, tables);
estimatedCountOutside = estimatedCount;
Stopwatch stopwatch = new Stopwatch("Backup"); Stopwatch stopwatch = new Stopwatch("Backup");
for (String table : tables) { for (String table : tables) {
throwIfCanceled(cancellationSignal); throwIfCanceled(cancellationSignal);
if (table.equals(MmsDatabase.TABLE_NAME)) { if (table.equals(MmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, cancellationSignal); count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, estimatedCount, cancellationSignal);
} else if (table.equals(SmsDatabase.TABLE_NAME)) { } else if (table.equals(SmsDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count, cancellationSignal); count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringSmsMessage, null, count, estimatedCount, cancellationSignal);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) { } else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, cancellationSignal); count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptDatabase.MMS_ID))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) { } else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount), count, cancellationSignal); count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (table.equals(StickerDatabase.TABLE_NAME)) { } else if (table.equals(StickerDatabase.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount), count, cancellationSignal); count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) { } else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) {
count = exportTable(table, input, outputStream, null, null, count, cancellationSignal); count = exportTable(table, input, outputStream, null, null, count, estimatedCount, cancellationSignal);
} }
stopwatch.split("table::" + table); stopwatch.split("table::" + table);
} }
for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) { for (BackupProtos.SharedPreference preference : IdentityKeyUtil.getBackupRecord(context)) {
throwIfCanceled(cancellationSignal); throwIfCanceled(cancellationSignal);
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(preference); outputStream.write(preference);
} }
for (BackupProtos.SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) { for (BackupProtos.SharedPreference preference : TextSecurePreferences.getPreferencesToSaveToBackup(context)) {
throwIfCanceled(cancellationSignal); throwIfCanceled(cancellationSignal);
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(preference); outputStream.write(preference);
} }
stopwatch.split("prefs"); stopwatch.split("prefs");
count = exportKeyValues(outputStream, SignalStore.getKeysToIncludeInBackup(), count, cancellationSignal); count = exportKeyValues(outputStream, SignalStore.getKeysToIncludeInBackup(), count, estimatedCount, cancellationSignal);
stopwatch.split("key_values"); stopwatch.split("key_values");
for (AvatarHelper.Avatar avatar : AvatarHelper.getAvatars(context)) { for (AvatarHelper.Avatar avatar : AvatarHelper.getAvatars(context)) {
throwIfCanceled(cancellationSignal); throwIfCanceled(cancellationSignal);
if (avatar != null) { if (avatar != null) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength()); outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength());
} }
} }
@@ -197,7 +207,49 @@ public class FullBackupExporter extends FullBackupBase {
if (closeOutputStream) { if (closeOutputStream) {
outputStream.close(); outputStream.close();
} }
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count, estimatedCountOutside));
}
}
private static long calculateCount(@NonNull Context context, @NonNull SQLiteDatabase input, List<String> tables) {
long count = DATABASE_VERSION_RECORD_COUNT + TABLE_RECORD_COUNT_MULTIPLIER * tables.size();
for (String table : tables) {
if (table.equals(MmsDatabase.TABLE_NAME)) {
count += getCount(input, BackupCountQueries.mmsCount);
} else if (table.equals(SmsDatabase.TABLE_NAME)) {
count += getCount(input, BackupCountQueries.smsCount);
} else if (table.equals(GroupReceiptDatabase.TABLE_NAME)) {
count += getCount(input, BackupCountQueries.getGroupReceiptCount());
} else if (table.equals(AttachmentDatabase.TABLE_NAME)) {
count += getCount(input, BackupCountQueries.getAttachmentCount());
} else if (table.equals(StickerDatabase.TABLE_NAME)) {
count += getCount(input, "SELECT COUNT(*) FROM " + table);
} else if (!BLACKLISTED_TABLES.contains(table) && !table.startsWith("sqlite_")) {
count += getCount(input, "SELECT COUNT(*) FROM " + table);
}
}
count += IDENTITY_KEY_BACKUP_RECORD_COUNT;
count += TextSecurePreferences.getPreferencesToSaveToBackupCount(context);
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication())
.getDataSet();
for (String key : SignalStore.getKeysToIncludeInBackup()) {
if (dataSet.containsKey(key)) {
count++;
}
}
count += AvatarHelper.getAvatarCount(context);
return count + FINAL_MESSAGE_COUNT;
}
private static long getCount(@NonNull SQLiteDatabase input, @NonNull String query) {
try (Cursor cursor = input.rawQuery(query)) {
return cursor.moveToFirst() ? cursor.getLong(0) : 0;
} }
} }
@@ -243,6 +295,7 @@ public class FullBackupExporter extends FullBackupBase {
@Nullable Predicate<Cursor> predicate, @Nullable Predicate<Cursor> predicate,
@Nullable PostProcessor postProcess, @Nullable PostProcessor postProcess,
int count, int count,
long estimatedCount,
@NonNull BackupCancellationSignal cancellationSignal) @NonNull BackupCancellationSignal cancellationSignal)
throws IOException throws IOException
{ {
@@ -282,7 +335,7 @@ public class FullBackupExporter extends FullBackupBase {
statement.append(')'); statement.append(')');
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(statementBuilder.setStatement(statement.toString()).build()); outputStream.write(statementBuilder.setStatement(statement.toString()).build());
if (postProcess != null) { if (postProcess != null) {
@@ -295,7 +348,7 @@ public class FullBackupExporter extends FullBackupBase {
return count; return count;
} }
private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count) { private static int exportAttachment(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) {
try { try {
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)); long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID));
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)); long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID));
@@ -320,7 +373,7 @@ public class FullBackupExporter extends FullBackupBase {
if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); if (random != null && random.length == 32) inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data)); else inputStream = ClassicDecryptingPartInputStream.createFor(attachmentSecret, new File(data));
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size); outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size);
} }
} catch (IOException e) { } catch (IOException e) {
@@ -330,7 +383,7 @@ public class FullBackupExporter extends FullBackupBase {
return count; return count;
} }
private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count) { private static int exportSticker(@NonNull AttachmentSecret attachmentSecret, @NonNull Cursor cursor, @NonNull BackupFrameOutputStream outputStream, int count, long estimatedCount) {
try { try {
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID)); long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase._ID));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH)); long size = cursor.getLong(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_LENGTH));
@@ -339,7 +392,7 @@ public class FullBackupExporter extends FullBackupBase {
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM)); byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(StickerDatabase.FILE_RANDOM));
if (!TextUtils.isEmpty(data) && size > 0) { if (!TextUtils.isEmpty(data) && size > 0) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0); InputStream inputStream = ModernDecryptingPartInputStream.createFor(attachmentSecret, random, new File(data), 0);
outputStream.writeSticker(rowId, inputStream, size); outputStream.writeSticker(rowId, inputStream, size);
} }
@@ -370,6 +423,7 @@ public class FullBackupExporter extends FullBackupBase {
private static int exportKeyValues(@NonNull BackupFrameOutputStream outputStream, private static int exportKeyValues(@NonNull BackupFrameOutputStream outputStream,
@NonNull List<String> keysToIncludeInBackup, @NonNull List<String> keysToIncludeInBackup,
int count, int count,
long estimatedCount,
BackupCancellationSignal cancellationSignal) throws IOException BackupCancellationSignal cancellationSignal) throws IOException
{ {
KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication()) KeyValueDataSet dataSet = KeyValueDatabase.getInstance(ApplicationDependencies.getApplication())
@@ -400,7 +454,7 @@ public class FullBackupExporter extends FullBackupBase {
throw new AssertionError("Unknown type: " + type); throw new AssertionError("Unknown type: " + type);
} }
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
outputStream.write(builder.build()); outputStream.write(builder.build());
} }
@@ -408,12 +462,12 @@ public class FullBackupExporter extends FullBackupBase {
} }
private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) { private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) {
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 && return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0 &&
cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0; cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) <= 0;
} }
private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) { private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) {
return cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0; return cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.EXPIRES_IN)) <= 0;
} }
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) { private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long mmsId) {
@@ -11,7 +11,7 @@ import android.util.Pair;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.signal.core.util.Conversions; import org.signal.core.util.Conversions;
@@ -95,7 +95,7 @@ public class FullBackupImporter extends FullBackupBase {
BackupFrame frame; BackupFrame frame;
while (!(frame = inputStream.readFrame()).getEnd()) { while (!(frame = inputStream.readFrame()).getEnd()) {
if (count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count)); if (count % 100 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, count, 0));
count++; count++;
if (frame.hasVersion()) processVersion(db, frame.getVersion()); if (frame.hasVersion()) processVersion(db, frame.getVersion());
@@ -115,7 +115,7 @@ public class FullBackupImporter extends FullBackupBase {
keyValueDatabase.endTransaction(); keyValueDatabase.endTransaction();
} }
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, count, 0));
} }
private static @NonNull InputStream getInputStream(@NonNull Context context, @NonNull Uri uri) throws IOException{ private static @NonNull InputStream getInputStream(@NonNull Context context, @NonNull Uri uri) throws IOException{
@@ -162,9 +162,8 @@ public class FullBackupImporter extends FullBackupBase {
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream) private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
throws IOException throws IOException
{ {
File partsDirectory = context.getDir(AttachmentDatabase.DIRECTORY, Context.MODE_PRIVATE); File dataFile = AttachmentDatabase.newFile(context);
File dataFile = File.createTempFile("part", ".mms", partsDirectory); Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
@@ -0,0 +1,94 @@
package org.thoughtcrime.securesms.badges
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.res.use
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ThemeUtil
import java.lang.IllegalArgumentException
class BadgeImageView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : AppCompatImageView(context, attrs) {
private var badgeSize: Int = 0
init {
context.obtainStyledAttributes(attrs, R.styleable.BadgeImageView).use {
badgeSize = it.getInt(R.styleable.BadgeImageView_badge_size, 0)
}
isClickable = false
}
override fun setOnClickListener(l: OnClickListener?) {
val wasClickable = isClickable
super.setOnClickListener(l)
this.isClickable = wasClickable
}
fun setBadgeFromRecipient(recipient: Recipient?) {
getGlideRequests()?.let {
setBadgeFromRecipient(recipient, it)
} ?: clearDrawable()
}
fun setBadgeFromRecipient(recipient: Recipient?, glideRequests: GlideRequests) {
if (recipient == null || recipient.badges.isEmpty()) {
setBadge(null, glideRequests)
} else if (recipient.isSelf) {
val badge = recipient.featuredBadge
if (badge == null || !badge.visible || badge.isExpired()) {
setBadge(null, glideRequests)
} else {
setBadge(badge, glideRequests)
}
} else {
setBadge(recipient.featuredBadge, glideRequests)
}
}
fun setBadge(badge: Badge?) {
getGlideRequests()?.let {
setBadge(badge, it)
} ?: clearDrawable()
}
fun setBadge(badge: Badge?, glideRequests: GlideRequests) {
if (badge != null) {
glideRequests
.load(badge)
.downsample(DownsampleStrategy.NONE)
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context)))
.into(this)
isClickable = true
} else {
glideRequests
.clear(this)
clearDrawable()
}
}
private fun clearDrawable() {
setImageDrawable(null)
isClickable = false
}
private fun getGlideRequests(): GlideRequests? {
return try {
GlideApp.with(this)
} catch (e: IllegalArgumentException) {
// View not attached to an activity or activity destroyed
null
}
}
}
@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.badges
import android.content.Context
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.schedulers.Schedulers
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.database.RecipientDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.ProfileUtil
class BadgeRepository(context: Context) {
private val context = context.applicationContext
fun setVisibilityForAllBadges(
displayBadgesOnProfile: Boolean,
selfBadges: List<Badge> = Recipient.self().badges
): Completable = Completable.fromAction {
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
val badges = selfBadges.map { it.copy(visible = displayBadgesOnProfile) }
ProfileUtil.uploadProfileWithBadges(context, badges)
SignalStore.donationsValues().setDisplayBadgesOnProfile(displayBadgesOnProfile)
recipientDatabase.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
recipientDatabase.setBadges(Recipient.self().id, badges)
}.subscribeOn(Schedulers.io())
fun setFeaturedBadge(featuredBadge: Badge): Completable = Completable.fromAction {
val badges = Recipient.self().badges
val reOrderedBadges = listOf(featuredBadge.copy(visible = true)) + (badges.filterNot { it.id == featuredBadge.id })
ProfileUtil.uploadProfileWithBadges(context, reOrderedBadges)
val recipientDatabase: RecipientDatabase = SignalDatabase.recipients
recipientDatabase.setBadges(Recipient.self().id, reOrderedBadges)
}.subscribeOn(Schedulers.io())
}
@@ -0,0 +1,128 @@
package org.thoughtcrime.securesms.badges
import android.content.Context
import android.net.Uri
import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.AlignItems
import com.google.android.flexbox.FlexDirection
import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.flexbox.JustifyContent
import org.signal.core.util.DimensionUnit
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.Badge.Category.Companion.fromCode
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
import org.thoughtcrime.securesms.util.ScreenDensity
import org.whispersystems.libsignal.util.Pair
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import java.math.BigDecimal
import java.sql.Timestamp
object Badges {
private val TAG: String = Log.tag(Badges::class.java)
fun DSLConfiguration.displayBadges(
context: Context,
badges: List<Badge>,
selectedBadge: Badge? = null,
fadedBadgeId: String? = null
) {
badges
.map {
Badge.Model(
badge = it,
isSelected = it == selectedBadge,
isFaded = it.id == fadedBadgeId
)
}
.forEach { customPref(it) }
val badgeSize = DimensionUnit.DP.toPixels(88f)
val windowWidth = context.resources.displayMetrics.widthPixels
val perRow = (windowWidth / badgeSize).toInt()
val empties = ((perRow - (badges.size % perRow)) % perRow)
repeat(empties) {
customPref(Badge.EmptyModel())
}
}
fun createLayoutManagerForGridWithBadges(context: Context): RecyclerView.LayoutManager {
val layoutManager = FlexboxLayoutManager(context)
layoutManager.flexDirection = FlexDirection.ROW
layoutManager.alignItems = AlignItems.CENTER
layoutManager.justifyContent = JustifyContent.CENTER
return layoutManager
}
private fun getBadgeImageUri(densityPath: String): Uri {
return Uri.parse(BuildConfig.BADGE_STATIC_ROOT).buildUpon()
.appendPath(densityPath)
.build()
}
private fun getBestBadgeImageUriForDevice(serviceBadge: SignalServiceProfile.Badge): Pair<Uri, String> {
return when (ScreenDensity.getBestDensityBucketForDevice()) {
"ldpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[0]), "ldpi")
"mdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[1]), "mdpi")
"hdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[2]), "hdpi")
"xxhdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[4]), "xxhdpi")
"xxxhdpi" -> Pair(getBadgeImageUri(serviceBadge.sprites6[5]), "xxxhdpi")
else -> Pair(getBadgeImageUri(serviceBadge.sprites6[3]), "xhdpi")
}.also {
Log.d(TAG, "Selected badge density ${it.second()}")
}
}
private fun getTimestamp(bigDecimal: BigDecimal): Long {
return Timestamp(bigDecimal.toLong() * 1000).time
}
@JvmStatic
fun fromDatabaseBadge(badge: BadgeList.Badge): Badge {
return Badge(
badge.id,
fromCode(badge.category),
badge.name,
badge.description,
Uri.parse(badge.imageUrl),
badge.imageDensity,
badge.expiration,
badge.visible
)
}
@JvmStatic
fun toDatabaseBadge(badge: Badge): BadgeList.Badge {
return BadgeList.Badge.newBuilder()
.setId(badge.id)
.setCategory(badge.category.code)
.setDescription(badge.description)
.setExpiration(badge.expirationTimestamp)
.setVisible(badge.visible)
.setName(badge.name)
.setImageUrl(badge.imageUrl.toString())
.setImageDensity(badge.imageDensity)
.build()
}
@JvmStatic
fun fromServiceBadge(serviceBadge: SignalServiceProfile.Badge): Badge {
val uriAndDensity: Pair<Uri, String> = getBestBadgeImageUriForDevice(serviceBadge)
return Badge(
serviceBadge.id,
fromCode(serviceBadge.category),
serviceBadge.name,
serviceBadge.description,
uriAndDensity.first(),
uriAndDensity.second(),
serviceBadge.expiration?.let { getTimestamp(it) } ?: 0,
serviceBadge.isVisible
)
}
}
@@ -0,0 +1,165 @@
package org.thoughtcrime.securesms.badges.glide
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Rect
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import java.security.MessageDigest
/**
* Cuts out the badge of the requested size from the sprite sheet.
*/
class BadgeSpriteTransformation(
private val size: Size,
private val density: String,
private val isDarkTheme: Boolean
) : BitmapTransformation() {
private val id = "BadgeSpriteTransformation(${size.code},$density,$isDarkTheme).$VERSION"
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(id.toByteArray(CHARSET))
}
override fun equals(other: Any?): Boolean {
return (other as? BadgeSpriteTransformation)?.id == id
}
override fun hashCode(): Int {
return id.hashCode()
}
override fun transform(pool: BitmapPool, toTransform: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
val outBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(outBitmap)
val inBounds = getInBounds(density, size, isDarkTheme)
val outBounds = Rect(0, 0, outWidth, outHeight)
canvas.drawBitmap(toTransform, inBounds, outBounds, null)
return outBitmap
}
enum class Size(val code: String, val frameMap: Map<Density, FrameSet>) {
SMALL(
"small",
mapOf(
Density.LDPI to FrameSet(Frame(124, 1, 12, 12), Frame(145, 31, 12, 12)),
Density.MDPI to FrameSet(Frame(163, 1, 16, 16), Frame(189, 39, 16, 16)),
Density.HDPI to FrameSet(Frame(244, 1, 24, 24), Frame(283, 58, 24, 24)),
Density.XHDPI to FrameSet(Frame(323, 1, 32, 32), Frame(373, 75, 32, 32)),
Density.XXHDPI to FrameSet(Frame(483, 1, 48, 48), Frame(557, 111, 48, 48)),
Density.XXXHDPI to FrameSet(Frame(643, 1, 64, 64), Frame(741, 147, 64, 64))
)
),
MEDIUM(
"medium",
mapOf(
Density.LDPI to FrameSet(Frame(124, 16, 18, 18), Frame(160, 31, 18, 18)),
Density.MDPI to FrameSet(Frame(163, 19, 24, 24), Frame(207, 39, 24, 24)),
Density.HDPI to FrameSet(Frame(244, 28, 36, 36), Frame(310, 58, 36, 36)),
Density.XHDPI to FrameSet(Frame(323, 35, 48, 48), Frame(407, 75, 48, 48)),
Density.XXHDPI to FrameSet(Frame(483, 51, 72, 72), Frame(607, 111, 72, 72)),
Density.XXXHDPI to FrameSet(Frame(643, 67, 96, 96), Frame(807, 147, 96, 96))
)
),
LARGE(
"large",
mapOf(
Density.LDPI to FrameSet(Frame(145, 1, 27, 27), Frame(124, 46, 27, 27)),
Density.MDPI to FrameSet(Frame(189, 1, 36, 36), Frame(163, 57, 36, 36)),
Density.HDPI to FrameSet(Frame(283, 1, 54, 54), Frame(244, 85, 54, 54)),
Density.XHDPI to FrameSet(Frame(373, 1, 72, 72), Frame(323, 109, 72, 72)),
Density.XXHDPI to FrameSet(Frame(557, 1, 108, 108), Frame(483, 161, 108, 108)),
Density.XXXHDPI to FrameSet(Frame(741, 1, 144, 144), Frame(643, 213, 144, 144))
)
),
BADGE_64(
"badge_64",
mapOf(
Density.LDPI to FrameSet(Frame(124, 73, 48, 48), Frame(124, 73, 48, 48)),
Density.MDPI to FrameSet(Frame(163, 97, 64, 64), Frame(163, 97, 64, 64)),
Density.HDPI to FrameSet(Frame(244, 145, 96, 96), Frame(244, 145, 96, 96)),
Density.XHDPI to FrameSet(Frame(323, 193, 128, 128), Frame(323, 193, 128, 128)),
Density.XXHDPI to FrameSet(Frame(483, 289, 192, 192), Frame(483, 289, 192, 192)),
Density.XXXHDPI to FrameSet(Frame(643, 385, 256, 256), Frame(643, 385, 256, 256))
)
),
BADGE_112(
"badge_112",
mapOf(
Density.LDPI to FrameSet(Frame(181, 1, 84, 84), Frame(181, 1, 84, 84)),
Density.MDPI to FrameSet(Frame(233, 1, 112, 112), Frame(233, 1, 112, 112)),
Density.HDPI to FrameSet(Frame(349, 1, 168, 168), Frame(349, 1, 168, 168)),
Density.XHDPI to FrameSet(Frame(457, 1, 224, 224), Frame(457, 1, 224, 224)),
Density.XXHDPI to FrameSet(Frame(681, 1, 336, 336), Frame(681, 1, 336, 336)),
Density.XXXHDPI to FrameSet(Frame(905, 1, 448, 448), Frame(905, 1, 448, 448))
)
),
XLARGE(
"xlarge",
mapOf(
Density.LDPI to FrameSet(Frame(1, 1, 120, 120), Frame(1, 1, 120, 120)),
Density.MDPI to FrameSet(Frame(1, 1, 160, 160), Frame(1, 1, 160, 160)),
Density.HDPI to FrameSet(Frame(1, 1, 240, 240), Frame(1, 1, 240, 240)),
Density.XHDPI to FrameSet(Frame(1, 1, 320, 320), Frame(1, 1, 320, 320)),
Density.XXHDPI to FrameSet(Frame(1, 1, 480, 480), Frame(1, 1, 480, 480)),
Density.XXXHDPI to FrameSet(Frame(1, 1, 640, 640), Frame(1, 1, 640, 640))
)
);
companion object {
fun fromInteger(integer: Int): Size {
return when (integer) {
0 -> SMALL
1 -> MEDIUM
2 -> LARGE
3 -> XLARGE
4 -> BADGE_64
5 -> BADGE_112
else -> LARGE
}
}
}
}
enum class Density(val density: String) {
LDPI("ldpi"),
MDPI("mdpi"),
HDPI("hdpi"),
XHDPI("xhdpi"),
XXHDPI("xxhdpi"),
XXXHDPI("xxxhdpi")
}
data class FrameSet(val light: Frame, val dark: Frame)
data class Frame(
val x: Int,
val y: Int,
val width: Int,
val height: Int
) {
fun toBounds(): Rect {
return Rect(x, y, x + width, y + height)
}
}
companion object {
private const val VERSION = 3
private fun getDensity(density: String): Density {
return Density.values().first { it.density == density }
}
private fun getFrame(size: Size, density: Density, isDarkTheme: Boolean): Frame {
val frameSet: FrameSet = size.frameMap[density]!!
return if (isDarkTheme) frameSet.dark else frameSet.light
}
private fun getInBounds(density: String, size: Size, isDarkTheme: Boolean): Rect {
return getFrame(size, getDensity(density), isDarkTheme).toBounds()
}
}
}
@@ -0,0 +1,173 @@
package org.thoughtcrime.securesms.badges.models
import android.animation.ObjectAnimator
import android.net.Uri
import android.os.Parcelable
import android.view.View
import android.widget.ImageView
import android.widget.TextView
import com.bumptech.glide.load.Key
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import kotlinx.parcelize.Parcelize
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
import java.security.MessageDigest
typealias OnBadgeClicked = (Badge, Boolean, Boolean) -> Unit
/**
* A Badge that can be collected and displayed by a user.
*/
@Parcelize
data class Badge(
val id: String,
val category: Category,
val name: String,
val description: String,
val imageUrl: Uri,
val imageDensity: String,
val expirationTimestamp: Long,
val visible: Boolean,
) : Parcelable, Key {
fun isExpired(): Boolean = expirationTimestamp < System.currentTimeMillis() && expirationTimestamp > 0
fun isBoost(): Boolean = id == BOOST_BADGE_ID
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(id.toByteArray(Key.CHARSET))
messageDigest.update(imageUrl.toString().toByteArray(Key.CHARSET))
messageDigest.update(imageDensity.toByteArray(Key.CHARSET))
}
fun resolveDescription(shortName: String): String {
return description.replace("{short_name}", shortName)
}
class EmptyModel : PreferenceModel<EmptyModel>() {
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
}
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
private val name: TextView = itemView.findViewById(R.id.name)
init {
itemView.isEnabled = false
itemView.isFocusable = false
itemView.isClickable = false
itemView.visibility = View.INVISIBLE
name.text = " "
}
override fun bind(model: EmptyModel) = Unit
}
class Model(
val badge: Badge,
val isSelected: Boolean = false,
val isFaded: Boolean = false
) : PreferenceModel<Model>() {
override fun areItemsTheSame(newItem: Model): Boolean {
return newItem.badge.id == badge.id
}
override fun areContentsTheSame(newItem: Model): Boolean {
return super.areContentsTheSame(newItem) &&
badge == newItem.badge &&
isSelected == newItem.isSelected &&
isFaded == newItem.isFaded
}
override fun getChangePayload(newItem: Model): Any? {
return if (badge == newItem.badge && isSelected != newItem.isSelected) {
SELECTION_CHANGED
} else {
null
}
}
}
class ViewHolder(itemView: View, private val onBadgeClicked: OnBadgeClicked) : MappingViewHolder<Model>(itemView) {
private val check: ImageView = itemView.findViewById(R.id.checkmark)
private val badge: ImageView = itemView.findViewById(R.id.badge)
private val name: TextView = itemView.findViewById(R.id.name)
private var checkAnimator: ObjectAnimator? = null
init {
check.isSelected = true
}
override fun bind(model: Model) {
itemView.setOnClickListener {
onBadgeClicked(model.badge, model.isSelected, model.isFaded)
}
checkAnimator?.cancel()
if (payload.isNotEmpty()) {
checkAnimator = if (model.isSelected) {
ObjectAnimator.ofFloat(check, "alpha", 1f)
} else {
ObjectAnimator.ofFloat(check, "alpha", 0f)
}
checkAnimator?.start()
return
}
badge.alpha = if (model.badge.isExpired() || model.isFaded) 0.5f else 1f
GlideApp.with(badge)
.load(model.badge)
.downsample(DownsampleStrategy.NONE)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transform(
BadgeSpriteTransformation(BadgeSpriteTransformation.Size.BADGE_64, model.badge.imageDensity, ThemeUtil.isDarkTheme(context)),
)
.into(badge)
if (model.isSelected) {
check.alpha = 1f
} else {
check.alpha = 0f
}
name.text = model.badge.name
}
}
enum class Category(val code: String) {
Donor("donor"),
Other("other"),
Testing("testing"); // Will be removed before final release
companion object {
fun fromCode(code: String): Category {
return when (code) {
"donor" -> Donor
"testing" -> Testing
else -> Other
}
}
}
}
companion object {
const val BOOST_BADGE_ID = "BOOST"
private val SELECTION_CHANGED = Any()
fun register(mappingAdapter: MappingAdapter, onBadgeClicked: OnBadgeClicked) {
mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it, onBadgeClicked) }, R.layout.badge_preference_view))
mappingAdapter.registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.badge_preference_view))
}
}
}
@@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.badges.models
import android.view.View
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
object BadgePreview {
fun register(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.featured_badge_preview_preference))
mappingAdapter.registerFactory(SubscriptionModel::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.subscription_flow_badge_preview_preference))
}
abstract class BadgeModel<T : BadgeModel<T>> : PreferenceModel<T>() {
abstract val badge: Badge?
}
data class Model(override val badge: Badge?) : BadgeModel<Model>() {
override fun areItemsTheSame(newItem: Model): Boolean {
return true
}
override fun areContentsTheSame(newItem: Model): Boolean {
return super.areContentsTheSame(newItem) && badge == newItem.badge
}
override fun getChangePayload(newItem: Model): Any? {
return Unit
}
}
data class SubscriptionModel(override val badge: Badge?) : BadgeModel<SubscriptionModel>() {
override fun areItemsTheSame(newItem: SubscriptionModel): Boolean {
return true
}
override fun areContentsTheSame(newItem: SubscriptionModel): Boolean {
return super.areContentsTheSame(newItem) && badge == newItem.badge
}
override fun getChangePayload(newItem: SubscriptionModel): Any? {
return Unit
}
}
class ViewHolder<T : BadgeModel<T>>(itemView: View) : MappingViewHolder<T>(itemView) {
private val avatar: AvatarImageView = itemView.findViewById(R.id.avatar)
private val badge: BadgeImageView = itemView.findViewById(R.id.badge)
override fun bind(model: T) {
if (payload.isEmpty()) {
avatar.setRecipient(Recipient.self())
avatar.disableQuickContact()
}
badge.setBadge(model.badge)
}
}
}
@@ -0,0 +1,35 @@
package org.thoughtcrime.securesms.badges.models
import android.view.View
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
object ExpiredBadge {
class Model(val badge: Badge) : PreferenceModel<Model>() {
override fun areItemsTheSame(newItem: Model): Boolean {
return newItem.badge.id == badge.id
}
override fun areContentsTheSame(newItem: Model): Boolean {
return super.areContentsTheSame(newItem) && newItem.badge == badge
}
}
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
private val badge: BadgeImageView = itemView.findViewById(R.id.expired_badge)
override fun bind(model: Model) {
badge.setBadge(model.badge)
}
}
fun register(adapter: MappingAdapter) {
adapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.expired_badge_preference))
}
}
@@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.badges.models
import android.view.View
import android.widget.TextView
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
data class LargeBadge(
val badge: Badge
) {
class Model(val largeBadge: LargeBadge, val shortName: String, val maxLines: Int) : MappingModel<Model> {
override fun areItemsTheSame(newItem: Model): Boolean {
return newItem.largeBadge.badge.id == largeBadge.badge.id
}
override fun areContentsTheSame(newItem: Model): Boolean {
return newItem.largeBadge == largeBadge && newItem.shortName == shortName && newItem.maxLines == maxLines
}
}
class EmptyModel : MappingModel<EmptyModel> {
override fun areItemsTheSame(newItem: EmptyModel): Boolean = true
override fun areContentsTheSame(newItem: EmptyModel): Boolean = true
}
class EmptyViewHolder(itemView: View) : MappingViewHolder<EmptyModel>(itemView) {
override fun bind(model: EmptyModel) {
}
}
class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
private val badge: BadgeImageView = itemView.findViewById(R.id.badge)
private val name: TextView = itemView.findViewById(R.id.name)
private val description: TextView = itemView.findViewById(R.id.description)
override fun bind(model: Model) {
badge.setBadge(model.largeBadge.badge)
name.text = model.largeBadge.badge.name
description.text = model.largeBadge.badge.resolveDescription(model.shortName)
description.setLines(model.maxLines)
description.maxLines = model.maxLines
description.minLines = model.maxLines
}
}
companion object {
fun register(mappingAdapter: MappingAdapter) {
mappingAdapter.registerFactory(Model::class.java, LayoutFactory({ ViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
mappingAdapter.registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.view_badge_bottom_sheet_dialog_fragment_page))
}
}
}
@@ -0,0 +1,120 @@
package org.thoughtcrime.securesms.badges.self.expired
import androidx.fragment.app.FragmentManager
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.models.ExpiredBadge
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsAdapter
import org.thoughtcrime.securesms.components.settings.DSLSettingsBottomSheetFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil
/**
* Bottom sheet displaying a fading badge with a notice and action for becoming a subscriber again.
*/
class ExpiredBadgeBottomSheetDialogFragment : DSLSettingsBottomSheetFragment(
peekHeightPercentage = 1f
) {
override fun bindAdapter(adapter: DSLSettingsAdapter) {
ExpiredBadge.register(adapter)
adapter.submitList(getConfiguration().toMappingModelList())
}
private fun getConfiguration(): DSLConfiguration {
val badge: Badge = ExpiredBadgeBottomSheetDialogFragmentArgs.fromBundle(requireArguments()).badge
val isLikelyASustainer = SignalStore.donationsValues().isLikelyASustainer()
return configure {
customPref(ExpiredBadge.Model(badge))
sectionHeaderPref(
DSLSettingsText.from(
if (badge.isBoost()) {
R.string.ExpiredBadgeBottomSheetDialogFragment__your_badge_has_expired
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__subscription_cancelled
},
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(4f).toInt())
noPadTextPref(
DSLSettingsText.from(
if (badge.isBoost()) {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_boost_badge_has_expired)
} else {
getString(R.string.ExpiredBadgeBottomSheetDialogFragment__your_sustainer, badge.name)
},
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(16f).toInt())
noPadTextPref(
DSLSettingsText.from(
if (badge.isBoost()) {
if (isLikelyASustainer) {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can_reactivate
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__to_continue_supporting_technology
}
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__you_can
},
DSLSettingsText.CenterModifier
)
)
space(DimensionUnit.DP.toPixels(92f).toInt())
primaryButton(
text = DSLSettingsText.from(
if (badge.isBoost()) {
if (isLikelyASustainer) {
R.string.ExpiredBadgeBottomSheetDialogFragment__add_a_boost
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__become_a_sustainer
}
} else {
R.string.ExpiredBadgeBottomSheetDialogFragment__renew_subscription
}
),
onClick = {
dismiss()
if (isLikelyASustainer) {
requireActivity().startActivity(AppSettingsActivity.boost(requireContext()))
} else {
requireActivity().startActivity(AppSettingsActivity.subscriptions(requireContext()))
}
}
)
secondaryButtonNoOutline(
text = DSLSettingsText.from(R.string.ExpiredBadgeBottomSheetDialogFragment__not_now),
onClick = {
dismiss()
}
)
}
}
companion object {
@JvmStatic
fun show(badge: Badge, fragmentManager: FragmentManager) {
val args = ExpiredBadgeBottomSheetDialogFragmentArgs.Builder(badge).build()
val fragment = ExpiredBadgeBottomSheetDialogFragment()
fragment.arguments = args.toBundle()
fragment.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
}
}
@@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.badges.self.featured
enum class SelectFeaturedBadgeEvent {
NO_BADGE_SELECTED,
FAILED_TO_UPDATE_PROFILE,
SAVE_SUCCESSFUL
}

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