Compare commits

..

642 Commits

Author SHA1 Message Date
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
Greyson Parrelli 8ee96b40d0 Bump version to 5.14.3 2021-06-10 16:50:51 -04:00
Greyson Parrelli 67f0f45b67 Updated language translations. 2021-06-10 16:50:17 -04:00
Cody Henthorne 881ab90982 Add additional logging to dialog. 2021-06-10 16:06:32 -04:00
Alex Hart 6d7e09fec1 Fix bug preventing VIEWED receipts from being sent to group recipients. 2021-06-10 16:52:24 -03:00
Greyson Parrelli c274ed6a96 Improve search performance. 2021-06-10 15:47:12 -04:00
Greyson Parrelli 53ffca964d Restrict group member names to 2 lines. 2021-06-10 11:08:45 -04:00
Greyson Parrelli 3da3367291 Ensure that multi-forwards have unique timestamps. 2021-06-10 11:03:07 -04:00
Cody Henthorne 412ee220ce Improve keyboard sizing in bubbled conversations. 2021-06-09 16:18:55 -04:00
Alex Hart a3e3667dc2 Add 'tick' to update conversation bubble timestamps every 1m. 2021-06-09 16:35:36 -03:00
Greyson Parrelli d5f63da9e4 Better database error handling. 2021-06-09 15:04:16 -04:00
Greyson Parrelli f8d2044356 Bump version to 5.14.2 2021-06-09 11:16:19 -04:00
Greyson Parrelli 4d2dc61f5d Updated language translations. 2021-06-09 11:16:19 -04:00
Cody Henthorne 5492685df2 Fix fragment lifecycle crash in Edit Profile. 2021-06-09 11:16:19 -04:00
Alex Hart ad8c6bc579 Hide 'remove from group' if not an admin of that group. 2021-06-09 11:16:19 -04:00
Alex Hart fb08f8ae17 Fix issue preventing people blocking receipts from seeing incoming voice notes as viewed. 2021-06-09 11:16:10 -04:00
Greyson Parrelli 7833d7c99a Handle the sender key capability better. 2021-06-09 09:56:57 -04:00
Alex Hart 335ff61011 Fix several Gif MP4 UX issues. 2021-06-09 10:23:41 -03:00
Greyson Parrelli 2029ea378f Bump version to 5.14.1 2021-06-08 16:48:10 -04:00
Greyson Parrelli cd7bc63cec Updated language translations. 2021-06-08 16:48:10 -04:00
Cody Henthorne 958331a8ea Fix bug with APNGParser over reading larger files and invalidating the stream. 2021-06-08 16:48:10 -04:00
Greyson Parrelli 2ba206b9db Rotate the mp4 gif feature flag. 2021-06-08 16:13:19 -04:00
Greyson Parrelli 9b90e371f9 Inline viewed receipt feature flags. 2021-06-08 16:10:34 -04:00
Alex Hart ff1c298817 Allow video gifs to download as if they were images. 2021-06-08 17:00:07 -03:00
Alex Hart dfe804dfa0 Increment GIF flag in AttachmentPointer to avoid android client bug. 2021-06-08 16:53:21 -03:00
Alex Hart 978c6f9349 Fix mp4 support and viewed dot coloring. 2021-06-08 16:10:08 -03:00
Alex Hart c5c176a818 Remove use of transitionmanager to prevent sticky header flickering. 2021-06-08 14:02:05 -03:00
Cody Henthorne 9f2d57493d Hide quality selector when no images selected. 2021-06-08 12:53:14 -04:00
Greyson Parrelli 0972d8f1e1 Inline the GV1 forced migration flag. 2021-06-08 12:42:51 -04:00
Alex Hart cf361334c4 Fix jank and decrease animation duration in share contact selection recycler. 2021-06-08 13:10:54 -03:00
Cody Henthorne c72dd86fed Remove old notification system and notification rewrite feature flag. 2021-06-08 11:20:19 -04:00
Cody Henthorne b6c653ff77 Remove Universal Expire Timer flag and fix bug with SMS. 2021-06-08 11:20:06 -04:00
Greyson Parrelli 5e3bbb0e64 Improve name rendering for nameless groups. 2021-06-08 11:18:08 -04:00
Greyson Parrelli 64124f6f4b Update strings from 'cellular' to 'mobile data'. 2021-06-08 08:16:02 -04:00
Cody Henthorne 6f6a6826d9 Restrict edit description to V2 and remove feature flag. 2021-06-07 20:07:49 -04:00
Greyson Parrelli 57c0b8fd0f Initial pre-alpha support for sender key. 2021-06-07 18:14:12 -04:00
Max Ullinger c54f016213 Fix inconsistent text scaling in quotes.
Fixes #10188
2021-06-07 17:26:47 -04:00
Cody Henthorne bece58d939 Improve notification channel consistency checks with Android Conversations. 2021-06-07 15:58:39 -04:00
Alex Hart 36443c59f9 Apply proximity wake lock in locked audio recording mode.
Fixes #10098
2021-06-07 16:55:26 -03:00
Cody Henthorne 02f0301f25 Change how we enable/disable vibration for notifications. 2021-06-07 15:44:38 -04:00
Alex Hart 334cf669ed Add support for multiple typing indicators in groups. 2021-06-07 15:35:19 -03:00
Greyson Parrelli 8442143818 Add support for the updated link device schema. 2021-06-07 11:19:06 -04:00
Greyson Parrelli b25b8b90e4 Set last search index download time. 2021-06-07 10:32:18 -04:00
Alex Hart 06aec0b7d7 Move bubble rendering from onMeasure to onLayout. 2021-06-07 09:16:18 -03:00
Alex Hart 835d7f5ccb Bump version to 5.14.0 2021-06-04 16:36:16 -03:00
Alex Hart ffd0b16753 Updated language translations. 2021-06-04 16:35:29 -03:00
Alex Hart b351fb43e6 Revert "Temporarily block payments in all regions."
This reverts commit 1466875293.
2021-06-04 16:29:37 -03:00
Cody Henthorne 7da47c9586 Fix NPE in ThumbnailsTask.
The async task was being cancelled, but there was still a race condition
in how the thumbnails list was being managed. This attempts to fix that.
2021-06-04 16:29:23 -03:00
Alex Hart e4755b298f Bump version to 5.13.8 2021-06-04 16:18:51 -03:00
Alex Hart 4a65487842 Updated language translations. 2021-06-04 16:18:09 -03:00
Alex Hart 1466875293 Temporarily block payments in all regions. 2021-06-04 16:18:09 -03:00
Alex Hart fd1e552ad1 Update name colors palette. 2021-06-04 16:05:02 -03:00
Alex Hart be3e89ac20 Utilize built in string id getter instead of using our own logic for name colors. 2021-06-04 16:05:02 -03:00
Alex Hart b8f1b98c74 Use user avatar or avatar color for bubble on wallpaper fragment. 2021-06-04 16:05:02 -03:00
Alex Hart 4bdd07db16 Fix NPE if system ringtone name lookup returns null. 2021-06-04 09:09:34 -03:00
Greyson Parrelli 511b095647 Bump version to 5.13.7 2021-06-03 21:27:56 -04:00
Greyson Parrelli 23f4d30e57 Updated language translations. 2021-06-03 21:27:37 -04:00
Greyson Parrelli 45c587c5e4 Allow variation selection in emoji search results. 2021-06-03 21:18:18 -04:00
Greyson Parrelli 115e74d844 Use borderless ripple for keyboard category buttons. 2021-06-03 20:50:31 -04:00
Greyson Parrelli 1475a77260 Update "GIFs moved" education tooltip. 2021-06-03 20:38:36 -04:00
Greyson Parrelli d0e2fbf8e7 Fix issues with emoji search backup/restore. 2021-06-03 20:30:44 -04:00
Greyson Parrelli 0f2f0450e3 Do not show chat color megaphone to new users.
They already have the onboarding variant.
2021-06-03 19:50:42 -04:00
Cody Henthorne e57f24c062 Bump version to 5.13.6 2021-06-03 17:23:34 -04:00
Cody Henthorne 203d7de6a2 Updated language translations. 2021-06-03 17:20:43 -04:00
Cody Henthorne 6e5f2f50fb Fix padding issue with keyboard indicator in compose. 2021-06-03 17:11:07 -04:00
Cody Henthorne 875895524e Fix media keyboard sizing issue by trying two ways to find window insets. 2021-06-03 17:11:07 -04:00
Cody Henthorne 3a21a2a49e Fix bad sync of default timer to linked devices. 2021-06-03 17:11:07 -04:00
Alex Hart 262b4e7d62 Hide bottom bar on scroll for Emoji pager. 2021-06-03 17:11:07 -04:00
Cody Henthorne c202f97088 Fix non-hiding bottom bar when not enough stickers. 2021-06-03 17:11:07 -04:00
Cody Henthorne f96eac96f9 Update keyboard colors to improve consistency. 2021-06-03 17:11:07 -04:00
Alex Hart c59006e06e Go back to emoji selection on keyboard close in search. 2021-06-03 14:21:56 -03:00
Cody Henthorne 84e27e7bff Remove GIFs from attachment keyboard. 2021-06-03 13:05:38 -04:00
Alex Hart a3a4b10f83 Wrap emoji pages with coordinator layout. Fix issue with bubble coloring in wallpaper preview. 2021-06-03 14:02:37 -03:00
Cody Henthorne a644c81736 Actually skip emoji searchd ata in backup restore. 2021-06-03 11:34:41 -04:00
Cody Henthorne 27b9fbe490 Add pull down search bar to stickers and auto hide when scrolling. 2021-06-03 11:33:06 -04:00
Alex Hart 2131c56513 Add unit testing for Recipient#getChatColors 2021-06-03 11:29:19 -03:00
Alex Hart 95dba15db8 Fix initial scroll position if there's not enough vertical space to hide search bar. 2021-06-03 11:26:49 -03:00
Cody Henthorne c23215604d Bump version to 5.13.5 2021-06-03 10:18:32 -04:00
Cody Henthorne 8c9f274d5a Updated language translations. 2021-06-03 10:18:18 -04:00
Cody Henthorne 504a70f3ee Skip emoji search data in backup/restore. 2021-06-03 10:12:12 -04:00
Alex Hart ad6f51901e Fix bad logic in chat color selection. 2021-06-03 10:12:12 -04:00
Cody Henthorne 52ef4c6235 Get more space on gif keyboard by hiding views. 2021-06-03 10:12:12 -04:00
Cody Henthorne 9ba4005433 Show keyboard when opening gif search. 2021-06-02 20:57:08 -04:00
Cody Henthorne 3cea3766ab Use correct GIF icon for dark theme. 2021-06-02 20:53:34 -04:00
Cody Henthorne ede24e0e73 Bump version to 5.13.4 2021-06-02 18:08:02 -04:00
Cody Henthorne df79bbc5aa Updated language translations. 2021-06-02 18:03:57 -04:00
Cody Henthorne 3c522c677b Fix crash when applying unknown fields. 2021-06-02 17:59:44 -04:00
Android Team 08e86b8c82 Add Emoji Search, Sticker Search, and GIF Keyboard.
Co-authored-by: Alex Hart <alex@signal.org>
Co-authored-by: Cody Henthorne <cody@signal.org>
Co-authored-by: ⁨Greyson Parrelli<greyson@signal.org>
2021-06-02 17:43:17 -04:00
Alex Hart 66c3b1388a Add new chat colors megaphone. 2021-06-02 16:52:21 -03:00
Alex Hart 8992f59c3b Update logic for color selection to match spec. 2021-06-02 16:51:23 -03:00
Alex Hart 1d6d27d46c Tweak name color palette and fix issue with non-present group members. 2021-06-02 16:31:55 -03:00
Alex Hart 625d36fb27 Start animation when megaphone is displayed. 2021-06-02 15:11:23 -03:00
Alex Hart 665ce14bb6 Fix RTL issue with thumbnail masking. 2021-06-02 14:49:26 -03:00
Alex Hart 6e4f002b6d Fix masking issue with multiselect highlighted items. 2021-06-02 14:34:34 -03:00
Cody Henthorne 39a7dbda94 Bump version to 5.13.3 2021-06-02 12:24:25 -04:00
Cody Henthorne 7ae8af4153 Updated language translations. 2021-06-02 12:22:17 -04:00
Alex Hart fb817e0c3b Add Chat Colors onboarding. 2021-06-02 12:16:10 -04:00
Tomer Rosenfeld 1eae360470 Do not remove system contact badging during partial syncs.
Fixes #11236
2021-06-02 12:16:10 -04:00
Cody Henthorne 0314db0b58 Small UI tweaks for edit reactions. 2021-06-02 12:16:10 -04:00
Cody Henthorne 4598387187 Bump version to 5.13.2 2021-05-27 16:27:43 -04:00
Cody Henthorne 445c93a756 Updated language translations. 2021-05-27 16:23:09 -04:00
Alex Hart 6c168ec575 Fix revealable color. 2021-05-27 16:17:07 -04:00
Greyson Parrelli 1322f5bc08 Be more careful with unknown IDs during storage sync. 2021-05-27 16:17:07 -04:00
Alex Hart 1c40f2d167 Fix issue with disappearing colors upon group removal. 2021-05-27 16:17:07 -04:00
Alex Hart 18133e2a10 Fix several issues with chatcolors. 2021-05-27 16:17:07 -04:00
Cody Henthorne e5b0941d30 Add ability to edit default reactions. 2021-05-27 16:17:07 -04:00
Cody Henthorne 811bef8c35 Bump version to 5.13.1 2021-05-26 20:07:20 -04:00
Cody Henthorne 057107ea7a Updated language translations. 2021-05-26 20:02:54 -04:00
Alex Hart 273e5f9168 Remove gradient support from api 19. 2021-05-26 19:56:20 -04:00
Alex Hart 35930fb23a Fix several ChatColors issues. 2021-05-26 20:06:57 -03:00
Alex Hart c794b5c2e7 Only display edit pencil if custom color is selected. 2021-05-26 19:56:04 -03:00
Greyson Parrelli e74d502ae6 Remove legacy session version.
Hasn't been used since the TextSecure days!
2021-05-26 17:46:58 -04:00
Greyson Parrelli e5ce6e3e2e Fix internal preference. 2021-05-26 12:45:54 -04:00
Greyson Parrelli 65020dde1a Fix some missed cases for blocking unregistered sends. 2021-05-26 12:02:22 -04:00
Alex Hart 98f432d23c Fix advanced prefs dialog title. 2021-05-26 11:50:17 -03:00
Cody Henthorne 2651b789dd Fix some group description UX oddities. 2021-05-26 10:42:36 -04:00
Cody Henthorne dbabac34b0 Fix video not showing until phone moved. 2021-05-26 10:25:58 -04:00
Alex Hart 6866b7a277 Fix chat color selection context menu positioning. 2021-05-26 11:13:25 -03:00
Alex Hart 03c19f54c2 Set background of typing indicator to match conversation. 2021-05-26 10:56:09 -03:00
Alex Hart ba510ca77d Update chat pluralization. 2021-05-26 10:47:40 -03:00
Alex Hart bb7409fd91 Add proper background color for quote preview. 2021-05-26 10:14:22 -03:00
Alex Hart 23e5da4d95 Fix issue where message sender was impacting bubble color in groups. 2021-05-26 09:41:20 -03:00
Greyson Parrelli fb1b46b67e Bump version to 5.13.0 2021-05-26 00:45:32 -04:00
Greyson Parrelli 7a21e6b5f8 Updated language translations. 2021-05-26 00:45:06 -04:00
Greyson Parrelli 6342a45b4e Separate avatar colors from chat colors. 2021-05-26 00:39:59 -04:00
Alex Hart bcc5d485ab Update chat colors. 2021-05-26 00:39:59 -04:00
Rainer Matischek 36fe150678 Increase maximum zoom level for large images. 2021-05-26 00:39:59 -04:00
Greyson Parrelli 54f92ae466 Do not send if unregistered. 2021-05-26 00:39:59 -04:00
Cody Henthorne b9b2924939 Add screen share receive support and improve video calling rotation. 2021-05-26 00:39:59 -04:00
Greyson Parrelli 513e5b45c5 Show notifications for group creates. 2021-05-26 00:39:59 -04:00
Greyson Parrelli 1fad5e2c1e Add some extra preconditions to reaction processing. 2021-05-26 00:39:59 -04:00
Greyson Parrelli 5a28cf616d Do not allow bad QR data to crash. 2021-05-26 00:39:59 -04:00
Cody Henthorne c08199659b Support pasting of images into input text. 2021-05-26 00:39:59 -04:00
Greyson Parrelli ca508514a7 Updated flipper to 0.91.0 2021-05-26 00:39:59 -04:00
Greyson Parrelli da2038dd46 Revert "Temporarily block payments in all regions."
This reverts commit 152cc27394.
2021-05-26 00:39:59 -04:00
Greyson Parrelli f02e2d23d0 Bump version to 5.12.3 2021-05-26 00:31:33 -04:00
Greyson Parrelli ef1c25c3d3 Updated language translations. 2021-05-26 00:31:02 -04:00
Alex Hart 152cc27394 Temporarily block payments in all regions. 2021-05-26 00:28:05 -04:00
Greyson Parrelli c582aca465 Bump version to 5.12.2 2021-05-20 11:15:00 -04:00
Greyson Parrelli 80e85fb49a Updated language translations. 2021-05-20 11:14:01 -04:00
Greyson Parrelli d660e22e61 Pull translations in parallel. 2021-05-20 11:10:16 -04:00
Cody Henthorne 51856c4f06 Add support back for Android Auto. 2021-05-20 10:42:06 -04:00
Cody Henthorne fd37da42f9 Revert "Remove Android Auto support (for now)."
This reverts commit 6c2adfeec2.
2021-05-20 09:46:38 -04:00
Cody Henthorne 11df2bc51f Replace spongy with libsignal x509 generation for device transfer. 2021-05-19 17:29:48 -04:00
Cody Henthorne 6770d21cf7 Fix crash when processing invalid mentions. 2021-05-19 13:15:28 -04:00
Cody Henthorne f490d1f6d2 Add long click copy for urls in group descriptions. 2021-05-19 12:29:34 -04:00
Cody Henthorne f890ae8ddc Enforce two line limit on group description.
Sorry.
2021-05-19 11:57:53 -04:00
Cody Henthorne 5d5d61d8ed Pluralize units for custom timer dialog. 2021-05-19 09:40:20 -04:00
Cody Henthorne 75589f1b2d Use new expire timer dialog from overflow menu. 2021-05-18 20:23:59 -04:00
Greyson Parrelli 6225c676e2 Bump version to 5.12.1 2021-05-18 19:31:12 -04:00
Greyson Parrelli 9b18668f49 Updated language translations. 2021-05-18 19:30:53 -04:00
Greyson Parrelli 2f80e7f1ff Put the default message timer behind a feature flag. 2021-05-18 19:26:25 -04:00
Greyson Parrelli 790413680d Bump version to 5.12.0 2021-05-18 18:28:24 -04:00
Cody Henthorne 47e9a4ec29 Fix hard to see media send HUD. 2021-05-18 18:21:49 -04:00
Cody Henthorne defd5e8047 Add universal disappearing messages. 2021-05-18 18:21:48 -04:00
Greyson Parrelli 8c6a88374b No longer use SignalServiceAddress legacy identifier.
We had to do this in the past because we previously didn't allow
UUID-only contacts back in the day. This hasn't been the case for some
time. We should be preferring the UUID in all cases.
2021-05-18 18:21:48 -04:00
Greyson Parrelli 7343613bea Revert "Temporarily block payments in all regions."
This reverts commit ec486d66f7.
2021-05-18 18:21:48 -04:00
Greyson Parrelli 155dda1fa4 Bump version to 5.11.5 2021-05-18 18:02:37 -04:00
Greyson Parrelli 3c74306c8d Updated language translations. 2021-05-18 18:02:18 -04:00
Alex Hart 13ecd9eee6 Temporarily block payments in all regions. 2021-05-18 17:56:38 -04:00
Alex Hart c48f3b4582 Bump version to 5.11.4 2021-05-17 17:03:21 -03:00
Alex Hart 30c007194d Updated language translations. 2021-05-17 17:03:21 -03:00
Cody Henthorne ef5b68eb35 Add report spam in message request state. 2021-05-17 17:03:21 -03:00
Cody Henthorne c47dcd5720 Add code formatting styles. 2021-05-17 17:03:21 -03:00
Greyson Parrelli ed3c5ab479 Do more to ensure that we have the latest self in StorageSyncJob. 2021-05-17 17:03:21 -03:00
Cody Henthorne a697b6c3d4 Fix long text layout bug in media quality selector. 2021-05-17 17:03:21 -03:00
Alex Hart 3965df78c9 Fix several settings issues. 2021-05-17 17:03:21 -03:00
Alex Hart 64ebf20c1b Bump version to 5.11.3 2021-05-14 16:46:51 -03:00
Alex Hart 797bed6701 Updated language translations. 2021-05-14 16:46:08 -03:00
Alex Hart e84e021127 Fix proxy settings navigation. 2021-05-14 16:46:08 -03:00
Greyson Parrelli 0b9515b58b Clean up another bad usage of self in StorageSyncJob. 2021-05-13 16:15:20 -04:00
Alex Hart 81ec9e96c7 Fix several settings issues. 2021-05-13 16:59:31 -03:00
Greyson Parrelli ee09793ef2 Fix outage reminder in dark theme.
Fixes #11258
2021-05-13 13:21:30 -04:00
Greyson Parrelli 61a130e645 Handle ServerRejectedException in more jobs. 2021-05-13 13:16:11 -04:00
Greyson Parrelli 19d342749a Bump version to 5.11.2 2021-05-13 12:27:50 -04:00
Greyson Parrelli 94adcf04f5 Updated language translations. 2021-05-13 12:27:24 -04:00
Alex Hart 53e1da0f43 Fix bad preference class setting. 2021-05-13 12:20:08 -04:00
Greyson Parrelli b41989de03 Be more consistent with 'self' in StorageSyncJob. 2021-05-13 12:20:08 -04:00
Greyson Parrelli 6c7848b750 Ensure we don't enqueue a ProfileKeySendJob to a v2 group. 2021-05-13 12:20:08 -04:00
Greyson Parrelli 07bd9ad840 Make debuglog submission slightly more discoverable. 2021-05-13 12:20:08 -04:00
Greyson Parrelli 14236d3062 Show About in AppSettings screen. 2021-05-13 12:00:24 -04:00
Cody Henthorne 6c4df30252 Fix flashing tap to view showing on conversation open. 2021-05-13 12:00:24 -04:00
Cody Henthorne 45218470af Update media send quality icons. 2021-05-13 12:00:24 -04:00
Greyson Parrelli 417ee1e047 Mark url as non-translatable. 2021-05-13 12:00:24 -04:00
Alex Hart 08a3bc457e Fix autodownload constraint. 2021-05-13 12:00:24 -04:00
Alex Hart 0cc2cba883 Clean up device fragments and utilize dsl toolbar. 2021-05-13 12:00:24 -04:00
Alex Hart 24d461c8b2 Fix settings crash and RTL bug. 2021-05-13 12:00:24 -04:00
Greyson Parrelli 4d472fccd2 Fix bugs with the bio preference in AppSettings.
- Always show the profile name.
- Pretty-print the phone number.
- Show the correct avatar when none is set.
2021-05-12 20:39:43 -04:00
Greyson Parrelli 45d010bdb6 Only update SMS setting if registration is complete.
Otherwise you could crash during registration if the user had previously
set Signal as the default.
2021-05-12 17:16:01 -04:00
Greyson Parrelli 70db617229 Fix some issues with avatar syncing.
- We weren't falling back to system avatars when no profile was present
- We weren't triggering a sync when the setting changed
2021-05-12 17:08:13 -04:00
Greyson Parrelli d8256013a3 Bump version to 5.11.1 2021-05-12 15:48:46 -04:00
Greyson Parrelli 6d2c22addc Updated language translations. 2021-05-12 15:48:46 -04:00
Greyson Parrelli 9640f3f215 Do not allow profile given names to be empty when editing. 2021-05-12 15:48:37 -04:00
Greyson Parrelli 80c911e118 Sync whether or not our primary device can send SMS. 2021-05-12 14:58:19 -04:00
Alex Hart f2d5ea0391 Refactor app settings. 2021-05-12 12:23:00 -04:00
Greyson Parrelli a94d77d81e Ensure inbound messages mark recipients as registered. 2021-05-12 12:20:14 -04:00
Greyson Parrelli 2d2de1a652 Fix storage service record merge. 2021-05-12 10:56:34 -04:00
Greyson Parrelli 01f8823fb2 Show an error animation if you don't select a help category. 2021-05-12 00:05:41 -04:00
Greyson Parrelli 260575d139 Utilize RecipientIdCache during message processing. 2021-05-11 12:19:07 -04:00
Greyson Parrelli 1fb3290038 Be more direct with AccountRecord updates. 2021-05-11 10:05:13 -04:00
Greyson Parrelli 37596320e8 Bump version to 5.11.0 2021-05-10 19:37:20 -04:00
Greyson Parrelli 7a959c2c3e Updated language translations. 2021-05-10 19:36:52 -04:00
Greyson Parrelli 877c03e6a1 Fix issue where bulk-archive wasn't triggering a storage sync.
Also took the opportunity to consolidate our archive code to reduce
duplication.
2021-05-10 19:30:11 -04:00
Greyson Parrelli d3431d227b Make selecting a help category mandatory. 2021-05-10 19:30:11 -04:00
Greyson Parrelli fbf307bf01 Manually handle 6-digit short codes for UK.
Also cleans up some set usages.

Fixes #11274
2021-05-10 19:30:11 -04:00
Alex Hart d672857e82 Fix layout designer deadlock. 2021-05-10 19:30:11 -04:00
Cody Henthorne dd934e0095 Add photo media quality selector when sending images. 2021-05-10 19:30:11 -04:00
Cody Henthorne 8c9df8d3be Add support for Group V2 description field. 2021-05-10 19:30:10 -04:00
Chris Eager b3aec58e69 Add additional test cases to VerificationCodeParserTest. 2021-05-10 19:30:10 -04:00
Greyson Parrelli b4111cffef Guard against shared content not having proper permissions.
Fixes #11269
2021-05-10 19:30:10 -04:00
Greyson Parrelli ecc8d1738e Respect system avatar preference when syncing with linked devices. 2021-05-10 19:30:10 -04:00
Greyson Parrelli d1982cbc0a Do not fetch profiles when unregistered. 2021-05-10 19:30:10 -04:00
Greyson Parrelli 03b65ce6dc Update to libsignal-client 0.5.1 2021-05-10 19:30:10 -04:00
Greyson Parrelli 56ea11cdff Reactively share profiles to those who should already have it. 2021-05-10 19:30:10 -04:00
Greyson Parrelli 7a02404f7b Update SQLCipher to v4.4.3 2021-05-10 19:30:10 -04:00
Greyson Parrelli b9a960a7c8 Revert "Temporarily block payments in all regions."
This reverts commit ec486d66f7.
2021-05-10 19:30:10 -04:00
Greyson Parrelli 02c87a4d7b Bump version to 5.10.8 2021-05-10 19:22:36 -04:00
Greyson Parrelli 0dd9cd82f8 Updated language translations. 2021-05-10 19:16:14 -04:00
Alex Hart ec486d66f7 Temporarily block payments in all regions. 2021-05-10 19:16:14 -04:00
Alex Hart 69cd7eb449 Fix issue with sharing resizable media to MMS. 2021-05-10 13:56:54 -03:00
Greyson Parrelli 1427de7c65 Bump version to 5.10.7 2021-05-07 13:23:13 -04:00
Greyson Parrelli 8ad66e1e5e Updated language translations. 2021-05-07 13:11:57 -04:00
Greyson Parrelli a2e31e97db Trim giphy queries. 2021-05-07 13:05:02 -04:00
Alex Hart 1f3e131690 Fix Emoji crashes when downloaded bitmap files cannot be found. 2021-05-07 13:56:14 -03:00
Greyson Parrelli 276b757e2d Fix the other possible NPE in database migration. 2021-05-07 10:43:45 -04:00
Cody Henthorne 093df70602 Bump version to 5.10.6 2021-05-06 18:06:50 -04:00
Cody Henthorne fe9ab66f31 Updated language translations. 2021-05-06 18:06:26 -04:00
Alex Hart 138f9476ac Revert emoji cache to old pattern. 2021-05-06 17:47:36 -04:00
Alex Hart cb9ab61b6b Fix issue where gifs would load as images. 2021-05-06 16:19:24 -03:00
Alex Hart bcbd365326 Fix gif player audio. 2021-05-06 13:29:28 -03:00
Greyson Parrelli afdf4e365f Fix possible NPE in database migration. 2021-05-05 17:56:06 -04:00
Greyson Parrelli 553b7522aa Bump version to 5.10.5 2021-05-05 16:52:51 -04:00
Greyson Parrelli 13f38dd594 Updated language translations. 2021-05-05 16:51:39 -04:00
Greyson Parrelli 31e1c6f7aa Handle 428 rate limiting. 2021-05-05 16:47:13 -04:00
Alex Hart 02d060ca0a Fix issue with gif search and emoji loading on lowmem devices. 2021-05-05 14:42:51 -04:00
Cody Henthorne 5e2a3ac644 Bump version to 5.10.4 2021-05-04 20:40:06 -04:00
Cody Henthorne 2fc461b85f Updated language translations. 2021-05-04 20:39:13 -04:00
Cody Henthorne 29a0b86411 Address API23 notification issues and update when conversation content changes. 2021-05-04 20:31:29 -04:00
Alex Hart efc3e7b25d Fix emoji on odd densities and add internal pref to force built-in. 2021-05-04 12:20:18 -04:00
Greyson Parrelli 6c2adfeec2 Remove Android Auto support (for now). 2021-05-03 22:11:52 -04:00
Cody Henthorne 3124d6d43e Bump version to 5.10.3 2021-05-03 14:17:50 -04:00
Cody Henthorne e5a6b7d47d Updated language translations. 2021-05-03 14:17:05 -04:00
Greyson Parrelli add65cf592 Prevent crash when opening conversation with unregistered UUID-only recipient. 2021-05-03 14:02:58 -04:00
Alex Hart 129effd0ec Add lighter weight emoji. 2021-05-03 14:02:58 -04:00
Cody Henthorne 2aad00df85 Add ability to configure locale specific media quality settings.
Part 1 of improve media quality controls. User selection coming soon.
2021-05-03 14:02:58 -04:00
Alex Hart 85e0e74bc6 Add support for OTA emoji download. 2021-05-03 14:02:58 -04:00
Alex Hart 7fa200401c Fix content insets for API30+ devices. 2021-04-30 13:16:23 -03:00
Cody Henthorne 1a452efbb9 Bump version to 5.10.2 2021-04-30 10:37:03 -04:00
Cody Henthorne eb4bdf1db2 Updated language translations. 2021-04-30 10:35:59 -04:00
Greyson Parrelli d58f68cb44 Fix issue where we could give storageIds to MMS groups or emails.
Things like force-unread and mute could be applied to MMS groups or
unregistered users (the worst kind being email SMS contacts) that could
result in crashes down the line.

Includes a DB migration to clean up the bad stuff.
2021-04-30 00:19:48 -04:00
Greyson Parrelli 2f30d29351 Ensure we have a storageId for self. 2021-04-29 17:55:34 -04:00
Cody Henthorne dc6dc192dc Bump version to 5.10.1 2021-04-28 20:56:28 -04:00
Cody Henthorne 751afadebd Fix notification issues introduced when adding lower API versions. 2021-04-28 20:49:36 -04:00
Alex Hart ac71c02dfa Bump version to 5.10.0 2021-04-28 16:45:09 -03:00
Alex Hart 4567da193e Revert "Temporarily block payments in all regions."
This reverts commit f4ae39dd44.
2021-04-28 16:36:40 -03:00
Cody Henthorne bd2a1d5574 Add support for lower APIs to new notification system. 2021-04-28 16:36:11 -03:00
Alex Hart ab44d608d2 Add support for sending and syncing viewed receipts behind a feature flag. 2021-04-28 16:36:11 -03:00
Greyson Parrelli cdc7f1565e Further simplify storage service syncing. 2021-04-28 16:36:11 -03:00
Jim Gustafson 1493581a4d Update to RingRTC v2.9.6 2021-04-28 16:36:11 -03:00
Greyson Parrelli 4461d6cf7f Rename StorageSyncJobV2 -> StorageSyncJob. 2021-04-28 16:36:11 -03:00
Greyson Parrelli 38e64b1f75 Remove old Storage Service V1 code. 2021-04-28 16:36:10 -03:00
Alex Hart eb1daf4a20 Pass exception to thrown AssertionError in shortuct icon generation. 2021-04-28 16:36:10 -03:00
Alex Hart e0c38f7c72 Bump version to 5.9.5 2021-04-28 16:27:31 -03:00
Alex Hart 5638ff4a3a Updated language translations. 2021-04-28 16:26:26 -03:00
Alex Hart f4ae39dd44 Temporarily block payments in all regions. 2021-04-28 16:16:04 -03:00
Cody Henthorne d235125138 Fix empty conversation banner view tap to unblur bug. 2021-04-28 14:08:30 -04:00
Cody Henthorne 78d7759197 Update witness file. 2021-04-28 12:09:57 -04:00
Cody Henthorne eddaad3b05 Bump version to 5.9.4 2021-04-26 16:44:26 -04:00
Cody Henthorne f2c80c800c Updated language translations. 2021-04-26 16:44:26 -04:00
Greyson Parrelli a0e787e424 Disable additional storage service validations for internal users. 2021-04-26 16:44:26 -04:00
Cody Henthorne c9d1fb8533 Fix reaction notification data inconsistencies.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2021-04-26 16:44:26 -04:00
Greyson Parrelli 006eebb09e Remove storageIds on rows that have no other identifier. 2021-04-26 16:44:26 -04:00
Alex Hart 4aec824bfd Retain minimum width for link previews. 2021-04-26 16:44:26 -04:00
Alex Hart 56f7564ce4 Re-enable internal sharing to SMS users. 2021-04-26 15:45:25 -03:00
Alex Hart f89daefd43 Hide and show video players as content changes. 2021-04-26 15:03:06 -03:00
Cody Henthorne 8572a2d262 Bump version to 5.9.3 2021-04-24 15:06:34 -04:00
Cody Henthorne d6e41be4b4 Updated language translations. 2021-04-24 15:05:44 -04:00
Cody Henthorne 5e715ffcce Fix crash when including self in contact search projection. 2021-04-24 14:55:00 -04:00
Cody Henthorne 5100341e60 Fix leaked receiver when call does not connect. 2021-04-24 14:34:16 -04:00
Cody Henthorne 5ca4db6ea5 Fix crashes and issues with blurred avatars.
- Tinting on Android 5/6 caused NPE deep in Android
- Invite group flow can have zero members
- Missed spot to blur avatar in old notification flow
2021-04-24 14:32:02 -04:00
Alex Hart e02e07ae7a Bump version to 5.9.2 2021-04-23 16:43:45 -03:00
Alex Hart f3caedc045 Updated language translations. 2021-04-23 16:38:23 -03:00
Cody Henthorne 59c49254e7 Insert temporary warning update message during message request state. 2021-04-23 15:29:59 -04:00
Cody Henthorne ad81b310e3 Blur avatar photos from unknown senders when in message request state. 2021-04-23 14:42:51 -04:00
Alex Hart bf124b87fa Fix bad flag parsing on attachment pointers. 2021-04-22 14:46:20 -03:00
Alex Hart a4868602b5 Add better handling for non-existent response bodies or empty responses from giphy. 2021-04-22 14:33:56 -03:00
Greyson Parrelli 763aeabddd Bump version to 5.9.1 2021-04-21 16:43:59 -04:00
Greyson Parrelli 71fc5af320 Updated language translations. 2021-04-21 16:43:38 -04:00
Greyson Parrelli d28f0e3544 Relax GIF size restrictions. 2021-04-21 16:43:38 -04:00
Greyson Parrelli b4d0dde129 Always get the storage manifest for internal users.
This will hopefully help us track down some of the validation issues
when writing local changes.
2021-04-21 16:43:38 -04:00
Alex Hart 281630e751 Add support for inline video playback of gifs in Conversation. 2021-04-21 16:43:38 -04:00
Karalix 32d79ead15 Add unicode wildcards for equivalent latin characters in contact search. 2021-04-21 16:43:38 -04:00
Martin d'Allens 5a91c7e84a Remove seconds from screen lock timeout input for coherence
Configure the TimeDurationPickerDialog to hide seconds.
Seconds were already ignored below 1min. This avoids the user expecting it to work.

Feature regression: after this change, seconds above 1min will also be impossible to input (ex: 1m30s).
But it makes little sense anyway to allow it: they are even less useful for longer durations.

Another possibility to reach a point where eveything is coherent would have been to just remove the Math.max(..., 60) that ignored seconds.

The duration will be displayed as "xx:xx:00" to make it clear that xx:xx represents minutes.

Fixes #10788.
2021-04-21 16:43:38 -04:00
Alex Hart e2e1200c89 Improvments to MP4 giphy fragment behaviour. 2021-04-21 16:43:38 -04:00
Cody Henthorne ed1be76606 Scrub domains from debug logs. 2021-04-19 20:28:54 -04:00
Greyson Parrelli a64de91781 Bump version to 5.9.0 2021-04-19 18:13:05 -04:00
Greyson Parrelli e802c8b8cc Updated language translations. 2021-04-19 18:11:59 -04:00
Alex Hart 86f2cf0ac4 Remove support for linear gif flow. 2021-04-19 18:11:56 -04:00
Alex Hart a844a6b6c1 Revert "Temporarily block payments in all regions."
This reverts commit 1169331462.
2021-04-19 18:11:56 -04:00
Alex Hart c31146e902 Render gifs in gif search as MP4s. 2021-04-19 18:11:56 -04:00
Greyson Parrelli fcc5db2fe6 Bump version to 5.8.10 2021-04-19 17:58:08 -04:00
Greyson Parrelli 9fdd3ae1be Updated language translations. 2021-04-19 17:56:33 -04:00
Greyson Parrelli c99509a967 Temporarily disable some storage service validations. 2021-04-19 10:26:32 -04:00
Alex Hart 1169331462 Temporarily block payments in all regions. 2021-04-19 11:20:41 -03:00
Greyson Parrelli 0978822939 Bump version to 5.8.9 2021-04-18 11:27:59 -04:00
Greyson Parrelli 8bf8ecf7fa Reset manifest version to zero after account restore. 2021-04-18 11:27:59 -04:00
Greyson Parrelli f55db6a5d7 Bump version to 5.8.8 2021-04-18 11:02:30 -04:00
Greyson Parrelli e82d6cf91d Updated language translations. 2021-04-18 10:45:34 -04:00
Greyson Parrelli 65a1d165ac Fix issue where storage dirty state wasn't cleared. 2021-04-18 10:36:40 -04:00
Greyson Parrelli e3b27bd39c Bump version to 5.8.7 2021-04-17 13:03:16 -04:00
Greyson Parrelli 07dde01c3b Updated language translations. 2021-04-17 13:03:16 -04:00
Greyson Parrelli 11a2e8686c Simplify local changes written to storage service. 2021-04-17 13:03:11 -04:00
Greyson Parrelli daeeb17142 Restore storage manifest version during StorageAccountRestoreJob. 2021-04-17 10:31:54 -04:00
Greyson Parrelli 35ab2f6704 Rename 'key' to 'id' where appropriate for storage service. 2021-04-17 10:14:59 -04:00
Greyson Parrelli 9c428f6db7 Bump version to 5.8.6 2021-04-16 19:04:08 -04:00
Greyson Parrelli a76b067b96 Updated language translations. 2021-04-16 19:03:44 -04:00
Cody Henthorne a843619c5b Fix various notification display issues and properly support reply. 2021-04-16 18:59:21 -04:00
Greyson Parrelli 5bbc4aea95 Default storage sync feature flag to enabled. 2021-04-16 18:59:21 -04:00
Greyson Parrelli 4676043826 Simplify storage sync write construction.
Instead of trying to keep track of changes as we go and hope that lines
up with reality, now we just write all of our changes and do another
diff at the end to build our insert/delete set. Nice and simple.
2021-04-16 18:58:36 -04:00
Greyson Parrelli 64a841487f Fix storage sync validation error when re-registering with a new number. 2021-04-16 13:22:01 -04:00
Alex Hart cfd69f2da8 Fix issue where we do not display initials for contacts we have names for. 2021-04-16 10:37:23 -03:00
Alex Hart e97a14f617 Prevent crash when thumbnail decoder cannot stop, log instead. 2021-04-16 09:29:58 -03:00
Cody Henthorne b7118b6bd8 Fix view once media in notifications. 2021-04-15 23:36:15 -04:00
Greyson Parrelli 742da4ccb8 Bump version to 5.8.5 2021-04-15 18:13:11 -04:00
Cody Henthorne 97296ca7d7 Fix bugs with preference and lock state changes. 2021-04-15 15:08:27 -04:00
Greyson Parrelli 1486a9ae1b Bump version to 5.8.4 2021-04-15 12:30:39 -04:00
Greyson Parrelli a847e385cb Updated language translations. 2021-04-15 12:30:13 -04:00
Greyson Parrelli 134284723b Fix storage service crash when matching a local GV2 group without a master key. 2021-04-15 12:07:00 -04:00
Greyson Parrelli a6eb44ba95 Log when there's local storage inserts. 2021-04-15 11:45:24 -04:00
Cody Henthorne 1457738905 Fix bug with notification privacy and bubbles. 2021-04-15 11:35:42 -04:00
Greyson Parrelli 3dd0a60555 Rotate storage service V2 feature flag. 2021-04-15 11:08:29 -04:00
Greyson Parrelli 1eef18dcd3 Add a failsafe for deleting storageIds. 2021-04-15 11:08:29 -04:00
Greyson Parrelli c86ee33371 Skip new validations in old storage sync job. 2021-04-15 11:08:29 -04:00
Greyson Parrelli 60690208de Fix storage service crash when matching a local contact without an ID.
It's possible that we could match a local contact that doesn't have a
storageId, which would crash when we tried to make a model from it for
merging. This isn't an impossible case -- it could be that the manifest
has record of a user that is newly registered (or just registered at
some point and never deleted) and so we need to give our local record a
storageId for merging.
2021-04-15 11:08:29 -04:00
Cody Henthorne 69ebee3eeb Fix notification thumbnail being shown when content should be hidden. 2021-04-15 10:24:06 -04:00
Cody Henthorne 4bdb367c19 Fix thumbnail being shown for reaction notifications. 2021-04-15 10:02:53 -04:00
1776 changed files with 104913 additions and 37662 deletions
+3
View File
@@ -24,6 +24,9 @@ jobs:
- name: Validate Gradle Wrapper
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
run: ./gradlew qa
+193
View File
@@ -0,0 +1,193 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="240" />
<option name="FORMATTER_TAGS_ENABLED" value="true" />
<option name="SOFT_MARGINS" value="160" />
<JavaCodeStyleSettings>
<option name="GENERATE_FINAL_LOCALS" value="true" />
<option name="DO_NOT_WRAP_AFTER_SINGLE_ANNOTATION" value="true" />
<option name="ALIGN_MULTILINE_ANNOTATION_PARAMETERS" value="true" />
<option name="ALIGN_MULTILINE_TEXT_BLOCKS" value="true" />
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value />
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="JAVA">
<option name="BRACE_STYLE" value="5" />
<option name="CLASS_BRACE_STYLE" value="5" />
<option name="METHOD_BRACE_STYLE" value="5" />
<option name="ALIGN_MULTILINE_CHAINED_METHODS" value="true" />
<option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="true" />
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="true" />
<option name="ALIGN_MULTILINE_ASSIGNMENT" value="true" />
<option name="ALIGN_MULTILINE_TERNARY_OPERATION" value="true" />
<option name="ALIGN_MULTILINE_THROWS_LIST" value="true" />
<option name="ALIGN_MULTILINE_EXTENDS_LIST" value="true" />
<option name="ALIGN_MULTILINE_ARRAY_INITIALIZER_EXPRESSION" value="true" />
<option name="ALIGN_GROUP_FIELD_DECLARATIONS" value="true" />
<option name="ALIGN_CONSECUTIVE_VARIABLE_DECLARATIONS" value="true" />
<option name="ALIGN_CONSECUTIVE_ASSIGNMENTS" value="true" />
<option name="SPACE_WITHIN_ARRAY_INITIALIZER_BRACES" value="true" />
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
<option name="WRAP_FIRST_METHOD_IN_CALL_CHAIN" value="true" />
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
<option name="KEEP_SIMPLE_BLOCKS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_METHODS_IN_ONE_LINE" value="true" />
<option name="KEEP_SIMPLE_LAMBDAS_IN_ONE_LINE" value="true" />
<option name="KEEP_MULTIPLE_EXPRESSIONS_IN_ONE_LINE" value="true" />
<option name="METHOD_ANNOTATION_WRAP" value="0" />
<option name="CLASS_ANNOTATION_WRAP" value="0" />
<option name="FIELD_ANNOTATION_WRAP" value="0" />
<option name="ENUM_CONSTANTS_WRAP" value="5" />
<option name="WRAP_ON_TYPING" value="0" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
<arrangement>
<groups>
<group>
<type>GETTERS_AND_SETTERS</type>
<order>KEEP</order>
</group>
<group>
<type>OVERRIDDEN_METHODS</type>
<order>KEEP</order>
</group>
<group>
<type>DEPENDENT_METHODS</type>
<order>BREADTH_FIRST</order>
</group>
</groups>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</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>
</component>
+5
View File
@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
+115 -21
View File
@@ -11,6 +11,8 @@ apply plugin: 'witness'
apply plugin: 'org.jlleitschuh.gradle.ktlint'
apply from: 'translations.gradle'
apply from: 'witness-verifications.gradle'
apply plugin: 'org.jetbrains.kotlin.android'
apply plugin: 'app.cash.exhaustive'
repositories {
maven {
@@ -25,12 +27,6 @@ repositories {
includeGroupByRegex "com\\.github\\.dmytrodanylyk\\.circular-progress-button\\.*"
}
}
maven {
url "https://raw.github.com/signalapp/maven/master/sqlcipher/release/"
content {
includeGroupByRegex "org\\.signal.*"
}
}
maven { // textdrawable
url 'https://dl.bintray.com/amulyakhare/maven'
content {
@@ -61,8 +57,8 @@ protobuf {
}
}
def canonicalVersionCode = 819
def canonicalVersionName = "5.8.3"
def canonicalVersionCode = 893
def canonicalVersionName = "5.20.1"
def postFixSize = 100
def abiPostFix = ['universal' : 0,
@@ -80,6 +76,11 @@ android {
flavorDimensions 'distribution', 'environment'
useLibrary 'org.apache.http.legacy'
kotlinOptions {
jvmTarget = "1.8"
freeCompilerArgs = ["-Xallow-result-return-type"]
}
dexOptions {
javaMaxHeapSize "4g"
}
@@ -117,6 +118,8 @@ android {
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\""
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_NAMES", "new String[]{\"Test\", \"Staging\"}"
buildConfigField "String[]", "SIGNAL_SFU_INTERNAL_URLS", "new String[]{\"https://sfu.test.voip.signal.org\", \"https://sfu.staging.voip.signal.org\"}"
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
@@ -131,6 +134,12 @@ android {
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "String", "DEFAULT_CURRENCIES", "\"EUR,AUD,GBP,CAD,CNY\""
buildConfigField "int[]", "MOBILE_COIN_REGIONS", "new int[]{44}"
buildConfigField "String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\""
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\""
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
@@ -196,28 +205,34 @@ android {
'proguard/proguard.cfg'
testProguardFiles 'proguard/proguard-automation.pro',
'proguard/proguard.cfg'
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Debug\""
}
flipper {
initWith debug
isDefault false
minifyEnabled false
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Flipper\""
}
release {
minifyEnabled true
proguardFiles = buildTypes.debug.proguardFiles
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Release\""
}
perf {
initWith debug
isDefault false
debuggable false
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Perf\""
}
mock {
initWith debug
isDefault false
minifyEnabled false
matchingFallbacks = ['debug']
buildConfigField "String", "BUILD_VARIANT_TYPE", "\"Mock\""
}
}
@@ -228,6 +243,7 @@ android {
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"play\""
}
website {
@@ -235,6 +251,7 @@ android {
ext.websiteUpdateUrl = "https://updates.signal.org/android"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "true"
buildConfigField "String", "NOPLAY_UPDATE_URL", "\"$ext.websiteUpdateUrl\""
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"website\""
}
internal {
@@ -242,6 +259,16 @@ android {
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"internal\""
}
nightly {
dimension 'distribution'
versionNameSuffix "-nightly-untagged-${getDateSuffix()}"
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\""
}
study {
@@ -251,6 +278,7 @@ android {
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "String", "BUILD_DISTRIBUTION_TYPE", "\"study\""
}
prod {
@@ -259,6 +287,7 @@ android {
isDefault true
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"mainnet\""
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Prod\""
}
staging {
@@ -280,18 +309,29 @@ android {
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdls=\""
buildConfigField "String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\""
buildConfigField "String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\""
buildConfigField "String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\""
}
}
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
def abiName = output.getFilter("ABI") ?: 'universal'
def postFix = abiPostFix.get(abiName, 0)
if (output.baseName.contains('nightly')) {
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
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
}
}
}
@@ -304,6 +344,12 @@ android {
variant.setIgnore(true)
} else if (distribution != 'study' && buildType == 'mock') {
variant.setIgnore(true)
} else if (distribution == 'internal' && buildType != 'flipper' && buildType != 'perf' && buildType != 'release') {
variant.setIgnore(true)
} else if (distribution == 'nightly' && environment != 'prod') {
variant.setIgnore(true)
} else if (distribution == 'nightly' && buildType != 'flipper' && buildType != 'perf' && buildType != 'release') {
variant.setIgnore(true)
}
}
@@ -321,6 +367,8 @@ android {
}
dependencies {
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.fragment:fragment-ktx:1.3.5'
lintChecks project(':lintchecks')
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
@@ -328,8 +376,9 @@ dependencies {
implementation ('androidx.appcompat:appcompat:1.2.0') {
force = true
}
implementation "androidx.window:window:1.0.0-alpha09"
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
@@ -343,6 +392,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-savedstate:1.0.0-alpha05'
implementation 'androidx.lifecycle:lifecycle-common-java8:2.1.0'
implementation 'androidx.lifecycle:lifecycle-reactivestreams-ktx:2.1.0'
implementation "androidx.camera:camera-core:1.0.0-beta11"
implementation "androidx.camera:camera-camera2:1.0.0-beta11"
implementation "androidx.camera:camera-lifecycle:1.0.0-beta11"
@@ -350,8 +400,9 @@ dependencies {
implementation "androidx.concurrent:concurrent-futures:1.0.0"
implementation "androidx.autofill:autofill:1.0.0"
implementation "androidx.biometric:biometric:1.1.0"
implementation "androidx.sharetarget:sharetarget:1.1.0"
implementation ('com.google.firebase:firebase-messaging:20.2.0') {
implementation ('com.google.firebase:firebase-messaging:22.0.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
@@ -374,16 +425,16 @@ dependencies {
implementation project(':device-transfer')
implementation 'org.signal:zkgroup-android:0.7.0'
implementation 'org.whispersystems:signal-client-android:0.1.7'
implementation 'org.whispersystems:signal-client-android:0.8.3'
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
implementation('com.mobilecoin:android-sdk:1.0.0') {
implementation('com.mobilecoin:android-sdk:1.1.0') {
exclude group: 'com.google.protobuf'
}
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.9.4'
implementation 'org.signal:ringrtc-android:2.10.7'
implementation "me.leolin:ShortcutBadger:1.1.22"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
@@ -425,15 +476,18 @@ dependencies {
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
implementation "net.zetetic:android-database-sqlcipher:4.4.3"
implementation "androidx.sqlite:sqlite:2.1.0"
implementation ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
exclude group: 'com.fasterxml.jackson.core'
exclude group: 'org.freemarker'
}
implementation 'dnsjava:dnsjava:2.1.9'
flipperImplementation 'com.facebook.flipper:flipper:0.32.2'
flipperImplementation 'com.facebook.soloader:soloader:0.8.2'
flipperImplementation 'com.facebook.flipper:flipper:0.91.0'
flipperImplementation 'com.facebook.soloader:soloader:0.10.1'
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.11.1'
@@ -450,10 +504,16 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.4'
testImplementation 'org.hamcrest:hamcrest:2.2'
testImplementation(testFixtures(project(":libsignal-service")))
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "com.fasterxml.jackson.module:jackson-module-kotlin:2.12.0"
implementation 'io.reactivex.rxjava3:rxandroid:3.0.0'
implementation 'io.reactivex.rxjava3:rxkotlin:3.0.1'
}
dependencyVerification {
@@ -522,6 +582,10 @@ task signProductionWebsiteRelease {
}
def getLastCommitTimestamp() {
if (!(new File('.git').exists())) {
return System.currentTimeMillis().toString()
}
new ByteArrayOutputStream().withStream { os ->
def result = exec {
executable = 'git'
@@ -534,6 +598,10 @@ def getLastCommitTimestamp() {
}
def getGitHash() {
if (!(new File('.git').exists())) {
return "abcd1234"
}
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
@@ -542,6 +610,26 @@ def getGitHash() {
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) {
return output.split('\n')[0];
} else {
return null
}
}
tasks.withType(Test) {
testLogging {
events "failed"
@@ -562,3 +650,9 @@ def loadKeystoreProperties(filename) {
return null;
}
}
def getDateSuffix() {
def date = new Date()
def formattedDate = date.format('yyyy-MM-dd-HH:mm')
return formattedDate
}
+2
View File
@@ -9,3 +9,5 @@
# Protobuf lite
-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }
-keep class androidx.window.** { *; }
@@ -17,6 +17,7 @@ import net.sqlcipher.database.SQLiteStatement;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.util.Hex;
import java.lang.reflect.Field;
import java.util.ArrayList;
@@ -49,11 +50,13 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
SignalDatabase metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext());
return Arrays.asList(new Descriptor(mainOpenHelper),
new Descriptor(keyValueOpenHelper),
new Descriptor(megaphoneOpenHelper),
new Descriptor(jobManagerOpenHelper));
new Descriptor(jobManagerOpenHelper),
new Descriptor(metricsOpenHelper));
} catch (Exception e) {
Log.i(TAG, "Unable to use reflection to access raw database.", e);
}
@@ -237,7 +240,12 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
case Cursor.FIELD_TYPE_FLOAT:
return cursor.getDouble(column);
case Cursor.FIELD_TYPE_BLOB:
return cursor.getBlob(column);
byte[] blob = cursor.getBlob(column);
String bytes = blob != null ? "(blob) " + Hex.toStringCondensed(Arrays.copyOf(blob, Math.min(blob.length, 32))) : null;
if (bytes != null && bytes.length() == 32 && blob.length > 32) {
bytes += "...";
}
return bytes;
case Cursor.FIELD_TYPE_STRING:
default:
return cursor.getString(column);
+50 -34
View File
@@ -96,6 +96,7 @@
android:label="@string/app_name"
android:supportsRtl="true"
tools:replace="android:allowBackup"
android:resizeableActivity="true"
android:allowBackup="false"
android:theme="@style/TextSecure.LightTheme"
android:largeHeap="true">
@@ -104,6 +105,9 @@
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyCSx9xea86GwDKGznCAULE9Y5a8b-TfN9U"/>
<meta-data android:name="android.supports_size_changes"
android:value="true" />
<meta-data android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
@@ -117,16 +121,15 @@
<activity android:name=".WebRtcCallActivity"
android:theme="@style/TextSecure.DarkTheme.WebRTCCall"
android:excludeFromRecents="true"
android:screenOrientation="portrait"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:taskAffinity=".calling"
android:resizeableActivity="true"
android:launchMode="singleTask"/>
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
android:theme="@style/TextSecure.DarkNoActionBar"
android:screenOrientation="portrait"
android:noHistory="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -153,6 +156,14 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="tsdevice"/>
</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="sgnl"
android:host="linkdevice"/>
</intent-filter>
</activity>
<activity android:name=".preferences.MmsPreferencesActivity"
@@ -190,7 +201,7 @@
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value=".service.DirectShareService" />
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
</activity>
@@ -264,6 +275,16 @@
<data android:scheme="sgnl"
android:host="signal.tube" />
</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 android:name=".conversation.ConversationActivity"
@@ -300,13 +321,10 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
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"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
@@ -314,7 +332,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".migrations.ApplicationMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -358,8 +376,9 @@
<activity android:name=".VerifyIdentityActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ApplicationPreferencesActivity"
<activity android:name=".components.settings.app.AppSettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -367,6 +386,13 @@
</intent-filter>
</activity>
<activity android:name=".components.settings.conversation.ConversationSettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.ConversationSettings"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="stateAlwaysHidden">
@@ -465,6 +491,7 @@
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
android:excludeFromRecents="true"
android:permission="android.permission.CALL_PHONE"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
@@ -486,7 +513,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".scribbles.ImageEditorStickerSelectActivity"
android:theme="@style/TextSecure.DarkTheme"
android:theme="@style/Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".profiles.edit.EditProfileActivity"
@@ -576,6 +603,10 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" />
<activity android:name=".ratelimit.RecaptchaProofActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" />
<activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.FullScreenMedia" />
@@ -585,6 +616,10 @@
android:screenOrientation="portrait"
android:theme="@style/Theme.Signal.WallpaperCropper" />
<activity android:name=".reactions.edit.EditReactionsActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
@@ -630,13 +665,6 @@
<meta-data android:name="android.provider.CONTACTS_STRUCTURE" android:resource="@xml/contactsformat" />
</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=".gcm.FcmFetchService" />
@@ -696,24 +724,12 @@
</intent-filter>
</receiver>
<receiver android:name=".notifications.AndroidAutoHeardReceiver"
android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.notifications.ANDROID_AUTO_HEARD"/>
</intent-filter>
</receiver>
<receiver android:name=".notifications.AndroidAutoReplyReceiver"
android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.notifications.ANDROID_AUTO_REPLY"/>
</intent-filter>
</receiver>
<receiver android:name=".service.ExpirationListener" />
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
<receiver android:name=".service.PendingRetryReceiptManager$PendingRetryReceiptAlarm" />
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
<receiver android:name=".payments.backup.phrase.ClearClipboardAlarmReceiver" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 421 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 365 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 608 KiB

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 552 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 631 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 653 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 652 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 531 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 685 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 603 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 93 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -512,7 +512,12 @@ final class SignalCameraXModule {
return rotationDegrees;
}
@SuppressLint("UnsafeExperimentalUsageError")
public void invalidateView() {
if (mPreview != null) {
mPreview.setTargetRotation(getDisplaySurfaceRotation()); // Fixes issue #10940 (rotation not updated on phones using "Legacy API")
}
updateViewInfo();
}
@@ -8,15 +8,16 @@ public final class AppCapabilities {
private AppCapabilities() {
}
private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
private static final boolean GV1_MIGRATION = true;
private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
private static final boolean GV1_MIGRATION = true;
private static final boolean ANNOUNCEMENT_GROUPS = true;
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION);
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION, FeatureFlags.senderKey(), ANNOUNCEMENT_GROUPS);
}
}
@@ -8,6 +8,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.insights.InsightsOptOut;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
@@ -60,6 +61,7 @@ public final class AppInitialization {
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
EmojiSearchIndexDownloadJob.scheduleImmediately();
}
/**
@@ -18,6 +18,7 @@ package org.thoughtcrime.securesms;
import android.content.Context;
import android.os.Build;
import android.os.SystemClock;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -32,16 +33,21 @@ import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.logging.PersistentLogger;
import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
@@ -50,17 +56,16 @@ import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
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.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
@@ -71,6 +76,7 @@ import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -83,6 +89,9 @@ import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
import java.security.Security;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.plugins.RxJavaPlugins;
import io.reactivex.rxjava3.schedulers.Schedulers;
/**
* Will be called once when the TextSecure process is created.
*
@@ -105,6 +114,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
public void onCreate() {
Tracer.getInstance().start("Application#onCreate()");
AppStartup.getInstance().onApplicationCreate();
SignalLocalMetrics.ColdStart.start();
long startTime = System.currentTimeMillis();
@@ -115,12 +125,16 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
super.onCreate();
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
.addBlocking("sqlcipher-init", () -> SqlCipherLibraryLoader.load(this))
.addBlocking("logging", () -> {
initializeLogging();
Log.i(TAG, "onCreate()");
initializeLogging();
Log.i(TAG, "onCreate()");
})
.addBlocking("crash-handling", this::initializeCrashHandling)
.addBlocking("eat-db", () -> DatabaseFactory.getInstance(this))
.addBlocking("rx-init", () -> {
RxJavaPlugins.setInitIoSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED_IO, true, false));
RxJavaPlugins.setInitComputationSchedulerHandler(schedulerSupplier -> Schedulers.from(SignalExecutors.BOUNDED, true, false));
})
.addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("notification-channels", () -> NotificationChannels.create(this))
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
@@ -143,7 +157,9 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
})
.addBlocking("blob-provider", this::initializeBlobProvider)
.addBlocking("feature-flags", FeatureFlags::init)
.addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager)
.addNonBlocking(this::initializeGcmCheck)
.addNonBlocking(this::initializeSignedPreKeyCheck)
.addNonBlocking(this::initializePeriodicTasks)
@@ -154,10 +170,17 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(RefreshPreKeysJob::scheduleIfNecessary)
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
.addNonBlocking(EmojiSource::refresh)
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> DatabaseFactory.getMessageLogDatabase(this).trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
SignalLocalMetrics.ColdStart.onApplicationCreateFinished();
Tracer.getInstance().end("Application#onCreate()");
}
@@ -228,10 +251,12 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
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);
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
SignalExecutors.UNBOUNDED.execute(() -> LogDatabase.getInstance(this).trimToSize());
}
private void initializeCrashHandling() {
@@ -293,6 +318,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
}
private void initializePendingRetryReceiptManager() {
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
}
private void initializePeriodicTasks() {
RotateSignedPreKeyListener.schedule(this);
DirectoryRefreshListener.schedule(this);
@@ -355,6 +384,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
BlobProvider.getInstance().initialize(this);
}
@WorkerThread
private void cleanAvatarStorage() {
AvatarPickerStorage.cleanOrphans(this);
}
@WorkerThread
private void initializeCleanup() {
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
@@ -1,404 +0,0 @@
/*
* Copyright (C) 2011 Whisper Systems
* Copyright (C) 2013-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.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.PorterDuff;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.preference.Preference;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.conversationlist.model.UnreadPayments;
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData;
import org.thoughtcrime.securesms.help.HelpFragment;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.DataAndStoragePreferenceFragment;
import org.thoughtcrime.securesms.preferences.EditProxyFragment;
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.PaymentsPreference;
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
* The Activity for application preference display and management.
*
* @author Moxie Marlinspike
*
*/
public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
implements SharedPreferences.OnSharedPreferenceChangeListener
{
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment";
public static final String LAUNCH_TO_PROXY_FRAGMENT = "launch.to.proxy.fragment";
public static final String LAUNCH_TO_NOTIFICATIONS_FRAGMENT = "launch.to.notifications.fragment";
@SuppressWarnings("unused")
private static final String TAG = Log.tag(ApplicationPreferencesActivity.class);
private static final String PREFERENCE_CATEGORY_PROFILE = "preference_category_profile";
private static final String PREFERENCE_CATEGORY_USERNAME = "preference_category_username";
private static final String PREFERENCE_CATEGORY_SMS_MMS = "preference_category_sms_mms";
private static final String PREFERENCE_CATEGORY_NOTIFICATIONS = "preference_category_notifications";
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage";
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
private static final String PREFERENCE_CATEGORY_HELP = "preference_category_help";
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
private static final String PREFERENCE_CATEGORY_DONATE = "preference_category_donate";
private static final String PREFERENCE_CATEGORY_PAYMENTS = "preference_category_payments";
private static final String WAS_CONFIGURATION_UPDATED = "was_configuration_updated";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private boolean wasConfigurationUpdated = false;
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
protected void onCreate(Bundle icicle, boolean ready) {
//noinspection ConstantConditions
this.getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (getIntent() != null && getIntent().getCategories() != null && getIntent().getCategories().contains("android.intent.category.NOTIFICATION_PREFERENCES")) {
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
initFragment(android.R.id.content, new BackupsPreferenceFragment());
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_HELP_FRAGMENT, false)) {
Bundle bundle = new Bundle();
bundle.putInt(HelpFragment.START_CATEGORY_INDEX, getIntent().getIntExtra(HelpFragment.START_CATEGORY_INDEX, 0));
initFragment(android.R.id.content, new HelpFragment(), null, bundle);
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_PROXY_FRAGMENT, false)) {
initFragment(android.R.id.content, EditProxyFragment.newInstance());
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_NOTIFICATIONS_FRAGMENT, false)) {
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
} else if (icicle == null) {
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
} else {
wasConfigurationUpdated = icicle.getBoolean(WAS_CONFIGURATION_UPDATED);
}
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
outState.putBoolean(WAS_CONFIGURATION_UPDATED, wasConfigurationUpdated);
super.onSaveInstanceState(outState);
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
super.onActivityResult(requestCode, resultCode, data);
Fragment fragment = getSupportFragmentManager().findFragmentById(android.R.id.content);
fragment.onActivityResult(requestCode, resultCode, data);
}
@Override
public boolean onSupportNavigateUp() {
FragmentManager fragmentManager = getSupportFragmentManager();
if (fragmentManager.getBackStackEntryCount() > 0) {
fragmentManager.popBackStack();
} else {
if (wasConfigurationUpdated) {
setResult(MainActivity.RESULT_CONFIG_CHANGED);
} else {
setResult(RESULT_OK);
}
finish();
}
return true;
}
@Override
public void onBackPressed() {
onSupportNavigateUp();
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(TextSecurePreferences.THEME_PREF)) {
DynamicTheme.setDefaultDayNightMode(this);
recreate();
} else if (key.equals(TextSecurePreferences.LANGUAGE_PREF)) {
CachedInflater.from(this).clear();
wasConfigurationUpdated = true;
recreate();
Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(KeyCachingService.LOCALE_CHANGE_EVENT);
startService(intent);
}
}
public void pushFragment(@NonNull Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
.replace(android.R.id.content, fragment)
.addToBackStack(null)
.commit();
}
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
private final UnreadPaymentsLiveData unreadPaymentsLiveData = new UnreadPaymentsLiveData();
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
this.findPreference(PREFERENCE_CATEGORY_PROFILE)
.setOnPreferenceClickListener(new ProfileClickListener());
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
.setOnPreferenceClickListener(new UsernameClickListener());
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_SMS_MMS));
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_NOTIFICATIONS));
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APP_PROTECTION));
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
this.findPreference(PREFERENCE_CATEGORY_CHATS)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
this.findPreference(PREFERENCE_CATEGORY_STORAGE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_STORAGE));
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
this.findPreference(PREFERENCE_CATEGORY_HELP)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_HELP));
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_ADVANCED));
this.findPreference(PREFERENCE_CATEGORY_DONATE)
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DONATE));
Preference paymentsPreference = this.findPreference(PREFERENCE_CATEGORY_PAYMENTS);
if (SignalStore.paymentsValues().getPaymentsAvailability().showPaymentsMenu()) {
paymentsPreference.setVisible(true);
paymentsPreference.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_PAYMENTS));
} else {
paymentsPreference.setVisible(false);
}
tintIcons();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (SignalStore.paymentsValues().getPaymentsAvailability().showPaymentsMenu()) {
PaymentsPreference paymentsPreference = (PaymentsPreference) this.findPreference(PREFERENCE_CATEGORY_PAYMENTS);
unreadPaymentsLiveData.observe(getViewLifecycleOwner(), unreadPayments -> paymentsPreference.setUnreadCount(unreadPayments.transform(UnreadPayments::getUnreadCount).or(-1)));
}
}
private void tintIcons() {
if (Build.VERSION.SDK_INT >= 21) return;
Preference preference = this.findPreference(PREFERENCE_CATEGORY_SMS_MMS);
preference.getIcon().setColorFilter(ContextCompat.getColor(requireContext(), R.color.signal_icon_tint_primary), PorterDuff.Mode.SRC_IN);
}
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences);
if (FeatureFlags.usernames()) {
UsernamePreference pref = (UsernamePreference) findPreference(PREFERENCE_CATEGORY_USERNAME);
pref.setVisible(shouldDisplayUsernameReminder());
pref.setOnLongClickListener(v -> {
new AlertDialog.Builder(requireContext())
.setMessage(R.string.ApplicationPreferencesActivity_hide_reminder)
.setPositiveButton(R.string.ApplicationPreferencesActivity_hide, (dialog, which) -> {
dialog.dismiss();
SignalStore.misc().hideUsernameReminder();
findPreference(PREFERENCE_CATEGORY_USERNAME).setVisible(false);
})
.setNegativeButton(android.R.string.cancel, ((dialog, which) -> dialog.dismiss()))
.setCancelable(true)
.show();
return true;
});
}
}
@Override
public void onResume() {
super.onResume();
//noinspection ConstantConditions
((ApplicationPreferencesActivity) getActivity()).getSupportActionBar().setTitle(R.string.text_secure_normal__menu_settings);
setCategorySummaries();
setCategoryVisibility();
}
private void setCategorySummaries() {
((ProfilePreference)this.findPreference(PREFERENCE_CATEGORY_PROFILE)).refresh();
if (FeatureFlags.usernames()) {
this.findPreference(PREFERENCE_CATEGORY_USERNAME)
.setVisible(shouldDisplayUsernameReminder());
}
this.findPreference(PREFERENCE_CATEGORY_SMS_MMS)
.setSummary(SmsMmsPreferenceFragment.getSummary(getActivity()));
this.findPreference(PREFERENCE_CATEGORY_NOTIFICATIONS)
.setSummary(NotificationsPreferenceFragment.getSummary(getActivity()));
this.findPreference(PREFERENCE_CATEGORY_APP_PROTECTION)
.setSummary(AppProtectionPreferenceFragment.getSummary(getActivity()));
this.findPreference(PREFERENCE_CATEGORY_APPEARANCE)
.setSummary(AppearancePreferenceFragment.getSummary(getActivity()));
this.findPreference(PREFERENCE_CATEGORY_CHATS)
.setSummary(ChatsPreferenceFragment.getSummary(getActivity()));
}
private void setCategoryVisibility() {
Preference devicePreference = this.findPreference(PREFERENCE_CATEGORY_DEVICES);
if (devicePreference != null && !TextSecurePreferences.isPushRegistered(getActivity())) {
getPreferenceScreen().removePreference(devicePreference);
}
}
private static boolean shouldDisplayUsernameReminder() {
return FeatureFlags.usernames() && !Recipient.self().getUsername().isPresent() && SignalStore.misc().shouldShowUsernameReminder();
}
private class CategoryClickListener implements Preference.OnPreferenceClickListener {
private String category;
CategoryClickListener(String category) {
this.category = category;
}
@Override
public boolean onPreferenceClick(Preference preference) {
Fragment fragment = null;
switch (category) {
case PREFERENCE_CATEGORY_SMS_MMS:
fragment = new SmsMmsPreferenceFragment();
break;
case PREFERENCE_CATEGORY_NOTIFICATIONS:
fragment = new NotificationsPreferenceFragment();
break;
case PREFERENCE_CATEGORY_APP_PROTECTION:
fragment = new AppProtectionPreferenceFragment();
break;
case PREFERENCE_CATEGORY_APPEARANCE:
fragment = new AppearancePreferenceFragment();
break;
case PREFERENCE_CATEGORY_CHATS:
fragment = new ChatsPreferenceFragment();
break;
case PREFERENCE_CATEGORY_STORAGE:
fragment = new DataAndStoragePreferenceFragment();
break;
case PREFERENCE_CATEGORY_DEVICES:
Intent intent = new Intent(getActivity(), DeviceActivity.class);
startActivity(intent);
break;
case PREFERENCE_CATEGORY_ADVANCED:
fragment = new AdvancedPreferenceFragment();
break;
case PREFERENCE_CATEGORY_HELP:
fragment = new HelpFragment();
break;
case PREFERENCE_CATEGORY_DONATE:
CommunicationActions.openBrowserLink(requireContext(), getString(R.string.donate_url));
break;
case PREFERENCE_CATEGORY_PAYMENTS:
startActivity(new Intent(requireContext(), PaymentsActivity.class));
break;
default:
throw new AssertionError();
}
if (fragment != null) {
Bundle args = new Bundle();
fragment.setArguments(args);
((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
}
return true;
}
}
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
requireActivity().startActivity(ManageProfileActivity.getIntent(requireActivity()));
return true;
}
}
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
requireActivity().startActivity(ManageProfileActivity.getIntentForUsernameEdit(preference.getContext()));
return true;
}
}
}
}
@@ -11,8 +11,12 @@ import androidx.lifecycle.Observer;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.conversation.ConversationMessage;
import org.thoughtcrime.securesms.conversation.colors.Colorizable;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@@ -20,13 +24,14 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.video.exo.AttachmentMediaSourceFactory;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
import java.util.Locale;
import java.util.Set;
public interface BindableConversationItem extends Unbindable {
public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, Colorizable {
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord,
@@ -38,12 +43,19 @@ public interface BindableConversationItem extends Unbindable {
@Nullable String searchQuery,
boolean pulseMention,
boolean hasWallpaper,
boolean isMessageRequestAccepted);
boolean isMessageRequestAccepted,
@NonNull AttachmentMediaSourceFactory attachmentMediaSourceFactory,
boolean canPlayInline,
@NonNull Colorizer colorizer);
ConversationMessage getConversationMessage();
void setEventListener(@Nullable EventListener listener);
default void updateTimestamps() {
// Intentionally Blank.
}
interface EventListener {
void onQuoteClicked(MmsMessageRecord messageRecord);
void onLinkPreviewClicked(@NonNull LinkPreview linkPreview);
@@ -57,17 +69,24 @@ public interface BindableConversationItem extends Unbindable {
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
void onMessageWithRecaptchaNeededClicked(@NonNull MessageRecord messageRecord);
void onIncomingIdentityMismatchClicked(@NonNull RecipientId recipientId);
void onRegisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onUnregisterVoiceNoteCallbacks(@NonNull Observer<VoiceNotePlaybackState> onPlaybackStartObserver);
void onVoiceNotePause(@NonNull Uri uri);
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
void onVoiceNotePlaybackSpeedChanged(@NonNull Uri uri, float speed);
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
void onDecryptionFailedLearnMoreClicked();
void onChatSessionRefreshLearnMoreClicked();
void onBadDecryptLearnMoreClicked(@NonNull RecipientId author);
void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient);
void onJoinGroupCallClicked();
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
void onEnableCallNotificationsClicked();
void onPlayInlineContent(ConversationMessage conversationMessage);
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);
@@ -9,6 +9,8 @@ import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.Lifecycle;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@@ -30,15 +32,15 @@ public final class BlockUnblockDialog {
AlertDialog.Builder::show);
}
public static void showBlockAndDeleteFor(@NonNull Context context,
@NonNull Lifecycle lifecycle,
@NonNull Recipient recipient,
@NonNull Runnable onBlock,
@NonNull Runnable onBlockAndDelete)
public static void showBlockAndReportSpamFor(@NonNull Context context,
@NonNull Lifecycle lifecycle,
@NonNull Recipient recipient,
@NonNull Runnable onBlock,
@NonNull Runnable onBlockAndReportSpam)
{
SimpleTask.run(lifecycle,
() -> buildBlockFor(context, recipient, onBlock, onBlockAndDelete),
AlertDialog.Builder::show);
() -> buildBlockFor(context, recipient, onBlock, onBlockAndReportSpam),
AlertDialog.Builder::show);
}
public static void showUnblockFor(@NonNull Context context,
@@ -55,11 +57,11 @@ public final class BlockUnblockDialog {
private static AlertDialog.Builder buildBlockFor(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull Runnable onBlock,
@Nullable Runnable onBlockAndDelete)
@Nullable Runnable onBlockAndReportSpam)
{
recipient = recipient.resolve();
AlertDialog.Builder builder = new AlertDialog.Builder(context);
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
Resources resources = context.getResources();
if (recipient.isGroup()) {
@@ -78,10 +80,10 @@ public final class BlockUnblockDialog {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
if (onBlockAndDelete != null) {
if (onBlockAndReportSpam != null) {
builder.setNeutralButton(android.R.string.cancel, null);
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_delete, (d, w) -> onBlockAndDelete.run());
builder.setNegativeButton(R.string.BlockUnblockDialog_block, (d, w) -> onBlock.run());
builder.setNegativeButton(R.string.BlockUnblockDialog_report_spam_and_block, (d, w) -> onBlockAndReportSpam.run());
builder.setPositiveButton(R.string.BlockUnblockDialog_block, (d, w) -> onBlock.run());
} else {
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
@@ -98,7 +100,7 @@ public final class BlockUnblockDialog {
{
recipient = recipient.resolve();
AlertDialog.Builder builder = new AlertDialog.Builder(context);
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context);
Resources resources = context.getResources();
if (recipient.isGroup()) {
@@ -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,21 +20,23 @@ import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
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.sync.DirectoryHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.function.Consumer;
/**
* Base activity container for selecting a list of contacts.
@@ -55,7 +57,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
protected ContactSelectionListFragment contactsFragment;
private ContactFilterToolbar toolbar;
private Toolbar toolbar;
private ContactFilterView contactFilterView;
@Override
protected void onPreCreate() {
@@ -65,13 +68,14 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
@Override
protected void onCreate(Bundle icicle, boolean ready) {
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
int displayMode = TextSecurePreferences.isSmsEnabled(this) ? DisplayMode.FLAG_ALL
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
int displayMode = Util.isDefaultSmsProvider(this) ? DisplayMode.FLAG_ALL
: DisplayMode.FLAG_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
}
setContentView(getIntent().getIntExtra(EXTRA_LAYOUT_RES_ID, R.layout.contact_selection_activity));
initializeContactFilterView();
initializeToolbar();
initializeResources();
initializeSearch();
@@ -83,16 +87,23 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
dynamicTheme.onResume(this);
}
protected ContactFilterToolbar getToolbar() {
protected Toolbar getToolbar() {
return toolbar;
}
protected ContactFilterView getContactFilterView() {
return contactFilterView;
}
private void initializeContactFilterView() {
this.contactFilterView = findViewById(R.id.contact_filter_edit_text);
}
private void initializeToolbar() {
this.toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
getSupportActionBar().setDisplayShowTitleEnabled(false);
getSupportActionBar().setIcon(null);
getSupportActionBar().setLogo(null);
}
@@ -103,7 +114,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
}
private void initializeSearch() {
toolbar.setOnFilterChangedListener(filter -> contactsFragment.setQueryFilter(filter));
contactFilterView.setOnFilterChangedListener(filter -> contactsFragment.setQueryFilter(filter));
}
@Override
@@ -112,8 +123,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
return true;
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
callback.accept(true);
}
@Override
@@ -154,7 +165,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
ContactSelectionActivity activity = this.activity.get();
if (activity != null && !activity.isFinishing()) {
activity.toolbar.clear();
activity.contactFilterView.clear();
activity.contactsFragment.resetQueryFilter();
}
}
@@ -53,17 +53,19 @@ import androidx.transition.TransitionManager;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.signal.core.util.logging.Log;
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.ContactChip;
import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter;
import org.thoughtcrime.securesms.contacts.ContactSelectionListItem;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.LetterHeaderDecoration;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.groups.SelectionLimits;
@@ -74,7 +76,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -88,6 +89,7 @@ import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
/**
* Fragment for selecting a one or more contacts from a list.
@@ -114,6 +116,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
public static final String HIDE_COUNT = "hide_count";
public static final String CAN_SELECT_SELF = "can_select_self";
public static final String DISPLAY_CHIPS = "display_chips";
public static final String RV_PADDING_BOTTOM = "recycler_view_padding_bottom";
public static final String RV_CLIP = "recycler_view_clipping";
private ConstraintLayout constraintLayout;
private TextView emptyText;
@@ -129,9 +133,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private WarningTextView groupLimit;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private AbstractContactsCursorLoaderFactoryProvider cursorFactoryProvider;
private View shadowView;
private ToolbarShadowAnimationHelper toolbarShadowAnimationHelper;
@Nullable private FixedViewsAdapter headerAdapter;
@@ -231,9 +236,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
showContactsProgress = view.findViewById(R.id.progress);
chipGroup = view.findViewById(R.id.chipGroup);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
groupLimit = view.findViewById(R.id.group_limit);
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.setItemAnimator(new DefaultItemAnimator() {
@Override
@@ -245,6 +253,15 @@ public final class ContactSelectionListFragment extends LoggingFragment
Intent intent = requireActivity().getIntent();
Bundle arguments = safeArguments();
int recyclerViewPadBottom = arguments.getInt(RV_PADDING_BOTTOM, intent.getIntExtra(RV_PADDING_BOTTOM, -1));
boolean recyclerViewClipping = arguments.getBoolean(RV_CLIP, intent.getBooleanExtra(RV_CLIP, true));
if (recyclerViewPadBottom != -1) {
ViewUtil.setPaddingBottom(recyclerView, recyclerViewPadBottom);
}
recyclerView.setClipToPadding(recyclerViewClipping);
swipeRefresh.setEnabled(arguments.getBoolean(REFRESHABLE, intent.getBooleanExtra(REFRESHABLE, true)));
hideCount = arguments.getBoolean(HIDE_COUNT, intent.getBooleanExtra(HIDE_COUNT, false));
@@ -261,8 +278,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
currentSelection = getCurrentSelection();
updateGroupLimit(getChipCount());
return view;
}
@@ -270,13 +285,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
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
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@@ -298,6 +306,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
return cursorRecyclerViewAdapter.getSelectedContactsCount();
}
public int getTotalMemberCount() {
if (cursorRecyclerViewAdapter == null) {
return 0;
}
return cursorRecyclerViewAdapter.getSelectedContactsCount() + cursorRecyclerViewAdapter.getCurrentContactsCount();
}
private Set<RecipientId> getCurrentSelection() {
List<RecipientId> currentSelection = safeArguments().getParcelableArrayList(CURRENT_SELECTION);
if (currentSelection == null) {
@@ -338,8 +354,8 @@ public final class ContactSelectionListFragment extends LoggingFragment
concatenateAdapter.addAdapter(footerAdapter);
}
recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
recyclerView.setAdapter(concatenateAdapter);
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true, 0));
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
@@ -350,6 +366,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
}
});
if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged();
}
}
private boolean hideLetterHeaders() {
return hasQueryFilter() || shouldDisplayRecents();
}
private View createInviteActionView(@NonNull ListCallback listCallback) {
@@ -418,7 +442,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
FragmentActivity activity = requireActivity();
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) {
return cursorFactoryProvider.get().create();
@@ -464,6 +488,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
fastScroller.setVisibility(View.GONE);
}
private boolean shouldDisplayRecents() {
return safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false));
}
@SuppressLint("StaticFieldLeak")
private void handleContactPermissionGranted() {
final Context context = requireContext();
@@ -540,28 +568,32 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null, allowed -> {
if (allowed) {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
});
} else {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
new MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.ContactSelectionListFragment_username_not_found)
.setMessage(getString(R.string.ContactSelectionListFragment_s_is_not_a_signal_user, contact.getNumber()))
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
.show();
}
});
} else {
if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber(), allowed -> {
if (allowed) {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
});
} else {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
@@ -595,12 +627,19 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (isMulti) {
addChipForSelectedContact(selectedContact);
}
if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged();
}
}
private void markContactUnselected(@NonNull SelectedContact selectedContact) {
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
removeChipForContact(selectedContact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onSelectionChanged();
}
}
private void removeChipForContact(@NonNull SelectedContact contact) {
@@ -611,8 +650,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
}
updateGroupLimit(getChipCount());
if (getChipCount() == 0) {
setChipGroupVisibility(ConstraintSet.GONE);
}
@@ -662,7 +699,6 @@ public final class ContactSelectionListFragment extends LoggingFragment
private void addChip(@NonNull ContactChip chip) {
chipGroup.addView(chip);
updateGroupLimit(getChipCount());
if (selectionWarningLimitReachedExactly()) {
if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
@@ -712,9 +748,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
public interface OnContactSelectedListener {
/** @return True if the contact is allowed to be selected, otherwise false. */
boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number);
/** Provides an opportunity to disallow selecting an item. Call the callback with false to disallow, or true to allow it. */
void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback);
void onContactDeselected(Optional<RecipientId> recipientId, String number);
void onSelectionChanged();
}
public interface OnSelectionLimitReachedListener {
@@ -16,6 +16,7 @@ import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import org.signal.core.util.ThreadUtil;
@@ -27,6 +28,7 @@ import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.ScanListener;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
@@ -47,7 +49,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
private static final String TAG = Log.tag(DeviceActivity.class);
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private DeviceAddFragment deviceAddFragment;
@@ -62,9 +64,14 @@ public class DeviceActivity extends PassphraseRequiredActivity
@Override
public void onCreate(Bundle bundle, boolean ready) {
getSupportActionBar().setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_arrow_left_24));
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.AndroidManifest__linked_devices);
setContentView(R.layout.device_activity);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
requireSupportActionBar().setDisplayHomeAsUpEnabled(true);
requireSupportActionBar().setTitle(R.string.AndroidManifest__linked_devices);
this.deviceAddFragment = new DeviceAddFragment();
this.deviceListFragment = new DeviceListFragment();
this.deviceLinkFragment = new DeviceLinkFragment();
@@ -73,20 +80,10 @@ public class DeviceActivity extends PassphraseRequiredActivity
this.deviceAddFragment.setScanListener(this);
if (getIntent().getBooleanExtra("add", false)) {
initFragment(android.R.id.content, deviceAddFragment, dynamicLanguage.getCurrentLocale());
initFragment(R.id.fragment_container, deviceAddFragment, dynamicLanguage.getCurrentLocale());
} else {
initFragment(android.R.id.content, deviceListFragment, dynamicLanguage.getCurrentLocale());
initFragment(R.id.fragment_container, deviceListFragment, dynamicLanguage.getCurrentLocale());
}
overridePendingTransition(R.anim.slide_from_end, R.anim.slide_to_start);
}
@Override
protected void onPause() {
if (isFinishing()) {
overridePendingTransition(R.anim.slide_from_start, R.anim.slide_to_end);
}
super.onPause();
}
@Override
@@ -98,8 +95,9 @@ public class DeviceActivity extends PassphraseRequiredActivity
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
if (item.getItemId() == android.R.id.home) {
finish();
return true;
}
return false;
@@ -113,7 +111,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
.onAllGranted(() -> {
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, deviceAddFragment)
.replace(R.id.fragment_container, deviceAddFragment)
.addToBackStack(null)
.commitAllowingStateLoss();
})
@@ -128,7 +126,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
Uri uri = Uri.parse(data);
deviceLinkFragment.setLinkClickedListener(uri, DeviceActivity.this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= 21) {
deviceAddFragment.setSharedElementReturnTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(R.transition.fragment_shared));
deviceAddFragment.setExitTransition(TransitionInflater.from(DeviceActivity.this).inflateTransition(android.R.transition.fade));
@@ -138,20 +136,21 @@ public class DeviceActivity extends PassphraseRequiredActivity
getSupportFragmentManager().beginTransaction()
.addToBackStack(null)
.addSharedElement(deviceAddFragment.getDevicesImage(), "devices")
.replace(android.R.id.content, deviceLinkFragment)
.replace(R.id.fragment_container, deviceLinkFragment)
.commit();
} else {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.slide_to_bottom,
R.anim.slide_from_bottom, R.anim.slide_to_bottom)
.replace(android.R.id.content, deviceLinkFragment)
.replace(R.id.fragment_container, deviceLinkFragment)
.addToBackStack(null)
.commit();
}
});
}
@SuppressLint("MissingSuperCall")
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@@ -42,9 +42,9 @@ public class DeviceAddFragment extends LoggingFragment {
this.overlay.setOrientation(LinearLayout.VERTICAL);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= 21) {
this.container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@TargetApi(21)
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom)
@@ -80,7 +80,7 @@ public class DeviceAddFragment extends LoggingFragment {
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
this.scannerView.onPause();
@@ -107,6 +107,4 @@ public class DeviceAddFragment extends LoggingFragment {
this.scanningThread.setScanListener(scanListener);
}
}
}
@@ -32,7 +32,7 @@ public class DeviceLinkFragment extends Fragment implements View.OnClickListener
}
@Override
public void onConfigurationChanged(Configuration newConfiguration) {
public void onConfigurationChanged(@NonNull Configuration newConfiguration) {
super.onConfigurationChanged(newConfiguration);
if (newConfiguration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
container.setOrientation(LinearLayout.HORIZONTAL);
@@ -21,6 +21,7 @@ import androidx.fragment.app.ListFragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.melnykov.fab.FloatingActionButton;
import org.signal.core.util.logging.Log;
@@ -52,12 +53,12 @@ public class DeviceListFragment extends ListFragment
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.locale = (Locale) getArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
this.locale = (Locale) requireArguments().getSerializable(PassphraseRequiredActivity.LOCALE_EXTRA);
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
public void onAttach(@NonNull Context context) {
super.onAttach(context);
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
}
@@ -121,42 +122,22 @@ public class DeviceListFragment extends ListFragment
final String deviceName = ((DeviceListItem)view).getDeviceName();
final long deviceId = ((DeviceListItem)view).getDeviceId();
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(getActivity().getString(R.string.DeviceListActivity_unlink_s, deviceName));
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
builder.setTitle(getString(R.string.DeviceListActivity_unlink_s, deviceName));
builder.setMessage(R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive);
builder.setNegativeButton(android.R.string.cancel, null);
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
handleDisconnectDevice(deviceId);
}
});
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> handleDisconnectDevice(deviceId));
builder.show();
}
private void handleLoaderFailed() {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(requireActivity());
builder.setMessage(R.string.DeviceListActivity_network_connection_failed);
builder.setPositiveButton(R.string.DeviceListActivity_try_again,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
}
});
(dialog, which) -> getLoaderManager().restartLoader(0, null, DeviceListFragment.this));
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
DeviceListFragment.this.getActivity().onBackPressed();
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
DeviceListFragment.this.getActivity().onBackPressed();
}
});
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> requireActivity().onBackPressed());
builder.setOnCancelListener(dialog -> requireActivity().onBackPressed());
builder.show();
}
@@ -1,103 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import java.util.Arrays;
import cn.carbswang.android.numberpickerview.library.NumberPickerView;
public class ExpirationDialog extends AlertDialog {
protected ExpirationDialog(Context context) {
super(context);
}
protected ExpirationDialog(Context context, int theme) {
super(context, theme);
}
protected ExpirationDialog(Context context, boolean cancelable, OnCancelListener cancelListener) {
super(context, cancelable, cancelListener);
}
public static void show(final Context context,
final int currentExpiration,
final @NonNull OnClickListener listener)
{
final View view = createNumberPickerView(context, currentExpiration);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(context.getString(R.string.ExpirationDialog_disappearing_messages));
builder.setView(view);
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int selected = ((NumberPickerView)view.findViewById(R.id.expiration_number_picker)).getValue();
listener.onClick(getExpirationTimes(context, currentExpiration)[selected]);
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
private static View createNumberPickerView(final Context context, final int currentExpiration) {
final LayoutInflater inflater = LayoutInflater.from(context);
final View view = inflater.inflate(R.layout.expiration_dialog, null);
final NumberPickerView numberPickerView = view.findViewById(R.id.expiration_number_picker);
final TextView textView = view.findViewById(R.id.expiration_details);
final int[] expirationTimes = getExpirationTimes(context, currentExpiration);
final String[] expirationDisplayValues = new String[expirationTimes.length];
int selectedIndex = expirationTimes.length - 1;
for (int i=0;i<expirationTimes.length;i++) {
expirationDisplayValues[i] = ExpirationUtil.getExpirationDisplayValue(context, expirationTimes[i]);
if ((currentExpiration >= expirationTimes[i]) &&
(i == expirationTimes.length -1 || currentExpiration < expirationTimes[i+1])) {
selectedIndex = i;
}
}
numberPickerView.setDisplayedValues(expirationDisplayValues);
numberPickerView.setMinValue(0);
numberPickerView.setMaxValue(expirationTimes.length-1);
NumberPickerView.OnValueChangeListener listener = (picker, oldVal, newVal) -> {
if (newVal == 0) {
textView.setText(R.string.ExpirationDialog_your_messages_will_not_expire);
} else {
textView.setText(context.getString(R.string.ExpirationDialog_your_messages_will_disappear_s_after_they_have_been_seen, picker.getDisplayedValues()[newVal]));
}
};
numberPickerView.setOnValueChangedListener(listener);
numberPickerView.setValue(selectedIndex);
listener.onValueChange(numberPickerView, selectedIndex, selectedIndex);
return view;
}
private static int[] getExpirationTimes(Context context, int currentExpiration) {
int[] expirationTimes = context.getResources().getIntArray(R.array.expiration_times);
int location = Arrays.binarySearch(expirationTimes, currentExpiration);
if (location < 0) {
int[] temp = Arrays.copyOf(expirationTimes, expirationTimes.length + 1);
temp[temp.length - 1] = currentExpiration;
Arrays.sort(temp);
expirationTimes = temp;
}
return expirationTimes;
}
public interface OnClickListener {
public void onClick(int expirationTime);
}
}
@@ -7,8 +7,6 @@ import android.graphics.PorterDuff;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
@@ -24,8 +22,8 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -46,6 +44,7 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
@@ -100,7 +99,8 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
View shareButton = findViewById(R.id.share_button);
Button smsButton = findViewById(R.id.sms_button);
Button smsCancelButton = findViewById(R.id.cancel_sms_button);
ContactFilterToolbar contactFilter = findViewById(R.id.contact_filter);
Toolbar smsToolbar = findViewById(R.id.sms_send_frame_toolbar);
ContactFilterView contactFilter = findViewById(R.id.contact_filter_edit_text);
inviteText = findViewById(R.id.invite_text);
smsSendFrame = findViewById(R.id.sms_send_frame);
@@ -121,7 +121,7 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
smsSendButton.setOnClickListener(new SmsSendClickListener());
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
contactFilter.setNavigationIcon(R.drawable.ic_search_conversation_24);
smsToolbar.setNavigationIcon(R.drawable.ic_search_conversation_24);
if (Util.isDefaultSmsProvider(this)) {
shareButton.setOnClickListener(new ShareClickListener());
@@ -140,9 +140,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
}
@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);
return true;
callback.accept(true);
}
@Override
@@ -150,6 +150,10 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
}
@Override
public void onSelectionChanged() {
}
private void sendSmsInvites() {
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
@@ -9,6 +9,8 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
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.keyvalue.SignalStore;
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.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;
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final MainNavigator navigator = new MainNavigator(this);
private VoiceNoteMediaController mediaController;
public static @NonNull Intent clearTop(@NonNull Context context) {
Intent intent = new Intent(context, MainActivity.class);
@@ -40,10 +44,12 @@ public class MainActivity extends PassphraseRequiredActivity {
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.main_activity);
mediaController = new VoiceNoteMediaController(this);
navigator.onCreate(savedInstanceState);
handleGroupLinkInIntent(getIntent());
handleProxyInIntent(getIntent());
handleSignalMeIntent(getIntent());
CachedInflater.from(this).clear();
}
@@ -60,6 +66,7 @@ public class MainActivity extends PassphraseRequiredActivity {
super.onNewIntent(intent);
handleGroupLinkInIntent(intent);
handleProxyInIntent(intent);
handleSignalMeIntent(intent);
}
@Override
@@ -109,4 +116,16 @@ public class MainActivity extends PassphraseRequiredActivity {
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;
}
}
@@ -9,6 +9,8 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import org.thoughtcrime.securesms.components.settings.DSLSettingsActivity;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.conversation.ConversationIntents;
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
@@ -69,8 +71,7 @@ public class MainNavigator {
}
public void goToAppSettings() {
Intent intent = new Intent(activity, ApplicationPreferencesActivity.class);
activity.startActivityForResult(intent, REQUEST_CONFIG_CHANGES);
activity.startActivityForResult(AppSettingsActivity.home(activity), REQUEST_CONFIG_CHANGES);
}
public void goToArchiveList() {
@@ -103,6 +103,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media";
public static final String SHOW_THREAD_EXTRA = "show_thread";
public static final String SORTING_EXTRA = "sorting";
public static final String IS_VIDEO_GIF = "is_video_gif";
private ViewPager mediaPager;
private View detailsContainer;
@@ -115,6 +116,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private String initialMediaType;
private long initialMediaSize;
private String initialCaption;
private boolean initialMediaIsVideoGif;
private boolean leftIsRecent;
private MediaPreviewViewModel viewModel;
private ViewPagerListener viewPagerListener;
@@ -139,6 +141,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif());
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
return intent;
}
@@ -168,6 +171,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
initializeObservers();
}
@SuppressLint("MissingSuperCall")
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@@ -296,12 +300,13 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false);
sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)];
initialMediaUri = intent.getData();
initialMediaType = intent.getType();
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
restartItem = -1;
initialMediaUri = intent.getData();
initialMediaType = intent.getType();
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
initialMediaIsVideoGif = intent.getBooleanExtra(IS_VIDEO_GIF, false);
restartItem = -1;
}
private void initializeObservers() {
@@ -354,7 +359,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
if (isMediaInDb()) {
LoaderManager.getInstance(this).restartLoader(0, null, this);
} else {
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize));
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize, initialMediaIsVideoGif));
if (initialCaption != null) {
detailsContainer.setVisibility(View.VISIBLE);
@@ -632,21 +637,24 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
private static class SingleItemPagerAdapter extends FragmentStatePagerAdapter implements MediaItemAdapter {
private final Uri uri;
private final String mediaType;
private final long size;
private final Uri uri;
private final String mediaType;
private final long size;
private final boolean isVideoGif;
private MediaPreviewFragment mediaPreviewFragment;
SingleItemPagerAdapter(@NonNull FragmentManager fragmentManager,
@NonNull Uri uri,
@NonNull String mediaType,
long size)
long size,
boolean isVideoGif)
{
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
this.uri = uri;
this.mediaType = mediaType;
this.size = size;
this.uri = uri;
this.mediaType = mediaType;
this.size = size;
this.isVideoGif = isVideoGif;
}
@Override
@@ -657,7 +665,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
@NonNull
@Override
public Fragment getItem(int position) {
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true);
mediaPreviewFragment = MediaPreviewFragment.newInstance(uri, mediaType, size, true, isVideoGif);
return mediaPreviewFragment;
}
@@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.concurrent.TimeUnit;
public class MuteDialog extends AlertDialog {
@@ -29,7 +31,7 @@ public class MuteDialog extends AlertDialog {
}
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.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
@Override
@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.function.Consumer;
/**
* Activity container for starting a new conversation.
@@ -56,10 +57,11 @@ public class NewConversationActivity extends ContactSelectionActivity
super.onCreate(bundle, ready);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.NewConversationActivity__new_message);
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
if (recipientId.isPresent()) {
launch(Recipient.resolved(recipientId.get()));
} else {
@@ -93,7 +95,11 @@ public class NewConversationActivity extends ContactSelectionActivity
}
}
return true;
callback.accept(true);
}
@Override
public void onSelectionChanged() {
}
private void launch(Recipient recipient) {
@@ -166,7 +166,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.storageServiceValues().needsAccountRestore()) {
} else if (SignalStore.storageService().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;
@@ -65,4 +65,8 @@ public class PushContactSelectionActivity extends ContactSelectionActivity {
setResult(RESULT_OK, resultIntent);
finish();
}
@Override
public void onSelectionChanged() {
}
}
@@ -29,7 +29,6 @@ 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;
@@ -63,9 +62,8 @@ 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.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -87,16 +85,13 @@ 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;
@@ -213,17 +208,12 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
.execute();
}
@SuppressLint("MissingSuperCall")
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
private void setActionBarNotificationBarColor(MaterialColor color) {
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this));
}
public static class VerifyDisplayFragment extends Fragment implements CompoundButton.OnCheckedChangeListener {
public static final String RECIPIENT_ID = "recipient_id";
@@ -420,11 +410,9 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
} else {
Toast.makeText(getActivity(), R.string.VerifyIdentityActivity_your_contact_is_running_an_old_version_of_signal, Toast.LENGTH_LONG).show();
}
} catch (FingerprintParsingException e) {
} 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();
} catch (UnsupportedEncodingException e) {
throw new AssertionError(e);
}
}
@@ -623,7 +611,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
final RecipientId recipientId = recipient.getId();
SignalExecutors.BOUNDED.execute(() -> {
try (SignalSessionLock.Lock unused = DatabaseSessionLock.INSTANCE.acquire()) {
try (SignalSessionLock.Lock unused = ReentrantSessionLock.INSTANCE.acquire()) {
if (isChecked) {
Log.i(TAG, "Saving identity: " + recipientId);
DatabaseFactory.getIdentityDatabase(getActivity())
@@ -17,12 +17,17 @@
package org.thoughtcrime.securesms;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.PictureInPictureParams;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
@@ -34,20 +39,27 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.lifecycle.ViewModelProviders;
import androidx.window.DisplayFeature;
import androidx.window.FoldingFeature;
import androidx.window.WindowLayoutInfo;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallViewModel;
import org.thoughtcrime.securesms.components.webrtc.WebRtcControls;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
@@ -62,10 +74,13 @@ import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.webrtc.CallParticipantsViewState;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import java.util.List;
import java.util.Optional;
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
@@ -82,11 +97,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
private DeviceOrientationMonitor deviceOrientationMonitor;
private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen;
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable;
private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen;
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable;
private androidx.window.WindowManager windowManager;
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
@Override
protected void attachBaseContext(@NonNull Context newBase) {
@@ -94,6 +111,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
super.attachBaseContext(newBase);
}
@SuppressLint("SourceLockedOrientationActivity")
@Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate()");
@@ -101,6 +119,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
super.onCreate(savedInstanceState);
boolean isLandscapeEnabled = getResources().getConfiguration().smallestScreenWidthDp >= 480;
if (!isLandscapeEnabled) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
@@ -109,12 +132,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
initializeResources();
initializeViewModel();
initializeViewModel(isLandscapeEnabled);
processIntent(getIntent());
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
windowManager = new androidx.window.WindowManager(this);
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
windowManager.registerLayoutChangeCallback(SignalExecutors.BOUNDED, windowLayoutInfoConsumer);
}
@Override
@@ -172,9 +200,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
protected void onDestroy() {
super.onDestroy();
windowManager.unregisterLayoutChangeCallback(windowLayoutInfoConsumer);
EventBus.getDefault().unregister(this);
}
@SuppressLint("MissingSuperCall")
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
@@ -201,8 +231,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private boolean enterPipModeIfPossible() {
if (viewModel.canEnterPipMode() && isSystemPipEnabledAndAvailable()) {
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(9, 16))
.build();
.setAspectRatio(new Rational(9, 16))
.build();
enterPictureInPictureMode(params);
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
@@ -241,19 +271,23 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
participantUpdateWindow = new CallParticipantsListUpdatePopupWindow(callScreen);
}
private void initializeViewModel() {
private void initializeViewModel(boolean isLandscapeEnabled) {
deviceOrientationMonitor = new DeviceOrientationMonitor(this);
getLifecycle().addObserver(deviceOrientationMonitor);
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor);
viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class);
viewModel.setIsLandscapeEnabled(isLandscapeEnabled);
viewModel.setIsInPipMode(isInPipMode());
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime);
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
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.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall());
@@ -268,29 +302,23 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
});
viewModel.getOrientation().observe(this, orientation -> {
ApplicationDependencies.getSignalCallManager().orientationChanged(orientation.getDegrees());
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);
}
});
viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
viewModel.getControlsRotation().observe(this, callScreen::rotateControls);
}
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
if (event instanceof WebRtcCallViewModel.Event.StartCall) {
startCall(((WebRtcCallViewModel.Event.StartCall)event).isVideoCall());
startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall());
return;
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords());
return;
} else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) {
callScreen.switchToSpeakerView();
return;
} else if (event instanceof WebRtcCallViewModel.Event.ShowSwipeToSpeakerHint) {
CallToastPopupWindow.show(callScreen);
return;
}
if (isInPipMode()) {
@@ -392,7 +420,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setRecipient(recipient);
@@ -476,13 +504,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
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
new AlertDialog.Builder(this)
.setTitle(R.string.RedPhone_number_not_registered)
.setIcon(R.drawable.ic_warning)
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
.setCancelable(true)
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.show();
.setTitle(R.string.RedPhone_number_not_registered)
.setIcon(R.drawable.ic_warning)
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
.setCancelable(true)
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.setOnCancelListener(d -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
.show();
}
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
@@ -574,20 +602,34 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
callScreen.setRecipient(event.getRecipient());
switch (event.getState()) {
case CALL_PRE_JOIN: handleCallPreJoin(event); break;
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(); break;
case CALL_RINGING: handleCallRinging(); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE: 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;
case CALL_PRE_JOIN:
handleCallPreJoin(event); break;
case CALL_CONNECTED:
handleCallConnected(event); break;
case NETWORK_FAILURE:
handleServerFailure(); break;
case CALL_RINGING:
handleCallRinging(); break;
case CALL_DISCONNECTED:
handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL); break;
case CALL_ACCEPTED_ELSEWHERE:
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;
@@ -718,4 +760,27 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
viewModel.onLocalPictureInPictureClicked();
}
}
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,44 @@
package org.thoughtcrime.securesms.animation.transitions
import android.animation.Animator
import android.animation.ObjectAnimator
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.transition.Transition
import androidx.transition.TransitionValues
private const val ALPHA = "signal.alpha_transition.alpha"
/**
* Alpha transition that can be used with [ConstraintLayout]
*/
class AlphaTransition : Transition() {
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 ConstraintLayout) {
transitionValues.values[ALPHA] = view.alpha
}
}
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? {
if (startValues == null || endValues == null) {
return null
}
val view: View = endValues.view
val startAlpha: Float = startValues.values[ALPHA] as? Float ?: view.alpha
val endAlpha: Float = endValues.values[ALPHA] as? Float ?: view.alpha
return ObjectAnimator.ofFloat(view, "alpha", startAlpha, endAlpha)
}
}
@@ -0,0 +1,97 @@
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
import org.thoughtcrime.securesms.components.AvatarImageView
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 is AvatarImageView) {
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 !is AvatarImageView || 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
}
)
}
}
}
@@ -40,6 +40,7 @@ public abstract class Attachment {
private final boolean voiceNote;
private final boolean borderless;
private final boolean videoGif;
private final int width;
private final int height;
private final boolean quote;
@@ -72,6 +73,7 @@ public abstract class Attachment {
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean videoGif,
int width,
int height,
boolean quote,
@@ -94,6 +96,7 @@ public abstract class Attachment {
this.fastPreflightId = fastPreflightId;
this.voiceNote = voiceNote;
this.borderless = borderless;
this.videoGif = videoGif;
this.width = width;
this.height = height;
this.quote = quote;
@@ -170,6 +173,10 @@ public abstract class Attachment {
return borderless;
}
public boolean isVideoGif() {
return videoGif;
}
public int getWidth() {
return width;
}
@@ -36,6 +36,7 @@ public class DatabaseAttachment extends Attachment {
String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean videoGif,
int width,
int height,
boolean quote,
@@ -47,7 +48,7 @@ public class DatabaseAttachment extends Attachment {
int displayOrder,
long uploadTimestamp)
{
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.attachmentId = attachmentId;
this.hasData = hasData;
this.hasThumbnail = hasThumbnail;
@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
public class MmsNotificationAttachment extends Attachment {
public MmsNotificationAttachment(int status, long size) {
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, 0, 0, false, 0, null, null, null, null, null);
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, false, false, 0, 0, false, 0, null, null, null, null, null);
}
@Nullable
@@ -30,6 +30,7 @@ public class PointerAttachment extends Attachment {
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean videoGif,
int width,
int height,
long uploadTimestamp,
@@ -37,7 +38,7 @@ public class PointerAttachment extends Attachment {
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash)
{
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, borderless, videoGif, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
}
@Nullable
@@ -111,6 +112,7 @@ public class PointerAttachment extends Attachment {
fastPreflightId,
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().isBorderless(),
pointer.get().asPointer().isGif(),
pointer.get().asPointer().getWidth(),
pointer.get().asPointer().getHeight(),
pointer.get().asPointer().getUploadTimestamp(),
@@ -135,6 +137,7 @@ public class PointerAttachment extends Attachment {
null,
false,
false,
false,
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.database.AttachmentDatabase;
public class TombstoneAttachment extends Attachment {
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, 0, 0, quote, 0, null, null, null, null, null);
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, false, false, 0, 0, quote, 0, null, null, null, null, null);
}
@Override
@@ -21,6 +21,7 @@ public class UriAttachment extends Attachment {
@Nullable String fileName,
boolean voiceNote,
boolean borderless,
boolean videoGif,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@@ -28,7 +29,7 @@ public class UriAttachment extends Attachment {
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, videoGif, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
}
public UriAttachment(@NonNull Uri dataUri,
@@ -41,6 +42,7 @@ public class UriAttachment extends Attachment {
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean videoGif,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@@ -48,7 +50,7 @@ public class UriAttachment extends Attachment {
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.dataUri = dataUri;
}
@@ -32,6 +32,7 @@ public class AudioCodec {
private final AudioRecord audioRecord;
private boolean running = true;
private boolean failed = false;
private boolean finished = false;
public AudioCodec() throws IOException {
@@ -76,10 +77,25 @@ public class AudioCodec {
} catch (IOException e) {
Log.w(TAG, e);
} 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();
StreamUtil.close(outputStream);
@@ -11,11 +11,11 @@ import androidx.annotation.NonNull;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libsignal.util.Pair;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
@@ -51,7 +51,7 @@ public class AudioRecorder {
captureUri = BlobProvider.getInstance()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.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]));
@@ -61,10 +61,10 @@ public class AudioRecorder {
});
}
public @NonNull ListenableFuture<Pair<Uri, Long>> stopRecording() {
public @NonNull ListenableFuture<VoiceNoteDraft> stopRecording() {
Log.i(TAG, "stopRecording()");
final SettableFuture<Pair<Uri, Long>> future = new SettableFuture<>();
final SettableFuture<VoiceNoteDraft> future = new SettableFuture<>();
executor.execute(() -> {
if (audioCodec == null) {
@@ -76,7 +76,7 @@ public class AudioRecorder {
try {
long size = MediaUtil.getMediaSize(context, captureUri);
sendToFuture(future, new Pair<>(captureUri, size));
sendToFuture(future, new VoiceNoteDraft(captureUri, size));
} catch (IOException ioe) {
Log.w(TAG, ioe);
sendToFuture(future, ioe);
@@ -65,12 +65,6 @@ public final class AudioWaveForm {
return;
}
if (!(attachment instanceof DatabaseAttachment)) {
Log.i(TAG, "Not yet in database");
ThreadUtil.runOnMain(onFailure);
return;
}
String cacheKey = uri.toString();
AudioFileInfo cached = WAVE_FORM_CACHE.get(cacheKey);
if (cached != null) {
@@ -104,26 +98,46 @@ public final class AudioWaveForm {
}
}
try {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
DatabaseAttachment dbAttachment = (DatabaseAttachment) attachment;
long startTime = System.currentTimeMillis();
if (attachment instanceof DatabaseAttachment) {
try {
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
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);
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (Throwable e) {
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
ThreadUtil.runOnMain(onFailure);
WAVE_FORM_CACHE.put(cacheKey, fileInfo);
ThreadUtil.runOnMain(() -> onSuccess.accept(fileInfo));
} catch (Throwable e) {
Log.w(TAG, "Failed to create audio wave form for " + cacheKey, e);
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,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,69 @@
package org.thoughtcrime.securesms.avatar
import android.os.Bundle
import java.lang.IllegalStateException
/**
* 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,42 @@
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.MappingAdapter
import org.thoughtcrime.securesms.util.MappingModel
import org.thoughtcrime.securesms.util.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, MappingAdapter.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.DatabaseFactory
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 = DatabaseFactory.getAvatarPickerDatabase(context)
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,132 @@
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,
): Drawable {
return TextAvatarDrawable(context, avatar, inverted, size)
}
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)
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,59 @@
package org.thoughtcrime.securesms.avatar
import android.content.Context
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
import android.util.TypedValue
import android.view.Gravity
import android.widget.FrameLayout
import androidx.core.view.updateLayoutParams
import org.thoughtcrime.securesms.components.emoji.EmojiTextView
/**
* Uses EmojiTextView to properly render a Text Avatar with emoji in it.
*/
class TextAvatarDrawable(
context: Context,
avatar: Avatar.Text,
inverted: Boolean = false,
private val size: Int = AvatarRenderer.DIMENSIONS,
) : Drawable() {
private val layout: FrameLayout = FrameLayout(context)
private val textView: EmojiTextView = EmojiTextView(context)
init {
textView.typeface = AvatarRenderer.getTypeface(context)
textView.setTextSize(TypedValue.COMPLEX_UNIT_PX, Avatars.getTextSizeForLength(context, avatar.text, size * 0.8f, size * 0.45f))
textView.text = avatar.text
textView.gravity = Gravity.CENTER
textView.setTextColor(if (inverted) avatar.color.backgroundColor else avatar.color.foregroundColor)
textView.setForceCustomEmoji(true)
layout.addView(textView)
textView.updateLayoutParams {
width = size
height = size
}
layout.measure(size, size)
layout.layout(0, 0, size, size)
}
override fun getIntrinsicHeight(): Int = size
override fun getIntrinsicWidth(): Int = size
override fun draw(canvas: Canvas) {
layout.draw(canvas)
}
override fun setAlpha(alpha: Int) = Unit
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
override fun getOpacity(): Int = PixelFormat.OPAQUE
}
@@ -0,0 +1,65 @@
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.DatabaseFactory
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 = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
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()
}
}
}
companion object {
const val REQUEST_KEY_EDIT = "org.thoughtcrime.securesms.avatar.photo.EDIT"
private const val IMAGE_EDITOR = "image_editor"
}
}
@@ -0,0 +1,243 @@
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.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.MappingAdapter
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.visible
import java.util.Objects
/**
* 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)
}
)
Navigation.findNavController(v).popBackStack()
},
{
setFragmentResult(
REQUEST_KEY_SELECT_AVATAR,
Bundle().apply {
putBoolean(SELECT_AVATAR_CLEAR, true)
}
)
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())
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_CODE_SELECT_IMAGE && resultCode == Activity.RESULT_OK && data != null) {
val media: Media = Objects.requireNonNull(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)
}
}
fun openPhotoEditor(photo: Avatar.Photo) {
Navigation.findNavController(requireView())
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToAvatarPhotoEditorFragment(AvatarBundler.bundlePhoto(photo)))
}
fun openVectorEditor(vector: Avatar.Vector) {
Navigation.findNavController(requireView())
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToVectorAvatarCreationFragment(AvatarBundler.bundleVector(vector)))
}
fun openTextEditor(text: Avatar.Text?) {
val bundle = if (text != null) AvatarBundler.bundleText(text) else null
Navigation.findNavController(requireView())
.navigate(AvatarPickerFragmentDirections.actionAvatarPickerFragmentToTextAvatarCreationFragment(bundle))
}
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()
}
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,147 @@
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.MappingAdapter
import org.thoughtcrime.securesms.util.MappingModel
import org.thoughtcrime.securesms.util.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, MappingAdapter.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.DatabaseFactory
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 {
DatabaseFactory.getAvatarPickerDatabase(applicationContext).getAvatarsForSelf()
}
fun getPersistedAvatarsForGroup(groupId: GroupId): Single<List<Avatar>> = Single.fromCallable {
DatabaseFactory.getAvatarPickerDatabase(applicationContext).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 = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
val savedAvatar = avatarDatabase.saveAvatarForSelf(avatar)
avatarDatabase.markUsage(savedAvatar)
onPersisted(savedAvatar)
}
}
fun persistAvatarForGroup(avatar: Avatar, groupId: GroupId, onPersisted: (Avatar) -> Unit) {
SignalExecutors.BOUNDED.execute {
val avatarDatabase = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
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 = DatabaseFactory.getAvatarPickerDatabase(applicationContext)
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.MappingAdapter
import org.thoughtcrime.securesms.util.ViewUtil
/**
* 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 { v, actionId, event ->
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.MappingAdapter
import org.thoughtcrime.securesms.util.ViewUtil
/**
* 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)))
}
}
}
@@ -74,7 +74,7 @@ public class BackupDialog {
BackupPassphrase.set(context, Util.join(password, " "));
TextSecurePreferences.setNextBackupTime(context, 0);
TextSecurePreferences.setBackupEnabled(context, true);
SignalStore.settings().setBackupEnabled(true);
LocalBackupListener.schedule(context);
onBackupsEnabled.run();
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.backup;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -11,8 +10,8 @@ import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
@@ -39,11 +38,7 @@ public enum BackupFileIOError {
}
public void postNotification(@NonNull Context context) {
Intent intent = new Intent(context, ApplicationPreferencesActivity.class);
intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_BACKUPS_FRAGMENT, true);
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, intent, 0);
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), 0);
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.FAILURES)
.setSmallIcon(R.drawable.ic_signal_backup)
.setContentTitle(context.getString(titleId))
@@ -24,20 +24,26 @@ import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.SenderKeyDatabase;
import org.thoughtcrime.securesms.database.SenderKeySharedDatabase;
import org.thoughtcrime.securesms.database.SessionDatabase;
import org.thoughtcrime.securesms.database.SignedPreKeyDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.service.PendingRetryReceiptManager;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -75,7 +81,12 @@ public class FullBackupExporter extends FullBackupBase {
OneTimePreKeyDatabase.TABLE_NAME,
SessionDatabase.TABLE_NAME,
SearchDatabase.SMS_FTS_TABLE_NAME,
SearchDatabase.MMS_FTS_TABLE_NAME
SearchDatabase.MMS_FTS_TABLE_NAME,
EmojiSearchDatabase.TABLE_NAME,
SenderKeyDatabase.TABLE_NAME,
SenderKeySharedDatabase.TABLE_NAME,
PendingRetryReceiptDatabase.TABLE_NAME,
AvatarPickerDatabase.TABLE_NAME
);
public static void export(@NonNull Context context,
@@ -210,11 +221,11 @@ public class FullBackupExporter extends FullBackupBase {
String type = cursor.getString(2);
if (sql != null) {
boolean isSmsFtsSecretTable = name != null && !name.equals(SearchDatabase.SMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME);
boolean isMmsFtsSecretTable = name != null && !name.equals(SearchDatabase.MMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME);
boolean isEmojiFtsSecretTable = name != null && !name.equals(EmojiSearchDatabase.TABLE_NAME) && name.startsWith(EmojiSearchDatabase.TABLE_NAME);
boolean isSmsFtsSecretTable = name != null && !name.equals(SearchDatabase.SMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.SMS_FTS_TABLE_NAME);
boolean isMmsFtsSecretTable = name != null && !name.equals(SearchDatabase.MMS_FTS_TABLE_NAME) && name.startsWith(SearchDatabase.MMS_FTS_TABLE_NAME);
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable) {
if (!isSmsFtsSecretTable && !isMmsFtsSecretTable && !isEmojiFtsSecretTable) {
if ("table".equals(type)) {
tables.add(name);
}
@@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.backup.BackupProtos.Sticker;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase;
@@ -136,9 +137,10 @@ public class FullBackupImporter extends FullBackupBase {
private static void processStatement(@NonNull SQLiteDatabase db, SqlStatement statement) {
boolean isForSmsFtsSecretTable = statement.getStatement().contains(SearchDatabase.SMS_FTS_TABLE_NAME + "_");
boolean isForMmsFtsSecretTable = statement.getStatement().contains(SearchDatabase.MMS_FTS_TABLE_NAME + "_");
boolean isForEmojiSecretTable = statement.getStatement().contains(EmojiSearchDatabase.TABLE_NAME + "_");
boolean isForSqliteSecretTable = statement.getStatement().toLowerCase().startsWith("create table sqlite_");
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForSqliteSecretTable) {
if (isForSmsFtsSecretTable || isForMmsFtsSecretTable || isForEmojiSecretTable || isForSqliteSecretTable) {
Log.i(TAG, "Ignoring import for statement: " + statement.getStatement());
return;
}
@@ -1,24 +1,24 @@
package org.thoughtcrime.securesms.blocked;
import android.app.AlertDialog;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.ViewSwitcher;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -26,6 +26,8 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.function.Consumer;
public class BlockedUsersActivity extends PassphraseRequiredActivity implements BlockedUsersFragment.Listener, ContactSelectionListFragment.OnContactSelectedListener {
private static final String CONTACT_SELECTION_FRAGMENT = "Contact.Selection.Fragment";
@@ -47,27 +49,26 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
viewModel = ViewModelProviders.of(this, factory).get(BlockedUsersViewModel.class);
ViewSwitcher viewSwitcher = findViewById(R.id.toolbar_switcher);
Toolbar toolbar = findViewById(R.id.toolbar);
ContactFilterToolbar contactFilterToolbar = findViewById(R.id.filter_toolbar);
View container = findViewById(R.id.fragment_container);
Toolbar toolbar = findViewById(R.id.toolbar);
ContactFilterView contactFilterView = findViewById(R.id.contact_filter_edit_text);
View container = findViewById(R.id.fragment_container);
toolbar.setNavigationOnClickListener(unused -> onBackPressed());
contactFilterToolbar.setNavigationOnClickListener(unused -> onBackPressed());
contactFilterToolbar.setOnFilterChangedListener(query -> {
contactFilterView.setOnFilterChangedListener(query -> {
Fragment fragment = getSupportFragmentManager().findFragmentByTag(CONTACT_SELECTION_FRAGMENT);
if (fragment != null) {
((ContactSelectionListFragment) fragment).setQueryFilter(query);
}
});
contactFilterToolbar.setHint(R.string.BlockedUsersActivity__add_blocked_user);
contactFilterView.setHint(R.string.BlockedUsersActivity__add_blocked_user);
//noinspection CodeBlock2Expr
getSupportFragmentManager().addOnBackStackChangedListener(() -> {
viewSwitcher.setDisplayedChild(getSupportFragmentManager().getBackStackEntryCount());
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
contactFilterToolbar.focusAndShowKeyboard();
contactFilterView.setVisibility(View.VISIBLE);
contactFilterView.focusAndShowKeyboard();
} else {
contactFilterView.setVisibility(View.GONE);
}
});
@@ -86,24 +87,24 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
}
@Override
public boolean onBeforeContactSelected(Optional<RecipientId> recipientId, String number) {
public void onBeforeContactSelected(Optional<RecipientId> recipientId, String number, Consumer<Boolean> callback) {
final String displayName = recipientId.transform(id -> Recipient.resolved(id).getDisplayName(this)).or(number);
AlertDialog confirmationDialog = new AlertDialog.Builder(BlockedUsersActivity.this)
.setTitle(R.string.BlockedUsersActivity__block_user)
.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
.setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
if (recipientId.isPresent()) {
viewModel.block(recipientId.get());
} else {
viewModel.createAndBlock(number);
}
dialog.dismiss();
onBackPressed();
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.setCancelable(true)
.create();
AlertDialog confirmationDialog = new MaterialAlertDialogBuilder(this)
.setTitle(R.string.BlockedUsersActivity__block_user)
.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
.setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
if (recipientId.isPresent()) {
viewModel.block(recipientId.get());
} else {
viewModel.createAndBlock(number);
}
dialog.dismiss();
onBackPressed();
})
.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
.setCancelable(true)
.create();
confirmationDialog.setOnShowListener(dialog -> {
confirmationDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(Color.RED);
@@ -111,7 +112,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
confirmationDialog.show();
return false;
callback.accept(false);
}
@Override
@@ -119,6 +120,10 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
}
@Override
public void onSelectionChanged() {
}
@Override
public void handleAddUserToBlockedList() {
ContactSelectionListFragment fragment = new ContactSelectionListFragment();
@@ -164,6 +169,6 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
throw new IllegalArgumentException("Unsupported event type " + event);
}
Snackbar.make(view, getString(messageResId, displayName), Snackbar.LENGTH_SHORT).show();
Snackbar.make(view, getString(messageResId, displayName), Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show();
}
}
@@ -91,14 +91,14 @@ public enum MaterialColor {
}
public @ColorRes int toQuoteBarColorResource(@NonNull Context context, boolean outgoing) {
if (outgoing) {
if (!outgoing) {
return isDarkTheme(context) ? tintColor : shadeColor ;
}
return R.color.core_white;
}
public @ColorInt int toQuoteBackgroundColor(@NonNull Context context, boolean outgoing) {
if (outgoing) {
if (!outgoing) {
int color = toConversationColor(context);
int alpha = isDarkTheme(context) ? (int) (0.2 * 255) : (int) (0.4 * 255);
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
@@ -108,7 +108,7 @@ public enum MaterialColor {
}
public @ColorInt int toQuoteFooterColor(@NonNull Context context, boolean outgoing) {
if (outgoing) {
if (!outgoing) {
int color = toConversationColor(context);
int alpha = isDarkTheme(context) ? (int) (0.4 * 255) : (int) (0.6 * 255);
return Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
@@ -69,4 +69,10 @@ public class AlertView extends LinearLayout {
approvalIndicator.setVisibility(View.GONE);
failedIndicator.setVisibility(View.VISIBLE);
}
public void setRateLimited() {
this.setVisibility(View.VISIBLE);
approvalIndicator.setVisibility(View.VISIBLE);
failedIndicator.setVisibility(View.GONE);
}
}
@@ -45,17 +45,21 @@ public final class AudioView extends FrameLayout {
private static final String TAG = Log.tag(AudioView.class);
private static final int MODE_NORMAL = 0;
private static final int MODE_SMALL = 1;
private static final int MODE_DRAFT = 2;
private static final int FORWARDS = 1;
private static final int REVERSE = -1;
@NonNull private final AnimatingToggle controlToggle;
@NonNull private final View progressAndPlay;
@NonNull private final LottieAnimationView playPauseButton;
@NonNull private final ImageView downloadButton;
@NonNull private final ProgressWheel circleProgress;
@NonNull private final SeekBar seekBar;
private final boolean smallView;
private final boolean autoRewind;
@NonNull private final AnimatingToggle controlToggle;
@NonNull private final View progressAndPlay;
@NonNull private final LottieAnimationView playPauseButton;
@NonNull private final ImageView downloadButton;
@Nullable private final ProgressWheel circleProgress;
@NonNull private final SeekBar seekBar;
private final boolean smallView;
private final boolean autoRewind;
@Nullable private final TextView duration;
@@ -87,10 +91,23 @@ public final class AudioView extends FrameLayout {
try {
typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0);
smallView = typedArray.getBoolean(R.styleable.AudioView_small, false);
int mode = typedArray.getInteger(R.styleable.AudioView_audioView_mode, MODE_NORMAL);
smallView = mode == MODE_SMALL;
autoRewind = typedArray.getBoolean(R.styleable.AudioView_autoRewind, false);
inflate(context, smallView ? R.layout.audio_view_small : R.layout.audio_view, this);
switch (mode) {
case MODE_NORMAL:
inflate(context, R.layout.audio_view, this);
break;
case MODE_SMALL:
inflate(context, R.layout.audio_view_small, this);
break;
case MODE_DRAFT:
inflate(context, R.layout.audio_view_draft, this);
break;
default:
throw new IllegalStateException("Unsupported mode: " + mode);
}
this.controlToggle = findViewById(R.id.control_toggle);
this.playPauseButton = findViewById(R.id.play);
@@ -110,7 +127,7 @@ public final class AudioView extends FrameLayout {
this.waveFormUnplayedBarsColor = typedArray.getColor(R.styleable.AudioView_waveformUnplayedBarsColor, Color.WHITE);
this.waveFormThumbTint = typedArray.getColor(R.styleable.AudioView_waveformThumbTint, Color.WHITE);
progressAndPlay.getBackground().setColorFilter(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK), PorterDuff.Mode.SRC_IN);
setProgressAndPlayBackgroundTint(typedArray.getColor(R.styleable.AudioView_progressAndPlayTint, Color.BLACK));
} finally {
if (typedArray != null) {
typedArray.recycle();
@@ -130,6 +147,10 @@ public final class AudioView extends FrameLayout {
EventBus.getDefault().unregister(this);
}
public void setProgressAndPlayBackgroundTint(@ColorInt int color) {
progressAndPlay.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_IN);
}
public Observer<VoiceNotePlaybackState> getPlaybackStateObserver() {
return playbackStateObserver;
}
@@ -158,16 +179,20 @@ public final class AudioView extends FrameLayout {
controlToggle.displayQuick(downloadButton);
seekBar.setEnabled(false);
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
circleProgress.setVisibility(View.GONE);
if (circleProgress != null) {
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
circleProgress.setVisibility(View.GONE);
}
} else if (showControls && audio.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
controlToggle.displayQuick(progressAndPlay);
seekBar.setEnabled(false);
circleProgress.setVisibility(View.VISIBLE);
circleProgress.spin();
if (circleProgress != null) {
circleProgress.setVisibility(View.VISIBLE);
circleProgress.spin();
}
} else {
seekBar.setEnabled(true);
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
if (circleProgress != null && circleProgress.isSpinning()) circleProgress.stopSpinning();
showPlayButton();
}
@@ -211,10 +236,11 @@ public final class AudioView extends FrameLayout {
private void onPlaybackState(@NonNull VoiceNotePlaybackState voiceNotePlaybackState) {
onDuration(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getTrackDuration());
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isAutoReset());
onProgress(voiceNotePlaybackState.getUri(),
(double) voiceNotePlaybackState.getPlayheadPositionMillis() / voiceNotePlaybackState.getTrackDuration(),
voiceNotePlaybackState.getPlayheadPositionMillis());
onSpeedChanged(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.getSpeed());
onStart(voiceNotePlaybackState.getUri(), voiceNotePlaybackState.isPlaying(), voiceNotePlaybackState.isAutoReset());
}
private void onDuration(@NonNull Uri uri, long durationMillis) {
@@ -223,8 +249,8 @@ public final class AudioView extends FrameLayout {
}
}
private void onStart(@NonNull Uri uri, boolean autoReset) {
if (!isTarget(uri)) {
private void onStart(@NonNull Uri uri, boolean statePlaying, boolean autoReset) {
if (!isTarget(uri) || !statePlaying) {
if (hasAudioUri()) {
onStop(audioSlide.getUri(), autoReset);
}
@@ -274,6 +300,12 @@ public final class AudioView extends FrameLayout {
}
}
private void onSpeedChanged(@NonNull Uri uri, float speed) {
if (callbacks != null) {
callbacks.onSpeedChanged(speed, isTarget(uri));
}
}
private boolean isTarget(@NonNull Uri uri) {
return hasAudioUri() && Objects.equals(uri, audioSlide.getUri());
}
@@ -314,11 +346,11 @@ public final class AudioView extends FrameLayout {
}
if (duration != null && durationMillis > 0) {
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis);
long remainingSecs = Math.max(0, TimeUnit.MILLISECONDS.toSeconds(durationMillis - millis));
duration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
}
if (smallView) {
if (smallView && circleProgress != null) {
circleProgress.setInstantProgress(seekBar.getProgress() == 0 ? 1 : progress);
}
}
@@ -329,7 +361,10 @@ public final class AudioView extends FrameLayout {
new LottieValueCallback<>(new SimpleColorFilter(foregroundTint))));
this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
this.circleProgress.setBarColor(foregroundTint);
if (circleProgress != null) {
this.circleProgress.setBarColor(foregroundTint);
}
if (this.duration != null) {
this.duration.setTextColor(foregroundTint);
@@ -372,11 +407,14 @@ public final class AudioView extends FrameLayout {
}
private void showPlayButton() {
if (!smallView) {
circleProgress.setVisibility(GONE);
} else if (seekBar.getProgress() == 0) {
circleProgress.setInstantProgress(1);
if (circleProgress != null) {
if (!smallView) {
circleProgress.setVisibility(GONE);
} else if (seekBar.getProgress() == 0) {
circleProgress.setInstantProgress(1);
}
}
playPauseButton.setVisibility(VISIBLE);
controlToggle.displayQuick(progressAndPlay);
}
@@ -451,6 +489,8 @@ public final class AudioView extends FrameLayout {
if (callbacks != null) {
if (wasPlaying) {
callbacks.onSeekTo(audioSlide.getUri(), getProgress());
} else {
callbacks.onProgressUpdated(durationMillis, Math.round(durationMillis * getProgress()));
}
}
}
@@ -465,7 +505,7 @@ public final class AudioView extends FrameLayout {
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventAsync(final PartProgressEvent event) {
if (audioSlide != null && event.attachment.equals(audioSlide.asAttachment())) {
if (audioSlide != null && circleProgress != null && event.attachment.equals(audioSlide.asAttachment())) {
circleProgress.setInstantProgress(((float) event.progress) / event.total);
}
}
@@ -475,6 +515,7 @@ public final class AudioView extends FrameLayout {
void onPause(@NonNull Uri audioUri);
void onSeekTo(@NonNull Uri audioUri, double progress);
void onStopAndReset(@NonNull Uri audioUri);
void onSpeedChanged(float speed, boolean isPlaying);
void onProgressUpdated(long durationMillis, long playheadMillis);
}
}
@@ -2,37 +2,51 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import androidx.annotation.DrawableRes;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.Px;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.FragmentActivity;
import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.Transformation;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.groups.ui.managegroup.ManageGroupActivity;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.ui.managerecipient.ManageRecipientActivity;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.BlurTransformation;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public final class AvatarImageView extends AppCompatImageView {
@@ -63,6 +77,9 @@ public final class AvatarImageView extends AppCompatImageView {
private Paint outlinePaint;
private OnClickListener listener;
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
private boolean blurred;
private ChatColors chatColors;
private FixedSizeTarget fixedSizeTarget;
private @Nullable RecipientContactPhoto recipientContactPhoto;
private @NonNull Drawable unknownRecipientDrawable;
@@ -82,23 +99,30 @@ public final class AvatarImageView extends AppCompatImageView {
if (attrs != null) {
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AvatarImageView, 0, 0);
inverted = typedArray.getBoolean(R.styleable.AvatarImageView_inverted, false);
size = typedArray.getInt(R.styleable.AvatarImageView_fallbackImageSize, SIZE_LARGE);
inverted = typedArray.getBoolean(R.styleable.AvatarImageView_inverted, false);
size = typedArray.getInt(R.styleable.AvatarImageView_fallbackImageSize, SIZE_LARGE);
typedArray.recycle();
}
outlinePaint = ThemeUtil.isDarkTheme(getContext()) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
outlinePaint = ThemeUtil.isDarkTheme(context) ? DARK_THEME_OUTLINE_PAINT : LIGHT_THEME_OUTLINE_PAINT;
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(getContext(), ContactColors.UNKNOWN_COLOR.toConversationColor(getContext()), inverted);
unknownRecipientDrawable = new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20).asDrawable(context, AvatarColor.UNKNOWN, inverted);
blurred = false;
chatColors = null;
}
@Override
public void setClipBounds(Rect clipBounds) {
super.setClipBounds(clipBounds);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float width = getWidth() - getPaddingRight() - getPaddingLeft();
float width = getWidth() - getPaddingRight() - getPaddingLeft();
float height = getHeight() - getPaddingBottom() - getPaddingTop();
float cx = width / 2f;
float cx = width / 2f;
float cy = height / 2f;
float radius = Math.min(cx, cy) - (outlinePaint.getStrokeWidth() / 2f);
@@ -135,6 +159,10 @@ public final class AvatarImageView extends AppCompatImageView {
}
}
public AvatarOptions.Builder buildOptions() {
return new AvatarOptions.Builder(this);
}
/**
* Shows self as the note to self icon.
*/
@@ -154,44 +182,78 @@ public final class AvatarImageView extends AppCompatImageView {
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
if (recipient != null) {
RecipientContactPhoto photo = (recipient.isSelf() && useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
new ProfileContactPhoto(Recipient.self(),
Recipient.self().getProfileAvatar()))
: new RecipientContactPhoto(recipient);
setAvatar(requestManager, recipient, new AvatarOptions.Builder(this)
.withUseSelfProfileAvatar(useSelfProfileAvatar)
.withQuickContactEnabled(quickContactEnabled)
.build());
}
if (!photo.equals(recipientContactPhoto)) {
private void setAvatar(@Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
setAvatar(GlideApp.with(this), recipient, avatarOptions);
}
private void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
if (recipient != null) {
RecipientContactPhoto photo = (recipient.isSelf() && avatarOptions.useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
new ProfileContactPhoto(Recipient.self(),
Recipient.self().getProfileAvatar()))
: new RecipientContactPhoto(recipient);
boolean shouldBlur = recipient.shouldBlurAvatar();
ChatColors chatColors = recipient.getChatColors();
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred || !Objects.equals(chatColors, this.chatColors)) {
requestManager.clear(this);
this.chatColors = chatColors;
recipientContactPhoto = photo;
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL
? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL ? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
if (fixedSizeTarget != null) {
requestManager.clear(fixedSizeTarget);
}
if (photo.contactPhoto != null) {
requestManager.load(photo.contactPhoto)
.fallback(fallbackContactPhotoDrawable)
.error(fallbackContactPhotoDrawable)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.circleCrop()
.into(this);
List<Transformation<Bitmap>> transforms = new ArrayList<>();
if (shouldBlur) {
transforms.add(new BlurTransformation(ApplicationDependencies.getApplication(), 0.25f, BlurTransformation.MAX_RADIUS));
}
transforms.add(new CircleCrop());
blurred = shouldBlur;
GlideRequest<Drawable> request = requestManager.load(photo.contactPhoto)
.fallback(fallbackContactPhotoDrawable)
.error(fallbackContactPhotoDrawable)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.downsample(DownsampleStrategy.CENTER_INSIDE)
.transform(new MultiTransformation<>(transforms));
if (avatarOptions.fixedSize > 0) {
fixedSizeTarget = new FixedSizeTarget(avatarOptions.fixedSize);
request.into(fixedSizeTarget);
} else {
request.into(this);
}
} else {
setImageDrawable(fallbackContactPhotoDrawable);
}
}
setAvatarClickHandler(recipient, quickContactEnabled);
setAvatarClickHandler(recipient, avatarOptions.quickContactEnabled);
} else {
recipientContactPhoto = null;
requestManager.clear(this);
if (fallbackPhotoProvider != null) {
setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName()
.asDrawable(getContext(), MaterialColor.STEEL.toAvatarColor(getContext()), inverted));
.asDrawable(getContext(), AvatarColor.UNKNOWN, inverted));
} else {
setImageDrawable(unknownRecipientDrawable);
}
super.setOnClickListener(listener);
disableQuickContact();
}
}
@@ -200,15 +262,15 @@ public final class AvatarImageView extends AppCompatImageView {
super.setOnClickListener(v -> {
Context context = getContext();
if (recipient.isPushGroup()) {
context.startActivity(ManageGroupActivity.newIntent(context, recipient.requireGroupId().requirePush()),
ManageGroupActivity.createTransitionBundle(context, this));
context.startActivity(ConversationSettingsActivity.forGroup(context, recipient.requireGroupId().requirePush()),
ConversationSettingsActivity.createTransitionBundle(context, this));
} else {
if (context instanceof FragmentActivity) {
RecipientBottomSheetDialogFragment.create(recipient.getId(), null)
.show(((FragmentActivity) context).getSupportFragmentManager(), "BOTTOM");
} else {
context.startActivity(ManageRecipientActivity.newIntent(context, recipient.getId()),
ManageRecipientActivity.createTransitionBundle(context, this));
context.startActivity(ConversationSettingsActivity.forRecipient(context, recipient.getId()),
ConversationSettingsActivity.createTransitionBundle(context, this));
}
}
});
@@ -219,11 +281,11 @@ public final class AvatarImageView extends AppCompatImageView {
public void setImageBytesForGroup(@Nullable byte[] avatarBytes,
@Nullable Recipient.FallbackPhotoProvider fallbackPhotoProvider,
@NonNull MaterialColor color)
@NonNull AvatarColor color)
{
Drawable fallback = Util.firstNonNull(fallbackPhotoProvider, Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER)
.getPhotoForGroup()
.asDrawable(getContext(), color.toAvatarColor(getContext()));
.asDrawable(getContext(), color);
GlideApp.with(this)
.load(avatarBytes)
@@ -264,9 +326,70 @@ public final class AvatarImageView extends AppCompatImageView {
if (other == null) return false;
return other.recipient.equals(recipient) &&
other.recipient.getColor().equals(recipient.getColor()) &&
other.recipient.getChatColors().equals(recipient.getChatColors()) &&
other.ready == ready &&
Objects.equals(other.contactPhoto, contactPhoto);
}
}
private final class FixedSizeTarget extends SimpleTarget<Drawable> {
FixedSizeTarget(int size) {
super(size, size);
}
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition<? super Drawable> transition) {
setImageDrawable(resource);
}
}
public static final class AvatarOptions {
private final boolean quickContactEnabled;
private final boolean useSelfProfileAvatar;
private final int fixedSize;
private AvatarOptions(@NonNull Builder builder) {
this.quickContactEnabled = builder.quickContactEnabled;
this.useSelfProfileAvatar = builder.useSelfProfileAvatar;
this.fixedSize = builder.fixedSize;
}
public static final class Builder {
private final AvatarImageView avatarImageView;
private boolean quickContactEnabled = false;
private boolean useSelfProfileAvatar = false;
private int fixedSize = -1;
private Builder(@NonNull AvatarImageView avatarImageView) {
this.avatarImageView = avatarImageView;
}
public @NonNull Builder withQuickContactEnabled(boolean quickContactEnabled) {
this.quickContactEnabled = quickContactEnabled;
return this;
}
public @NonNull Builder withUseSelfProfileAvatar(boolean useSelfProfileAvatar) {
this.useSelfProfileAvatar = useSelfProfileAvatar;
return this;
}
public @NonNull Builder withFixedSize(@Px @IntRange(from = 1) int fixedSize) {
this.fixedSize = fixedSize;
return this;
}
public AvatarOptions build() {
return new AvatarOptions(this);
}
public void load(@Nullable Recipient recipient) {
avatarImageView.setAvatar(recipient, build());
}
}
}
}
@@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.widget.doAfterTextChanged
import com.google.android.material.tabs.TabLayout
import org.thoughtcrime.securesms.R
import java.util.Objects
/**
* Custom View for Tabs which will render bold text when the view is selected
*/
class BoldSelectionTabItem @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : FrameLayout(context, attrs, defStyleAttr) {
private lateinit var unselectedTextView: TextView
private lateinit var selectedTextView: TextView
override fun onFinishInflate() {
super.onFinishInflate()
unselectedTextView = findViewById(android.R.id.text1)
selectedTextView = findViewById(R.id.text1_bold)
unselectedTextView.doAfterTextChanged {
selectedTextView.text = it
}
}
fun select() {
unselectedTextView.alpha = 0f
selectedTextView.alpha = 1f
}
fun unselect() {
unselectedTextView.alpha = 1f
selectedTextView.alpha = 0f
}
companion object {
@JvmStatic
fun registerListeners(tabLayout: ControllableTabLayout) {
val newTabListener = NewTabListener()
val onTabSelectedListener = OnTabSelectedListener()
(0 until tabLayout.tabCount).mapNotNull { tabLayout.getTabAt(it) }.forEach {
newTabListener.onNewTab(it)
if (it.isSelected) {
onTabSelectedListener.onTabSelected(it)
} else {
onTabSelectedListener.onTabUnselected(it)
}
}
tabLayout.setNewTabListener(newTabListener)
tabLayout.addOnTabSelectedListener(onTabSelectedListener)
}
}
private class NewTabListener : ControllableTabLayout.NewTabListener {
override fun onNewTab(tab: TabLayout.Tab) {
val customView = tab.customView
if (customView == null) {
tab.setCustomView(R.layout.bold_selection_tab_item)
}
}
}
private class OnTabSelectedListener : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
val view = Objects.requireNonNull(tab.customView) as BoldSelectionTabItem
view.select()
}
override fun onTabUnselected(tab: TabLayout.Tab) {
val view = Objects.requireNonNull(tab.customView) as BoldSelectionTabItem
view.unselect()
}
override fun onTabReselected(tab: TabLayout.Tab) = Unit
}
}
@@ -0,0 +1,41 @@
package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import org.thoughtcrime.securesms.R
class ButtonStripItemView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : ConstraintLayout(context, attrs, defStyleAttr) {
private val iconView: ImageView
private val labelView: TextView
init {
inflate(context, R.layout.button_strip_item_view, this)
iconView = findViewById(R.id.icon)
labelView = findViewById(R.id.label)
val array = context.obtainStyledAttributes(attrs, R.styleable.ButtonStripItemView)
val icon = array.getDrawable(R.styleable.ButtonStripItemView_bsiv_icon)
val contentDescription = array.getString(R.styleable.ButtonStripItemView_bsiv_icon_contentDescription)
val label = array.getString(R.styleable.ButtonStripItemView_bsiv_label)
iconView.setImageDrawable(icon)
iconView.contentDescription = contentDescription
labelView.text = label
array.recycle()
}
fun setOnIconClickedListener(onIconClickedListener: (() -> Unit)?) {
iconView.setOnClickListener { onIconClickedListener?.invoke() }
}
}
@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.components.mention.MentionDeleter;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -201,7 +202,7 @@ public class ComposeText extends EmojiEditText {
}
public void setTransport(TransportOption transport) {
final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext());
final boolean useSystemEmoji = SignalStore.settings().isPreferSystemEmoji();
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
int inputType = getInputType();
@@ -225,7 +226,7 @@ public class ComposeText extends EmojiEditText {
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
InputConnection inputConnection = super.onCreateInputConnection(editorInfo);
if(TextSecurePreferences.isEnterSendsEnabled(getContext())) {
if(SignalStore.settings().isEnterKeySends()) {
editorInfo.imeOptions &= ~EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
@@ -10,6 +10,7 @@ import android.util.AttributeSet;
import android.view.TouchDelegate;
import android.view.View;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -20,9 +21,8 @@ import androidx.core.widget.TextViewCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.DarkOverflowToolbar;
public final class ContactFilterToolbar extends DarkOverflowToolbar {
public final class ContactFilterView extends FrameLayout {
private OnFilterChangedListener listener;
private final EditText searchText;
@@ -32,17 +32,17 @@ public final class ContactFilterToolbar extends DarkOverflowToolbar {
private final ImageView clearToggle;
private final LinearLayout toggleContainer;
public ContactFilterToolbar(Context context) {
public ContactFilterView(Context context) {
this(context, null);
}
public ContactFilterToolbar(Context context, AttributeSet attrs) {
public ContactFilterView(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.toolbarStyle);
}
public ContactFilterToolbar(Context context, AttributeSet attrs, int defStyleAttr) {
public ContactFilterView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
inflate(context, R.layout.contact_filter_toolbar, this);
inflate(context, R.layout.contact_filter_view, this);
this.searchText = findViewById(R.id.search_view);
this.toggle = findViewById(R.id.button_toggle);
@@ -99,8 +99,6 @@ public final class ContactFilterToolbar extends DarkOverflowToolbar {
}
});
setLogo(null);
setContentInsetStartWithNavigation(0);
expandTapArea(toggleContainer, dialpadToggle);
applyAttributes(searchText, context, attrs, defStyleAttr);
searchText.requestFocus();
@@ -4,6 +4,9 @@ import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.tabs.TabLayout;
import java.util.List;
@@ -15,6 +18,8 @@ public class ControllableTabLayout extends TabLayout {
private List<View> touchables;
private NewTabListener newTabListener;
public ControllableTabLayout(Context context) {
super(context);
}
@@ -39,4 +44,28 @@ public class ControllableTabLayout extends TabLayout {
super.setEnabled(enabled);
}
public void setNewTabListener(@Nullable NewTabListener newTabListener) {
this.newTabListener = newTabListener;
}
@Override
public @NonNull Tab newTab() {
Tab tab = super.newTab();
if (newTabListener != null) {
newTabListener.onNewTab(tab);
}
return tab;
}
/**
* Allows implementor to modify tabs when they are created, before they are added to the tab layout.
* This is useful for loading custom views, to ensure that time is not spent inflating these views
* as the user is switching between pages.
*/
public interface NewTabListener {
void onNewTab(@NonNull Tab tab);
}
}
@@ -1,55 +1,71 @@
package org.thoughtcrime.securesms.components;
import android.Manifest;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionInfoCompat;
import org.thoughtcrime.securesms.util.dualsim.SubscriptionManagerCompat;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
public class ConversationItemFooter extends LinearLayout {
public class ConversationItemFooter extends ConstraintLayout {
private TextView dateView;
private TextView simView;
private ExpirationTimerView timerView;
private ImageView insecureIndicatorView;
private DeliveryStatusView deliveryStatusView;
private boolean onlyShowSendingStatus;
private View audioSpace;
private TextView audioDuration;
private LottieAnimationView revealDot;
private TextView dateView;
private TextView simView;
private ExpirationTimerView timerView;
private ImageView insecureIndicatorView;
private DeliveryStatusView deliveryStatusView;
private boolean onlyShowSendingStatus;
private TextView audioDuration;
private LottieAnimationView revealDot;
private PlaybackSpeedToggleTextView playbackSpeedToggleTextView;
private boolean isOutgoing;
private boolean hasShrunkDate;
private OnTouchDelegateChangedListener onTouchDelegateChangedListener;
private final Rect speedToggleHitRect = new Rect();
private final int touchTargetSize = ViewUtil.dpToPx(48);
private long previousMessageId;
public ConversationItemFooter(Context context) {
super(context);
@@ -67,24 +83,55 @@ public class ConversationItemFooter extends LinearLayout {
}
private void init(@Nullable AttributeSet attrs) {
inflate(getContext(), R.layout.conversation_item_footer, this);
dateView = findViewById(R.id.footer_date);
simView = findViewById(R.id.footer_sim_info);
timerView = findViewById(R.id.footer_expiration_timer);
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
deliveryStatusView = findViewById(R.id.footer_delivery_status);
audioDuration = findViewById(R.id.footer_audio_duration);
audioSpace = findViewById(R.id.footer_audio_duration_space);
revealDot = findViewById(R.id.footer_revealed_dot);
final TypedArray typedArray;
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemFooter, 0, 0);
} else {
typedArray = null;
}
final @LayoutRes int contentId;
if (typedArray != null) {
int mode = typedArray.getInt(R.styleable.ConversationItemFooter_footer_mode, 0);
isOutgoing = mode == 0;
if (isOutgoing) {
contentId = R.layout.conversation_item_footer_outgoing;
} else {
contentId = R.layout.conversation_item_footer_incoming;
}
} else {
contentId = R.layout.conversation_item_footer_outgoing;
isOutgoing = true;
}
inflate(getContext(), contentId, this);
dateView = findViewById(R.id.footer_date);
simView = findViewById(R.id.footer_sim_info);
timerView = findViewById(R.id.footer_expiration_timer);
insecureIndicatorView = findViewById(R.id.footer_insecure_indicator);
deliveryStatusView = findViewById(R.id.footer_delivery_status);
audioDuration = findViewById(R.id.footer_audio_duration);
revealDot = findViewById(R.id.footer_revealed_dot);
playbackSpeedToggleTextView = findViewById(R.id.footer_audio_playback_speed_toggle);
if (typedArray != null) {
setTextColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_text_color, getResources().getColor(R.color.core_white)));
setIconColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_icon_color, getResources().getColor(R.color.core_white)));
setRevealDotColor(typedArray.getInt(R.styleable.ConversationItemFooter_footer_reveal_dot_color, getResources().getColor(R.color.core_white)));
typedArray.recycle();
}
dateView.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (oldLeft != left || oldRight != right) {
notifyTouchDelegateChanged(getPlaybackSpeedToggleTouchDelegateRect(), playbackSpeedToggleTextView);
}
});
}
public void setOnTouchDelegateChangedListener(@Nullable OnTouchDelegateChangedListener onTouchDelegateChangedListener) {
this.onTouchDelegateChangedListener = onTouchDelegateChangedListener;
}
@Override
@@ -99,14 +146,28 @@ public class ConversationItemFooter extends LinearLayout {
presentTimer(messageRecord);
presentInsecureIndicator(messageRecord);
presentDeliveryStatus(messageRecord);
hideAudioDurationViews();
presentAudioDuration(messageRecord);
}
public void setAudioDuration(long totalDurationMillis, long currentPostionMillis) {
long remainingSecs = TimeUnit.MILLISECONDS.toSeconds(totalDurationMillis - currentPostionMillis);
long remainingSecs = Math.max(0, TimeUnit.MILLISECONDS.toSeconds(totalDurationMillis - currentPostionMillis));
audioDuration.setText(getResources().getString(R.string.AudioView_duration, remainingSecs / 60, remainingSecs % 60));
}
public void setPlaybackSpeedListener(@Nullable PlaybackSpeedToggleTextView.PlaybackSpeedListener playbackSpeedListener) {
playbackSpeedToggleTextView.setPlaybackSpeedListener(playbackSpeedListener);
}
public void setAudioPlaybackSpeed(float playbackSpeed, boolean isPlaying) {
if (isPlaying) {
showPlaybackSpeedToggle();
} else {
hidePlaybackSpeedToggle();
}
playbackSpeedToggleTextView.setCurrentSpeed(playbackSpeed);
}
public void setTextColor(int color) {
dateView.setTextColor(color);
simView.setTextColor(color);
@@ -123,7 +184,7 @@ public class ConversationItemFooter extends LinearLayout {
revealDot.addValueCallback(
new KeyPath("**"),
LottieProperty.COLOR_FILTER,
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)
frameInfo -> new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
);
}
@@ -146,6 +207,92 @@ public class ConversationItemFooter extends LinearLayout {
setBackground(null);
}
public @Nullable Projection getProjection() {
if (getVisibility() == VISIBLE) {
return Projection.relativeToViewRoot(this, new Projection.Corners(ViewUtil.dpToPx(11)));
} else {
return null;
}
}
private void notifyTouchDelegateChanged(@NonNull Rect rect, @NonNull View touchDelegate) {
if (onTouchDelegateChangedListener != null) {
onTouchDelegateChangedListener.onTouchDelegateChanged(rect, touchDelegate);
}
}
private void showPlaybackSpeedToggle() {
if (hasShrunkDate) {
return;
}
hasShrunkDate = true;
playbackSpeedToggleTextView.animate()
.alpha(1f)
.scaleX(1f)
.scaleY(1f)
.setDuration(150L)
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
playbackSpeedToggleTextView.setClickable(true);
}
});
if (isOutgoing) {
dateView.setMaxWidth(ViewUtil.dpToPx(28));
} else {
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(this);
constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, ViewUtil.dpToPx(40));
constraintSet.applyTo(this);
}
}
private void hidePlaybackSpeedToggle() {
if (!hasShrunkDate) {
return;
}
hasShrunkDate = false;
playbackSpeedToggleTextView.animate()
.alpha(0f)
.scaleX(0.5f)
.scaleY(0.5f)
.setDuration(150L).setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
playbackSpeedToggleTextView.setClickable(false);
playbackSpeedToggleTextView.clearRequestedSpeed();
}
});
if (isOutgoing) {
dateView.setMaxWidth(Integer.MAX_VALUE);
} else {
ConstraintSet constraintSet = new ConstraintSet();
constraintSet.clone(this);
constraintSet.constrainMaxWidth(R.id.date_and_expiry_wrapper, -1);
constraintSet.applyTo(this);
}
}
private @NonNull Rect getPlaybackSpeedToggleTouchDelegateRect() {
playbackSpeedToggleTextView.getHitRect(speedToggleHitRect);
int widthOffset = (touchTargetSize - speedToggleHitRect.width()) / 2;
int heightOffset = (touchTargetSize - speedToggleHitRect.height()) / 2;
speedToggleHitRect.top -= heightOffset;
speedToggleHitRect.left -= widthOffset;
speedToggleHitRect.right += widthOffset;
speedToggleHitRect.bottom += heightOffset;
return speedToggleHitRect;
}
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
dateView.forceLayout();
if (messageRecord.isFailed()) {
@@ -161,8 +308,10 @@ public class ConversationItemFooter extends LinearLayout {
dateView.setText(errorMsg);
} else if (messageRecord.isPendingInsecureSmsFallback()) {
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
} else if (messageRecord.isRateLimited()) {
dateView.setText(R.string.ConversationItem_send_paused);
} else {
dateView.setText(DateUtils.getExtendedRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
dateView.setText(DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
}
}
@@ -178,7 +327,7 @@ public class ConversationItemFooter extends LinearLayout {
simView.setText(getContext().getString(R.string.ConversationItem_from_s, subscriptionInfo.get().getDisplayName()));
simView.setVisibility(View.VISIBLE);
} else if (subscriptionInfo.isPresent()) {
simView.setText(getContext().getString(R.string.ConversationItem_to_s, subscriptionInfo.get().getDisplayName()));
simView.setText(getContext().getString(R.string.ConversationItem_to_s, subscriptionInfo.get().getDisplayName()));
simView.setVisibility(View.VISIBLE);
} else {
simView.setVisibility(View.GONE);
@@ -207,7 +356,7 @@ public class ConversationItemFooter extends LinearLayout {
boolean mms = messageRecord.isMms();
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
});
@@ -222,6 +371,19 @@ public class ConversationItemFooter extends LinearLayout {
}
private void presentDeliveryStatus(@NonNull MessageRecord messageRecord) {
long newMessageId = buildMessageId(messageRecord);
if (previousMessageId == newMessageId && deliveryStatusView.isPending() && !messageRecord.isPending()) {
if (messageRecord.getRecipient().isGroup()) {
SignalLocalMetrics.GroupMessageSend.onUiUpdated(messageRecord.getId());
} else {
SignalLocalMetrics.IndividualMessageSend.onUiUpdated(messageRecord.getId());
}
}
previousMessageId = newMessageId;
if (messageRecord.isFailed() || messageRecord.isPendingInsecureSmsFallback()) {
deliveryStatusView.setNone();
return;
@@ -234,7 +396,7 @@ public class ConversationItemFooter extends LinearLayout {
deliveryStatusView.setNone();
}
} else {
if (!messageRecord.isOutgoing()) {
if (!messageRecord.isOutgoing()) {
deliveryStatusView.setNone();
} else if (messageRecord.isPending()) {
deliveryStatusView.setPending();
@@ -253,12 +415,13 @@ public class ConversationItemFooter extends LinearLayout {
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
if (mmsMessageRecord.getSlideDeck().getAudioSlide() != null) {
if (messageRecord.isOutgoing()) {
moveAudioViewsForOutgoing();
} else {
moveAudioViewsForIncoming();
}
showAudioDurationViews();
if (messageRecord.getViewedReceiptCount() > 0 || (messageRecord.isOutgoing() && Objects.equals(messageRecord.getRecipient(), Recipient.self()))) {
revealDot.setProgress(1f);
} else {
revealDot.setProgress(0f);
}
} else {
hideAudioDurationViews();
}
@@ -267,44 +430,23 @@ public class ConversationItemFooter extends LinearLayout {
}
}
private void moveAudioViewsForOutgoing() {
removeView(audioSpace);
removeView(audioDuration);
removeView(revealDot);
addView(audioSpace, 0);
addView(revealDot, 0);
addView(audioDuration, 0);
int padStart = ViewUtil.dpToPx(60);
int padLeft = ViewUtil.isLtr(this) ? padStart : 0;
int padRight = ViewUtil.isRtl(this) ? padStart : 0;
audioDuration.setPadding(padLeft, 0, padRight, 0);
}
private void moveAudioViewsForIncoming() {
removeView(audioSpace);
removeView(audioDuration);
removeView(revealDot);
addView(audioSpace);
addView(revealDot);
addView(audioDuration);
audioDuration.setPadding(0, 0, 0, 0);
}
private void showAudioDurationViews() {
audioSpace.setVisibility(View.VISIBLE);
audioDuration.setVisibility(View.VISIBLE);
if (FeatureFlags.viewedReceipts()) {
revealDot.setVisibility(View.VISIBLE);
}
revealDot.setVisibility(View.VISIBLE);
playbackSpeedToggleTextView.setVisibility(View.VISIBLE);
}
private void hideAudioDurationViews() {
audioSpace.setVisibility(View.GONE);
audioDuration.setVisibility(View.GONE);
revealDot.setVisibility(View.GONE);
playbackSpeedToggleTextView.setVisibility(View.GONE);
}
private long buildMessageId(@NonNull MessageRecord record) {
return record.isMms() ? -record.getId() : record.getId();
}
public interface OnTouchDelegateChangedListener {
void onTouchDelegateChanged(@NonNull Rect delegateRect, @NonNull View delegateView);
}
}
@@ -19,6 +19,8 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
@@ -32,6 +34,9 @@ public class ConversationItemThumbnail extends FrameLayout {
private Outliner outliner;
private Outliner pulseOutliner;
private boolean borderless;
private int[] normalBounds;
private int[] gifBounds;
private int minimumThumbnailWidth;
public ConversationItemThumbnail(Context context) {
super(context);
@@ -60,14 +65,30 @@ public class ConversationItemThumbnail extends FrameLayout {
outliner.setColor(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_20));
int gifWidth = ViewUtil.dpToPx(260);
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.ConversationItemThumbnail, 0, 0);
thumbnail.setBounds(typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0));
normalBounds = new int[]{
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxWidth, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_minHeight, 0),
typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_maxHeight, 0)
};
gifWidth = typedArray.getDimensionPixelSize(R.styleable.ConversationItemThumbnail_conversationThumbnail_gifWidth, gifWidth);
typedArray.recycle();
} else {
normalBounds = new int[]{0, 0, 0, 0};
}
gifBounds = new int[]{
gifWidth,
gifWidth,
1,
Integer.MAX_VALUE
};
minimumThumbnailWidth = -1;
}
@SuppressWarnings("SuspiciousNameCombination")
@@ -88,6 +109,18 @@ public class ConversationItemThumbnail extends FrameLayout {
}
}
public void hideThumbnailView() {
thumbnail.setAlpha(0f);
}
public void showThumbnailView() {
thumbnail.setAlpha(1f);
}
public @NonNull Projection.Corners getCorners() {
return new Projection.Corners(cornerMask.getRadii());
}
public void setPulseOutliner(@NonNull Outliner outliner) {
this.pulseOutliner = outliner;
}
@@ -121,6 +154,7 @@ public class ConversationItemThumbnail extends FrameLayout {
}
public void setMinimumThumbnailWidth(int width) {
minimumThumbnailWidth = width;
thumbnail.setMinimumThumbnailWidth(width);
}
@@ -137,6 +171,17 @@ public class ConversationItemThumbnail extends FrameLayout {
boolean showControls, boolean isPreview)
{
if (slides.size() == 1) {
Slide slide = slides.get(0);
if (slide.isVideoGif()) {
setThumbnailBounds(gifBounds);
} else {
setThumbnailBounds(normalBounds);
if (minimumThumbnailWidth != -1) {
thumbnail.setMinimumThumbnailWidth(minimumThumbnailWidth);
}
}
thumbnail.setVisibility(VISIBLE);
album.setVisibility(GONE);
@@ -167,4 +212,8 @@ public class ConversationItemThumbnail extends FrameLayout {
thumbnail.setDownloadClickListener(listener);
album.setDownloadClickListener(listener);
}
private void setThumbnailBounds(@NonNull int[] bounds) {
thumbnail.setBounds(bounds[0], bounds[1], bounds[2], bounds[3]);
}
}
@@ -4,22 +4,31 @@ import android.content.Context;
import android.graphics.PorterDuff;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.libsignal.util.Pair;
import java.util.LinkedList;
import java.util.List;
public class ConversationTypingView extends LinearLayout {
public class ConversationTypingView extends ConstraintLayout {
private AvatarImageView avatar;
private AvatarImageView avatar1;
private AvatarImageView avatar2;
private AvatarImageView avatar3;
private View bubble;
private TypingIndicatorView indicator;
private TextView typistCount;
public ConversationTypingView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
@@ -29,27 +38,58 @@ public class ConversationTypingView extends LinearLayout {
protected void onFinishInflate() {
super.onFinishInflate();
avatar = findViewById(R.id.typing_avatar);
bubble = findViewById(R.id.typing_bubble);
indicator = findViewById(R.id.typing_indicator);
avatar1 = findViewById(R.id.typing_avatar_1);
avatar2 = findViewById(R.id.typing_avatar_2);
avatar3 = findViewById(R.id.typing_avatar_3);
typistCount = findViewById(R.id.typing_count);
bubble = findViewById(R.id.typing_bubble);
indicator = findViewById(R.id.typing_indicator);
}
public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists, boolean isGroupThread) {
public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists, boolean isGroupThread, boolean hasWallpaper) {
if (typists.isEmpty()) {
indicator.stopAnimation();
return;
}
Recipient typist = typists.get(0);
bubble.getBackground().setColorFilter(typist.getColor().toConversationColor(getContext()), PorterDuff.Mode.MULTIPLY);
avatar1.setVisibility(GONE);
avatar2.setVisibility(GONE);
avatar3.setVisibility(GONE);
typistCount.setVisibility(GONE);
if (isGroupThread) {
avatar.setAvatar(glideRequests, typist, true);
avatar.setVisibility(VISIBLE);
presentGroupThreadAvatars(glideRequests, typists);
}
if (hasWallpaper) {
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.conversation_item_wallpaper_bubble_color));
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.conversation_item_wallpaper_bubble_color), PorterDuff.Mode.SRC_IN);
} else {
avatar.setVisibility(GONE);
bubble.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.signal_background_secondary));
typistCount.getBackground().setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_background_secondary), PorterDuff.Mode.SRC_IN);
}
indicator.startAnimation();
}
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) {
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1);
avatar1.setVisibility(VISIBLE);
if (typists.size() > 1) {
avatar2.setAvatar(glideRequests, typists.get(1), false);
avatar2.setVisibility(VISIBLE);
}
if (typists.size() == 3) {
avatar3.setAvatar(glideRequests, typists.get(2), false);
avatar3.setVisibility(VISIBLE);
}
if (typists.size() > 3) {
typistCount.setText(getResources().getString(R.string.ConversationTypingView__plus_d, typists.size() - 2));
typistCount.setVisibility(VISIBLE);
}
}
}
@@ -7,9 +7,11 @@ import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.drawable.shapes.RoundRectShape;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class CornerMask {
@@ -29,10 +31,7 @@ public class CornerMask {
}
public void mask(Canvas canvas) {
bounds.left = 0;
bounds.top = 0;
bounds.right = canvas.getWidth();
bounds.bottom = canvas.getHeight();
bounds.set(canvas.getClipBounds());
corners.reset();
corners.addRoundRect(bounds, radii, Path.Direction.CW);
@@ -57,6 +56,13 @@ public class CornerMask {
radii[6] = radii[7] = bottomLeft;
}
public void setRadii(float topLeft, float topRight, float bottomRight, float bottomLeft) {
radii[0] = radii[1] = topLeft;
radii[2] = radii[3] = topRight;
radii[4] = radii[5] = bottomRight;
radii[6] = radii[7] = bottomLeft;
}
public void setTopLeftRadius(int radius) {
radii[0] = radii[1] = radius;
}
@@ -72,4 +78,8 @@ public class CornerMask {
public void setBottomLeftRadius(int radius) {
radii[6] = radii[7] = radius;
}
public float[] getRadii() {
return radii;
}
}
@@ -1,32 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.EditText;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.R;
/**
* Custom styled search view that we can insert into ActionBar menus
*/
public class DarkSearchView extends androidx.appcompat.widget.SearchView {
public DarkSearchView(@NonNull Context context) {
this(context, null);
}
public DarkSearchView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, R.attr.search_view_style_dark);
}
public DarkSearchView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
super(context, attrs, defStyleAttr);
EditText searchText = findViewById(androidx.appcompat.R.id.search_src_text);
searchText.setTextColor(ContextCompat.getColor(context, R.color.signal_text_toolbar_subtitle));
}
}

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