Compare commits

..

434 Commits

Author SHA1 Message Date
Alex Hart
b917cccbee Bumped version to 4.60.0 2020-05-14 11:22:28 -03:00
Alex Hart
01d2d05d8e Updated language translations. 2020-05-14 11:22:28 -03:00
Alan Evans
4de86cb6cf Prevent ZkGroup link crashes. 2020-05-14 11:22:28 -03:00
henry
8861ad76ed Fix start SubmitDebugLog from registration and passphrase prompt. 2020-05-14 11:22:28 -03:00
Alan Evans
ef86372635 Ensure that the unknown UUID does not create an entry. 2020-05-14 11:22:28 -03:00
Alex Hart
ccff7b1148 Implement new group creation screens behind flag. 2020-05-14 11:22:28 -03:00
Greyson Parrelli
ed0825112d Fix some ordering problems with conversation data loading. 2020-05-14 11:22:28 -03:00
Alan Evans
b8df90531f GV2 message contexts. 2020-05-14 11:22:28 -03:00
Greyson Parrelli
f099c3591c Run PushProcessMessageJobs in parallel. 2020-05-14 11:22:28 -03:00
Greyson Parrelli
ed33e048ad Add CachedLayoutInflater to improve conversation render performance. 2020-05-14 11:22:28 -03:00
Greyson Parrelli
7fd3bfa30c Revert "Check to see if FCM is available at app launch."
This reverts commit eea7174f1d.
2020-05-14 11:22:28 -03:00
Alex Hart
07a492a32c Add dot character to reactions bottom sheet all tab label. 2020-05-14 11:22:28 -03:00
Alan Evans
11fffbd79e Remove P2P group change sending. 2020-05-14 11:22:28 -03:00
Alan Evans
eff564ad88 Adapt message requests to support invite flow. 2020-05-14 11:22:28 -03:00
Greyson Parrelli
d3d53e6099 Reduce recipient dirty state logging verbosity. 2020-05-14 11:22:28 -03:00
Greyson Parrelli
53d122ed55 Fix jumping to last seen position. 2020-05-14 11:22:28 -03:00
Alan Evans
1778c1ef7d Prevent some IOExceptions when past the end of stream. 2020-05-14 11:22:28 -03:00
Alan Evans
a510bc74e6 Recipient Id cache. 2020-05-14 11:22:28 -03:00
Alan Evans
a9ecdbdfec Groups V2 capability set by the feature flag. 2020-05-14 11:22:28 -03:00
Alan Evans
06ab3cf013 Fix cases of inlined & missing log tags. 2020-05-14 11:22:28 -03:00
Alan Evans
3db5da1c8d Generalize media input for use with Audio. 2020-05-14 11:22:28 -03:00
Greyson Parrelli
5937a50b6d Fix message receive timestamps on media messages. 2020-05-14 11:22:28 -03:00
Alan Evans
b4191ee5cc Fix usages of service logging in app. 2020-05-14 11:22:28 -03:00
Alan Evans
c63e42715e New logging lint checks.
[LogNotAppSignal] tells you about using signal service logger in the app.
[LogTagInlined] tells you about not using a constant tag.
2020-05-14 11:22:28 -03:00
Alex Hart
26e582d806 Integrate RingRTC v2.0.1 2020-05-14 11:22:28 -03:00
Alan Evans
ee9270845a Create GV2 group behind feature flag. 2020-05-14 11:22:28 -03:00
Alan Evans
6cf33897c0 Remove superfluous groups v2 capability checks. 2020-05-14 11:22:28 -03:00
Freddy Tuxworth
2161bbb8fa Display "No matching countries" when no filter matches found.
Fixes #9518
2020-05-14 11:22:28 -03:00
Greyson Parrelli
b75088874e Migrate conversation rendering to the paging library. 2020-05-14 11:22:28 -03:00
Alan Evans
9ac1897880 Job changes for GroupsV2 message receive and profile key updates. 2020-05-14 11:22:28 -03:00
Alan Evans
36c43ed2fa Ensure latest V2 group state from server upon conversation open. 2020-05-14 11:22:28 -03:00
Alan Evans
8084822f16 Connect GV2 title and avatar updates and prevent no-change avatar updates. 2020-05-14 11:22:28 -03:00
Alan Evans
959718618f Deprecate some ViewUtil methods. Inline others. Remove some old API code. 2020-05-14 11:22:28 -03:00
Alan Evans
75f3fe0cec Correct access control for MMS groups. 2020-05-14 11:22:28 -03:00
Alan Evans
b800477365 GV2 leave and eject operations. 2020-05-14 11:22:28 -03:00
Alex Hart
b191341c57 Add some polish to the groups V2 manager UI. 2020-05-14 11:22:28 -03:00
leet
88a40be901 Fix backup timestamp language.
Fixes #8842
Fixes #8986
2020-05-14 11:22:28 -03:00
Greyson Parrelli
3fef58057e Add additional info to support emails and debuglogs. 2020-05-14 11:22:28 -03:00
Greyson Parrelli
b156e4a79a Always use the UD cert with a UUID. 2020-05-14 11:22:28 -03:00
Alan Evans
30ac264cd3 Rename Group update message classes. 2020-05-14 11:22:28 -03:00
Alan Evans
a9b00e1cd3 Remove instances of Android logging. 2020-05-14 11:22:28 -03:00
Alex Hart
d94fc4bc13 Implement ability to react with any emoji behind a flag. 2020-05-14 11:22:28 -03:00
Greyson Parrelli
40b5339ef8 Allow auto-download for users you've shared your profile with. 2020-05-14 11:22:28 -03:00
Alan Evans
86f0456e8c Group Manager V2 operations. 2020-05-14 11:22:28 -03:00
Alan Evans
48a693793f GV2 Group Manager. 2020-05-14 11:22:28 -03:00
Alan Evans
ff28d72db6 New GV2 internal prefix and scrubber. 2020-05-13 16:18:18 -04:00
Alan Evans
456857bbbd Add custom lint rule project. 2020-05-13 16:18:18 -04:00
Alan Evans
7f17b66a6c Upgrade gradle and gradle plugin. 2020-05-13 16:18:18 -04:00
Greyson Parrelli
310ec8f296 Remove CellServiceConstraint in favor of NetworkOrCellServiceConstraint.
If a job was enqueued with a CellServiceConstraint (which is currently
only SMS jobs), then it'll never run until it gets service, even if you
flip the "enable SMS sending over wifi" toggle.

This has created bad situations in the past, where SMS jobs just get
stuck on devices that never report having cell service (like VM's or
wifi only devices).

This fixes it by *always* using NetworkOrCellServiceConstraint, and then
deciding whether a constraint is met by checking the "wifi SMS" setting
at decision-time.
2020-05-13 16:18:18 -04:00
Alan Evans
0c2afa9438 Fix FCM token via phone call registration.
Fixes #8992
2020-05-13 16:18:18 -04:00
Alex Hart
c3832cf8b1 New group notifications management ui. 2020-05-13 16:18:18 -04:00
Greyson Parrelli
a2de8a2a05 Ensure you can't set null values in DefaultValueLiveData. 2020-05-13 16:18:18 -04:00
Greyson Parrelli
3b601896d2 Fix crash in SubmitDebugLogActivity. 2020-05-13 16:18:18 -04:00
Greyson Parrelli
e1a90bcb00 Transition conversation loading from a Loader to a Repository. 2020-05-13 16:18:18 -04:00
Greyson Parrelli
2b65916344 Show calling foreground notification on all OS versions.
Fixes #9516
Fixes #9414
2020-05-13 16:18:18 -04:00
Greyson Parrelli
f149005026 Add support for remote config v1.1 2020-05-13 16:18:18 -04:00
Alex Hart
5eb663aa1b New group avatar and name selection screen. 2020-05-13 16:18:18 -04:00
Alan Evans
12b7d6c0e3 Use bottom sheet shape. 2020-05-13 16:18:18 -04:00
Alan Evans
723639d928 New group management screen. 2020-05-13 16:18:18 -04:00
Greyson Parrelli
e0502c24e1 Only search for visible parts of a contact. 2020-05-13 16:18:18 -04:00
Alex Hart
358d6333c3 Open new recipient bottom sheet when accessing contact from group context. 2020-05-13 16:18:18 -04:00
Alan Evans
0b279d1df3 Group contact chips behind feature flag. 2020-05-13 16:18:18 -04:00
Alan Evans
8e0fba7992 New group button behind new Group UI feature flag. 2020-05-13 16:18:18 -04:00
Alex Hart
d5419ec9fa Implement new call screen UI/UX. 2020-05-13 16:18:18 -04:00
Alan Evans
33e3f78be6 LiveDataUtil combineLatest. 2020-05-13 16:17:29 -04:00
Alex Hart
3c5ad519dd Decrease QuoteView reveal animation duration to 150ms. 2020-05-13 16:17:29 -04:00
Alan Evans
17c5b858b5 Recipient bottom sheet. 2020-05-13 16:17:29 -04:00
Greyson Parrelli
f6f6496c9c Bump version to 4.59.10 2020-05-13 15:40:44 -04:00
Greyson Parrelli
b1d725e23a Updated language translations. 2020-05-13 15:40:44 -04:00
Greyson Parrelli
a74622997e Bump libsignal-metadata to 0.1.2
Includes fix for how we're prioritizing UUID vs E164.

Fixes #9621
2020-05-13 15:40:42 -04:00
Greyson Parrelli
b1a200001e Bump version to 4.59.9 2020-05-09 13:19:57 -04:00
Greyson Parrelli
3b1041fa1f Updated language translations. 2020-05-09 13:19:31 -04:00
Greyson Parrelli
a83ccc18bb Fix processing of early messages.
1. Eliminated any possibility of infinite recursion.
2. Handle the fact that you can have multiple 'early contents' for a
   single message.
2020-05-09 13:16:45 -04:00
Greyson Parrelli
618b1b5ace Handle PIN creation failure better. 2020-05-09 13:16:45 -04:00
Greyson Parrelli
14858adc88 Bump version to 4.59.8 2020-05-04 18:22:45 -04:00
Greyson Parrelli
c07f35f3aa Updated language translations. 2020-05-04 18:21:50 -04:00
Alan Evans
87eab27996 Prevent the creation of 'weak' PINs.
Simple checks to prevent the same number, or sequentially
increasing/decreasing PINs. e.g. 1111, 1234, 54321, etc.
2020-05-04 18:17:36 -04:00
Greyson Parrelli
b7296a4fe3 Bump version to 4.59.7 2020-05-04 11:06:28 -04:00
Greyson Parrelli
3fb9ae1fb4 Updated language translations. 2020-05-04 11:05:55 -04:00
Greyson Parrelli
9705939489 Fix issue with editing and forwarding a received video. 2020-05-04 10:54:55 -04:00
Greyson Parrelli
eca67b1204 Broaden exception handling in custom DNS.
A set of LG devices is crashing when using the custom DNS. Safest thing
for now would be to treat all failures as network errors while we we try
to get a repro to figure out what's happening.
2020-05-04 10:54:55 -04:00
Greyson Parrelli
c59fc3581a Make LiveRecipientCache throw exceptions instead of errors.
Errors were causing crash loops if they occur in a job. This will still
allow the app to crash, but prevent loops.
2020-05-04 00:48:09 -04:00
Greyson Parrelli
e00f8c94ff Bumped version to 4.59.6 2020-04-30 17:03:28 -04:00
Greyson Parrelli
4186153f0c Updated language translations. 2020-04-30 17:03:28 -04:00
Greyson Parrelli
6c01807f4f Fix issue with PIN verification. 2020-04-30 17:03:28 -04:00
Greyson Parrelli
9d35fb397b Fix issue with re-using forwarded attachment pointers.
We were deleting upload data for incoming attachments when we shouldn't
have.

Fixes #9570
2020-04-30 16:36:06 -04:00
Jim Gustafson
c9f2f57427 Update to ringrtc v1.3.2 2020-04-30 08:12:31 -07:00
Greyson Parrelli
c862ab0c56 Bump version to 4.59.5 2020-04-28 10:41:37 -04:00
Greyson Parrelli
7aaaa57c14 Updated language translations. 2020-04-28 10:41:06 -04:00
Greyson Parrelli
11b6394a87 Fix issue with group storage IDs. 2020-04-28 10:38:58 -04:00
Greyson Parrelli
bdd48fd2df Show PIN reminder for non-reglock users. 2020-04-28 10:38:49 -04:00
Alan Evans
e99af75400 Fix crash when blocking group. 2020-04-27 16:52:17 -03:00
Greyson Parrelli
321440e13f Bump version to 4.59.4 2020-04-27 13:07:41 -04:00
Greyson Parrelli
0556d984e0 Updated language translations. 2020-04-27 13:07:19 -04:00
Greyson Parrelli
0ba1f66136 Use the same Recipient.self() instance in storage sync. 2020-04-27 13:05:22 -04:00
Greyson Parrelli
7562555687 Add additional storage sync validations. 2020-04-27 12:38:07 -04:00
Greyson Parrelli
668ccfcd12 Clean up logging. 2020-04-27 11:29:52 -04:00
Greyson Parrelli
9c0337c4ef Fix threading issue with message resends. 2020-04-27 11:11:24 -04:00
Greyson Parrelli
3fde06ab0f Bump version to 4.59.3 2020-04-24 19:48:20 -04:00
Greyson Parrelli
73959f328a Updated language translations. 2020-04-24 19:47:57 -04:00
Greyson Parrelli
cca85bfee3 Fix some PinState bugs. 2020-04-24 19:40:50 -04:00
Greyson Parrelli
575caa53d3 Fix some storage service consistency issues. 2020-04-24 19:14:08 -04:00
Greyson Parrelli
33874a8866 Fix attachment upload bug. 2020-04-24 09:24:43 -04:00
Greyson Parrelli
b8e909a134 Revert to preferring system photos over profile photos. 2020-04-24 08:45:58 -04:00
Greyson Parrelli
5193a5d309 Prevent some crash loops. 2020-04-23 22:25:56 -04:00
Greyson Parrelli
7db288b9aa Make PINs work with password managers. 2020-04-23 22:25:56 -04:00
Greyson Parrelli
9f033e64aa Fix lint error. 2020-04-23 22:25:56 -04:00
Greyson Parrelli
5a15ba97dc Bump version to 4.59.2 2020-04-23 13:33:59 -04:00
Greyson Parrelli
ce6ec72683 Updated language translations. 2020-04-23 13:33:59 -04:00
Greyson Parrelli
eedbcdd564 Fix issue with group storage sync. 2020-04-23 13:33:59 -04:00
Greyson Parrelli
0ca2848e01 Improve logging for storage service. 2020-04-23 12:03:31 -04:00
Greyson Parrelli
208275b6a9 Improve logging for PinState. 2020-04-23 11:24:23 -04:00
Greyson Parrelli
4bdcaa72cd Fix some more UX issues with blocked users. 2020-04-23 11:06:52 -04:00
Greyson Parrelli
8c6001fa5a Improve correctness and performance of camera contact search. 2020-04-23 10:25:45 -04:00
Greyson Parrelli
c4e88abce1 Update PIN change strings. 2020-04-22 19:42:17 -04:00
Greyson Parrelli
eea7174f1d Check to see if FCM is available at app launch. 2020-04-22 19:32:40 -04:00
Greyson Parrelli
3f7d0688fc Bump version to 4.59.1 2020-04-22 14:18:28 -04:00
Greyson Parrelli
6d319618c6 Updated language translations. 2020-04-22 14:18:28 -04:00
Greyson Parrelli
4250fa707b Fix crash when videos are missing a duration.
Fixes #9556
2020-04-22 14:18:28 -04:00
Greyson Parrelli
7734cd2c8f Clean up some corner cases in storage syncing. 2020-04-22 14:18:28 -04:00
Alan Evans
57467bb338 Dismiss group participant list on contact click. 2020-04-22 14:18:28 -04:00
Alex Hart
8ad61a52b9 Fix call termination when muting before call is connected. 2020-04-22 14:18:28 -04:00
Alan Evans
9742a212a2 Fix Transifex string name clash. 2020-04-22 14:18:28 -04:00
Greyson Parrelli
fd21fc1a31 Fix some UX issues with blocked users. 2020-04-22 14:18:28 -04:00
Greyson Parrelli
1b5a0ab9f3 Sync the profile photo to linked devices when appropriate. 2020-04-22 10:34:43 -04:00
Greyson Parrelli
f466fef20a Fix issue where contact photos weren't being shown at all. 2020-04-22 10:13:56 -04:00
Greyson Parrelli
9bc70adbbd Update PIN setting strings. 2020-04-21 19:23:47 -04:00
Greyson Parrelli
6f39f9849a Bump version to 4.59.0 2020-04-21 16:09:30 -04:00
Greyson Parrelli
5bc950ed28 Updated language translations. 2020-04-21 16:09:30 -04:00
Alan Evans
b80d460a8f Account for deleted conversations in profile key send job. 2020-04-21 16:09:30 -04:00
Alan Evans
3f555ce5e2 Extract method for creating safety number intents. 2020-04-21 16:09:30 -04:00
Jim Gustafson
9513b476ef Update to ringrtc v1.3.1 2020-04-21 16:09:30 -04:00
Greyson Parrelli
8f9e79ae37 Updated PIN strings. 2020-04-21 16:09:30 -04:00
Alan Evans
53b681ef67 Make reaction and remote delete jobs GV2 ready. 2020-04-21 16:09:30 -04:00
Alan Evans
9a8094cb8a Guard against malformed group ids. 2020-04-21 16:09:30 -04:00
Alex Hart
00ee6d0bbd Dialog theme rename. 2020-04-21 16:09:30 -04:00
Greyson Parrelli
83f6640bd3 Add a more generic system for handling early messages. 2020-04-21 16:09:30 -04:00
Alex Hart
2afb939ee6 Implement send support for resumable uploads behind a flag. 2020-04-21 16:09:30 -04:00
Greyson Parrelli
7c442865c5 Interpret non-present message bodies as 'null'. 2020-04-21 16:09:30 -04:00
Greyson Parrelli
b3d57edb24 Update and centralize block strings. 2020-04-21 16:09:30 -04:00
Alex Hart
6d6e017c71 Proactively share profile key after accepting a message request. 2020-04-21 16:09:30 -04:00
Greyson Parrelli
fc6b5c1d7c Add ultramarine as a conversation color option. 2020-04-21 16:09:30 -04:00
Greyson Parrelli
6ecd3b59fd Add pre-alpha receive support for remote delete. 2020-04-21 16:09:13 -04:00
Ehren Kret
456bcf3d57 Require CDN number match rather than use default CDN
This marks messages as failed if the CDN number does not match a
configured CDN number rather than falling back to the default CDN in
the event the CDN is not recognized.
2020-04-21 13:33:41 -04:00
Greyson Parrelli
f12a9b9ac7 Store the server timestamp for a message. 2020-04-21 13:33:41 -04:00
Greyson Parrelli
00b6a222bd Remove jumpiness when rendering reactions. 2020-04-21 13:33:41 -04:00
Greyson Parrelli
b8ccc4453e Update pins4all flag.
We still have to keep the legacy one though so that people in the old
bucket stay in the new one.
2020-04-21 13:33:41 -04:00
Alan Evans
dbb31420af Ensure all support article urls are the correct format and not translatable. 2020-04-21 13:33:41 -04:00
Greyson Parrelli
35f4f3f81e Add support for passing data between jobs. 2020-04-21 13:33:41 -04:00
Greyson Parrelli
acbfff89d3 Update registration to allow PIN entry. 2020-04-21 13:33:41 -04:00
Greyson Parrelli
6b37675a81 Remove long-press action in settings. 2020-04-21 13:33:41 -04:00
Greyson Parrelli
a471ffa6d8 Fix UD indicators for sent transcripts. 2020-04-21 13:33:41 -04:00
Alan Evans
7bf090fdab GroupsV2 state mapping. 2020-04-21 13:33:41 -04:00
Alan Evans
4e0279200f Refactor out MediaPreviewActivity Intent creation method. 2020-04-21 13:33:41 -04:00
Alan Evans
78055e3ccb GroupsV2 update sending and local context storage. 2020-04-21 13:33:41 -04:00
Alan Evans
f5e6fd6340 Allow RetrieveProfileJob to be used for self. 2020-04-21 13:33:33 -04:00
Alex Hart
2d60d5fb1f Check menu item visibility when calculating menu size. 2020-04-21 13:33:33 -04:00
Alan Evans
c6dd25a119 Ensure group membership for typing indicators. 2020-04-21 13:33:33 -04:00
Alan Evans
68d29d9a0f Allow pending member invite cancelation. 2020-04-21 13:33:33 -04:00
Alan Evans
1d63970a25 Hardcode all class names in old work manager migration. 2020-04-21 13:33:33 -04:00
Alan Evans
2b1ffac564 Groups V2 avatar download job. 2020-04-21 13:33:33 -04:00
Alan Evans
e2d3a43593 Use readBodyBytes for correct exceptions. 2020-04-21 13:33:33 -04:00
Greyson Parrelli
8e13403cca Separate PINs from Registration Lock.
You can now have a PIN without having registration lock.

Note: We still need to change the registration flow to allow non-reglock
users to enter their PIN.
2020-04-21 13:33:33 -04:00
Ehren Kret
3c6a7b76ca Send increased protocol version number if CDN key or attachment are used. 2020-04-21 13:33:33 -04:00
Alan Evans
428128651e Move database protos to separate files. 2020-04-21 13:33:33 -04:00
Alan Evans
326678f214 Add support for GV2 group update messages. 2020-04-21 13:33:29 -04:00
Alex Hart
1f994495f8 Clear search if user sends message. 2020-04-21 13:33:29 -04:00
Greyson Parrelli
fb1637006d Include screen size details in debuglogs. 2020-04-21 13:33:29 -04:00
Ehren Kret
37a35e8f70 Add initial support for send/receive on CDN2. 2020-04-21 13:33:29 -04:00
Alan Evans
1290d0ead9 Add pending member activity. 2020-04-21 13:33:25 -04:00
Greyson Parrelli
ef0f26b64c Remove borders from images in the gallery picker. 2020-04-03 12:20:15 -04:00
Greyson Parrelli
485d211768 Remove border from images in the attachment keyboard. 2020-04-03 12:20:15 -04:00
Greyson Parrelli
f1ea035197 Re-enable and clean up Signal PINs.
- Require PINs during registration agian.
- Change min length to 4.
- Allow the full-screen megaphone to be enabled remotely.
- Clean up and remove some code.
2020-04-03 12:20:15 -04:00
Martijn van den Hoek
6f961ade74 Fix crash when importing vcf after exporting it.
Fixes #9465
2020-04-03 12:20:15 -04:00
Alan Evans
b8e17e0116 Enable video trimming feature by default. 2020-04-03 12:20:15 -04:00
Alan Evans
040e1fe8f6 Apply dark theme to scroll to bottom button. 2020-04-03 12:20:15 -04:00
Alan Evans
e9c92bdf51 Show unblock dialog when tap blocked contact. 2020-04-03 12:20:15 -04:00
Alan Evans
48c33f3dcd GroupsV2 service changes. 2020-04-03 12:20:15 -04:00
Alex Hart
6b2bc924dd Prefer profile photo over system contact photo. 2020-04-03 12:20:15 -04:00
Ehren Kret
a65c4f90f4 Avoid potential race condition in attachment uploads. 2020-04-03 12:19:13 -04:00
Alan Evans
04bb4b351a Refactor group leave dialog out of conversation. 2020-04-03 12:19:13 -04:00
Alan Evans
e02e4d52b4 Prevent empty message processing. 2020-04-03 12:19:13 -04:00
Alex Hart
6f3c4434f6 Add animation when replying to a message. 2020-04-03 12:19:13 -04:00
Greyson Parrelli
711715ca1e Add DNS fallback system. 2020-04-03 12:19:12 -04:00
Greyson Parrelli
d6000af843 Re-use recently-acquired attachment pointers. 2020-04-03 12:19:12 -04:00
Greyson Parrelli
9b0954a898 Bump version to 4.58.5 2020-04-03 11:18:39 -04:00
Greyson Parrelli
42a2c33fd7 Updated language translations. 2020-04-03 11:18:13 -04:00
Greyson Parrelli
a4d18a18d9 Don't use vector assets for notification icon. 2020-04-03 11:14:36 -04:00
Alex Hart
bf32409d4e Split drawable into light and dark. 2020-04-03 12:14:09 -03:00
Greyson Parrelli
e38aec225f Bump version to 4.58.4 2020-04-01 14:48:47 -04:00
Greyson Parrelli
995b7a4712 Updated language translations. 2020-04-01 14:48:25 -04:00
Alex Hart
9fe3026941 Fix AdaptiveActionsToolbar sizing algorithm. 2020-04-01 11:26:19 -03:00
Greyson Parrelli
520658e1b8 Bump version to 4.58.3 2020-03-31 17:10:00 -04:00
Greyson Parrelli
f822d8eddb Updated language translations. 2020-03-31 16:59:36 -04:00
Alex Hart
2f879ce4d6 Remove MMS groups from message request logic. 2020-03-31 16:00:26 -03:00
Greyson Parrelli
24528bf101 Fix accent color in alert dialogs in dark theme. 2020-03-31 14:59:00 -04:00
Greyson Parrelli
822682caba Fix NPE in BitmapUtil.toJpeg()
Fixes #9513
2020-03-31 11:44:18 -04:00
Greyson Parrelli
5dc3cc65a8 Bump version to 4.58.2 2020-03-30 17:53:08 -04:00
Greyson Parrelli
0f80caffb5 Updated language translations. 2020-03-30 17:53:08 -04:00
Greyson Parrelli
6c428b2777 Fix issue with some notifications linking to the wrong conversation. 2020-03-30 17:53:08 -04:00
Greyson Parrelli
c9be37b84a Fix camera rotation issues.
Had to manually detect when CameraX is giving us bad data.

Fixes #9509
2020-03-30 16:04:28 -04:00
Jim Gustafson
87ea2f86c0 Update ringrtc to v1.2.0 2020-03-30 13:01:14 -07:00
Greyson Parrelli
7e80be5ca0 Separate out model info in debug logs. 2020-03-30 15:24:29 -04:00
Greyson Parrelli
989a818a67 Fix issue where reaction notifications may jump to the wrong message. 2020-03-30 12:49:08 -04:00
Greyson Parrelli
af2e17df9e Ensure old typing observers are unsubscribed. 2020-03-30 11:33:21 -04:00
Greyson Parrelli
728ec1c16d Fix issue where leave messages were pending forever. 2020-03-30 10:11:15 -04:00
Greyson Parrelli
f859c5b1b5 Prevent conscrypt crash during profile retrieval.
This was a mitigation that was previously in place that was forgotten
during the recent avatar refactor.
2020-03-29 18:53:30 -04:00
Greyson Parrelli
ab600d7df1 Bump version to 4.58.1 2020-03-27 16:51:21 -04:00
Greyson Parrelli
4644f64fd6 Updated language translations. 2020-03-27 16:51:21 -04:00
Greyson Parrelli
c274312265 Fix scrolling in the emoji variation popup. 2020-03-27 16:51:21 -04:00
Greyson Parrelli
f8e63098a2 Don't show empty date popovers. 2020-03-27 16:40:55 -04:00
Greyson Parrelli
264d353ec2 Bump quality of camera1 photos. 2020-03-27 16:30:36 -04:00
Alex Hart
2b58dcbe7f Remove explicit CameraX initialization. 2020-03-27 16:30:36 -04:00
Greyson Parrelli
dc791487c5 Jump to the relevant message when tapping a reaction notification.
Fixes #9503
2020-03-27 16:30:36 -04:00
Greyson Parrelli
5637f132d4 Fixed issue where leave message wasn't displayed locally. 2020-03-27 16:30:36 -04:00
Alan Evans
9e6cca1cd0 GV2 database. 2020-03-27 16:30:36 -04:00
Alan Evans
640c82d517 GV2 group context proto. 2020-03-27 16:30:36 -04:00
Greyson Parrelli
20d1a93b09 Don't refresh own profile if not registered. 2020-03-27 16:30:36 -04:00
Greyson Parrelli
f5d1b11bda Fix some dark theme text contrast issues. 2020-03-27 16:30:36 -04:00
Alan Evans
66c7f8bcb2 GroupId for GV2. 2020-03-27 11:28:48 -03:00
Alex Hart
d8fa46c558 Copy action should display if message body is not empty.
Fixes #9491
2020-03-27 11:18:02 -03:00
Greyson Parrelli
10bfc8a753 Migrate avatars and group avatars. 2020-03-26 22:38:33 -04:00
Greyson Parrelli
9848599807 Bump version to 4.58.0 2020-03-26 17:49:10 -04:00
Greyson Parrelli
2e38ebcfbb Updated language translations. 2020-03-26 17:46:51 -04:00
Greyson Parrelli
f875623cd0 Resize avatars to 1024x1024. 2020-03-26 17:37:52 -04:00
Greyson Parrelli
e6f9cb9929 Remove TextSecurePreferences.getAvatarId() 2020-03-26 17:37:52 -04:00
Greyson Parrelli
6aac3baa55 Remove TextSecurePreferences.getProfileName() 2020-03-26 17:37:52 -04:00
Alan Evans
a860315587 GroupId class. 2020-03-26 17:37:52 -04:00
Greyson Parrelli
a73a73e42c Fix AudioView tinting on Android 10. 2020-03-26 17:37:52 -04:00
Greyson Parrelli
a3358e5b21 Rotate profile key after blocking if shared via group. 2020-03-26 17:37:52 -04:00
Alex Hart
7e9e2fead2 Fix NPE after call failure. 2020-03-26 17:37:52 -04:00
Alan Evans
0269a3eb6f Groups V2 protobufs and local conflict resolution. 2020-03-26 17:37:51 -04:00
Alex Hart
f449a45912 Utilize normal fallback for homescreen icons. 2020-03-26 17:37:51 -04:00
Alan Evans
f69d4ccd22 Increase Lib Signal Service compatibility to Java 8. 2020-03-26 17:37:51 -04:00
Alan Evans
0e2df2adbb Image Editor: Keep text on top.
Sorts children by a new z-order.
2020-03-26 17:37:51 -04:00
Alex Hart
d46894e5db Upgrade CameraX to Beta01. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
951a61117a Add storage support for the AccountRecord. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
7a038ab09d Add interim storage support for GroupV2Record. 2020-03-26 17:37:51 -04:00
Alex Hart
707a2aca0a Swap profile megaphone icon and use user avatar if present. 2020-03-26 17:37:51 -04:00
Alan Evans
624837fcf1 Include zkgroup 0.4.1
All behind feature flag, excluding .so files for space.
2020-03-26 17:37:51 -04:00
Greyson Parrelli
e3ea36c76f Remove unnecessary okhttp close when canceling.
Canceling should handle closing stuff now. And if we close from a
different thread than the calling thread, okhttp will crash.
2020-03-26 17:37:51 -04:00
Alan Evans
453996c374 Restrict CI branches. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
8add9ba0a6 Removed ExperienceUpgradeActivity.
Pour one out.
2020-03-26 17:37:51 -04:00
Alan Evans
da11b56eab Check for and clear quote on new intent.
Fixes #9478
2020-03-26 17:37:51 -04:00
Greyson Parrelli
19377c2132 Remote maxInstance restriction on RetrieveProfileAvatarJob. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
b2bff39fe1 Don't send group info requests in response to group info requests. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
5f7075d39a Update and refactor storage service syncing.
Switched to proto3, updated protos, and generally refactored things to
make it easier to add new storage record types.
2020-03-26 17:37:51 -04:00
Alex Hart
40d9d663ec Disable auto-mirror for help icon 2020-03-26 17:37:51 -04:00
Greyson Parrelli
31f9b77c32 Ignore empty names when populating contact list. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
690a66a093 Show any user with a displayable name in the contact list. 2020-03-26 17:37:51 -04:00
Ehren Kret
e7e7d36774 Configure Android Studio to pickup protobuf generated sources 2020-03-26 17:37:51 -04:00
Greyson Parrelli
f95a37956c Improve emoji sticker suggestions.
There was a bug around some emoji being marked as 'obsolete' and
therefore not being found.

I also made a change so that you can use skin variations of emoji and
still find emoji tagged with the default yellow version of it.

Fixes #9471
2020-03-26 17:37:51 -04:00
Greyson Parrelli
1e2a27f902 Close dangling groups cursor. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
d90e3dc210 Fix crash when syncing empty usernames. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
5df4b56c0d Update okhttp to 3.12.10 2020-03-26 17:37:51 -04:00
Jim Gustafson
436da1cb32 Update ringrtc to v1.1.0 2020-03-26 17:37:51 -04:00
Greyson Parrelli
4d0dbbc6cd Add ability to listen to jobs based on a filter. 2020-03-26 17:37:51 -04:00
Alan Evans
033bf77cbb Allow future display of pending member count. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
1068c3ca7e Fix UnknownSenderView in dark theme. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
df4422369d Update icons and colors. 2020-03-26 17:37:51 -04:00
Greyson Parrelli
a62183c9e0 Reduce AttachmentCipherTest flakiness. 2020-03-19 14:50:06 -04:00
Greyson Parrelli
de48cf8243 Bump version to 4.57.2 2020-03-19 14:49:52 -04:00
Greyson Parrelli
acd4fc4518 Fix issue where minimum PIN length was miscalculated.
Fixes #9484
2020-03-19 14:49:10 -04:00
Greyson Parrelli
da59ed019f Bump version to 4.57.1 2020-03-07 16:43:35 -05:00
Greyson Parrelli
e73b174d1d Updated language translations. 2020-03-07 16:43:08 -05:00
Alan Evans
2753a22e3a Remove old activity from manifest. 2020-03-07 16:40:17 -05:00
Alex Hart
79fc33630b Add toast instead of crash if no email app installed. 2020-03-07 16:40:17 -05:00
Greyson Parrelli
bf5331ba6e Bump version to 4.57.0 2020-03-05 18:51:45 -05:00
Greyson Parrelli
3be47d3e54 Updated language translations. 2020-03-05 18:51:21 -05:00
Alex Hart
f9de131017 Add new contact us flow. 2020-03-05 18:42:17 -05:00
Alan Evans
f1f505d41c Try getKeyStoreEntry twice on UnrecoverableKeyException.
To try to get around potentially temporary UnrecoverableKeyExceptions.
2020-03-05 18:42:17 -05:00
Alan Evans
51603be5ec Add video trimming time indication pill. 2020-03-05 18:42:17 -05:00
Alex Hart
2152b4a2cd Add warning dialog for insecure calls. 2020-03-05 18:42:17 -05:00
Alan Evans
a70023a32b Use group manager to leave group. 2020-03-05 18:42:17 -05:00
Alan Evans
5038210d78 Add tap to pause to video trimming editor. 2020-03-05 18:42:17 -05:00
Alan Evans
28bbfd88b2 Group member dialog update. 2020-03-05 18:42:17 -05:00
Greyson Parrelli
d05a71c8fe Update Glide to 4.11.0 2020-03-03 08:52:11 -05:00
Greyson Parrelli
245b0a7e50 Add a new buildType with Flipper. 2020-03-02 16:25:05 -05:00
Alan Evans
ceb9e4aee2 Write capabilities to service. 2020-03-02 12:01:50 -04:00
Alan Evans
d2e94dad7e Remove argon2 test job. 2020-03-02 11:22:37 -04:00
Alex Hart
240b2108f3 Use the image editor for avatars. 2020-03-02 11:21:57 -04:00
Greyson Parrelli
f68d99d16d Clean up dangling transfer files.
Fixes #9033
2020-03-02 10:11:40 -05:00
Greyson Parrelli
44e845c875 Update emoji. 2020-03-02 10:11:40 -05:00
Alan Evans
d8e2368a18 Convert UUID supported into a Capability enum. 2020-03-02 10:11:40 -05:00
Alan Evans
172a43679d Add GV2 recipient capability. 2020-03-02 10:11:40 -05:00
Greyson Parrelli
82305ce2b3 Bump version to 4.56.4 2020-03-02 09:55:04 -05:00
Greyson Parrelli
eaf73edcad Updated language translations. 2020-03-02 09:54:35 -05:00
Greyson Parrelli
543a4ee177 Fix backup restore crash. 2020-03-02 09:48:18 -05:00
Greyson Parrelli
fd2a464bae Use internal contact viewer for avatar clicks. 2020-03-02 08:33:25 -05:00
Alex Hart
b06152ba58 Fix and simplify Y translation calculation for Conversation. 2020-03-02 08:57:11 -04:00
Greyson Parrelli
c24d285cd3 Bump version to 4.56.3 2020-02-28 17:35:13 -05:00
Greyson Parrelli
be39cd653e Updated language translations. 2020-02-28 17:35:13 -05:00
Greyson Parrelli
6813f47bc1 Remove feature flags that are no longer remote capable. 2020-02-28 17:35:10 -05:00
Greyson Parrelli
8e795c4177 Read the sticker length during backup import. 2020-02-28 16:58:47 -05:00
Greyson Parrelli
9c96afee09 Fix storage service crash with new users. 2020-02-28 16:58:47 -05:00
Greyson Parrelli
d507be0ab0 Don't backup the KeyValueDatabase. 2020-02-28 16:58:47 -05:00
Greyson Parrelli
75a52f801a Implement storage service protocol changes. 2020-02-28 16:58:47 -05:00
Greyson Parrelli
d3b123f3a9 Fix StorageSyncHelperTest. 2020-02-28 08:07:36 -05:00
Greyson Parrelli
da3cdd984b Bump version to 4.56.2 2020-02-26 17:57:36 -05:00
Greyson Parrelli
6184e5f828 Update the storage service. 2020-02-26 17:11:34 -05:00
Curt Brune
133bd44b85 Update ringrtc to v1.0.2 2020-02-26 17:11:34 -05:00
Greyson Parrelli
0c254c9621 Improve debuglog submission. 2020-02-26 17:11:34 -05:00
Greyson Parrelli
1faf196f82 Implement additional message request improvements. 2020-02-26 17:11:29 -05:00
Greyson Parrelli
81c7887d47 Switch language string to 'System default.' 2020-02-26 17:08:27 -05:00
Greyson Parrelli
e62e630987 Fix theming issue in CameraContactSelectionFragment. 2020-02-26 17:08:27 -05:00
Alex Hart
739e38a047 Fix reaction details bottom sheet scrolling. 2020-02-26 17:08:27 -05:00
Greyson Parrelli
8c23b17517 Fix some state issues post backup restore. 2020-02-26 17:08:27 -05:00
Greyson Parrelli
fda8f3e1ce Refer to yourself as 'you' in reactions and group membership. 2020-02-26 17:08:27 -05:00
Alex Hart
9e5f64c431 Improve message requests, add megaphone. 2020-02-26 17:08:27 -05:00
Alex Hart
dc689d325b Various PIN bug fixes. 2020-02-26 17:06:21 -05:00
Greyson Parrelli
0a883dc234 Fix issue with mimeType resolution in share flow. 2020-02-26 17:06:06 -05:00
Greyson Parrelli
3824e90997 Improve prekey refresh logic. 2020-02-26 17:06:06 -05:00
Greyson Parrelli
5158a15379 Disable PIN requirement for new registrations. 2020-02-26 17:06:06 -05:00
Curt Brune
1bae79af5b Check callManager reference is still valid in ListenableFutureTask callbacks. 2020-02-26 17:06:04 -05:00
Curt Brune
58b7612987 Drop requests to deny stale incoming calls.
This is not an illegal state, as the remote side could have hung-up
a microsecond before the local side tries to deny the call.
2020-02-26 17:06:02 -05:00
Curt Brune
9506da6dd3 Validate activePeer during Bluetooth and Speaker audio state transitions. 2020-02-26 17:05:54 -05:00
Greyson Parrelli
4ea886d05a Bump version to 4.56.1 2020-02-14 12:20:40 -05:00
Greyson Parrelli
7a52fccfd1 Updated language translations. 2020-02-14 12:20:40 -05:00
Greyson Parrelli
f79d308a9f Update profile name megaphone. 2020-02-14 12:20:40 -05:00
Curt Brune
5aa64641d2 Convert IllegalStateException to warning log in receivedBusy()
This is not a fatal condition.  Convert to warning log message in
order to gather more information.
2020-02-14 08:13:11 -08:00
Curt Brune
ae594a0400 Check activeRemote is non-null during handleScreenOffChange() 2020-02-14 08:13:11 -08:00
Curt Brune
eeece55b45 Check activeRemote is non-null during handleWiredHeadsetChange() 2020-02-14 08:13:11 -08:00
Greyson Parrelli
9fbc50d26f Fix crash when cleared ShareViewModel. 2020-02-14 09:57:26 -05:00
Greyson Parrelli
16ebf0556a Bump version to 4.56.0 2020-02-13 20:53:10 -05:00
Greyson Parrelli
bb104b5763 Updated language translations. 2020-02-13 20:53:10 -05:00
Greyson Parrelli
9bac88697b Support sharing multiple photos/videos into Signal. 2020-02-13 20:53:10 -05:00
Alan Evans
7ab240643e Remove build instructions. 2020-02-13 20:53:10 -05:00
Greyson Parrelli
70d5b798b2 Add some developer utils. 2020-02-13 20:53:10 -05:00
Greyson Parrelli
4e7a92637c Follow system theme on Android 10+. 2020-02-13 20:53:10 -05:00
Greyson Parrelli
2e19d0459b Fix album ordering issue.
Fixes #9381
2020-02-13 20:53:10 -05:00
Curt Brune
0970fd7040 Update ringrtc to v1.0.1
Add support for RingRTC Call Manager, a new component which provides
the control layer for all calling features.
2020-02-13 20:53:10 -05:00
Greyson Parrelli
81532cad95 Use a min length of 6 for new PIN reminders. 2020-02-13 20:53:10 -05:00
Greyson Parrelli
dcb5f7b211 Update copy for PIN setting. 2020-02-13 20:53:10 -05:00
Alan Evans
40fd7ca332 Video trimming behind feature flag. 2020-02-13 20:53:10 -05:00
Alan Evans
7f867a6185 Remove KBS restore after set check. 2020-02-13 20:53:10 -05:00
Alan Evans
23e55ac5f7 Clear unidentified access mode when profile key changes. 2020-02-13 20:53:10 -05:00
Jonathan Leitschuh
455974cb05 Add official Gradle Wrapper validation action.
See: https://github.com/gradle/wrapper-validation-action
2020-02-13 20:53:10 -05:00
Greyson Parrelli
66a668f55b Add some Swoon blessed packs. 2020-02-13 20:53:10 -05:00
Alan Evans
7ecb50a3fe Versioned Profiles support (disabled). 2020-02-13 20:53:10 -05:00
Alan Evans
f10d1eac61 Migrate profile key into database. 2020-02-13 20:53:10 -05:00
Alan Evans
b92c389a5b Fix media rail thumbnail for in-app recorded video. 2020-02-13 20:53:10 -05:00
Alex Hart
9dfc57c462 Fix RTL icon issue in toolbar. 2020-02-13 20:53:10 -05:00
Alex Hart
3ea1492d67 Add profile names megaphone. 2020-02-13 20:53:07 -05:00
Greyson Parrelli
c041614d1f Only store remote values for flags in a whitelist. 2020-02-11 12:03:34 -05:00
Greyson Parrelli
d9c5907ea9 Bump version to 4.55.8 2020-02-11 12:03:12 -05:00
Greyson Parrelli
0b6a52277d Fixed PIN reminders showing too often, reminder UI improvements. 2020-02-11 12:03:12 -05:00
Greyson Parrelli
944adb5d7c Bump version to 4.55.7 2020-02-10 20:01:47 -05:00
Greyson Parrelli
ac4129c1e1 Updated language translations. 2020-02-10 20:01:28 -05:00
Greyson Parrelli
0220a88ea5 Mark registration complete after PIN failure. 2020-02-10 19:56:41 -05:00
Greyson Parrelli
6d33763ec9 Bump version to 4.55.6 2020-02-08 10:58:43 -05:00
Greyson Parrelli
3cedadbb97 Fix video duration rendering issue. 2020-02-08 10:58:12 -05:00
Greyson Parrelli
5a6d339a89 Bump version to 4.55.5 2020-02-08 10:26:01 -05:00
Greyson Parrelli
905d8a4f33 Updated language translations. 2020-02-08 10:26:01 -05:00
Greyson Parrelli
917d312ea0 Reduced padding above megaphone button. 2020-02-08 10:26:01 -05:00
Greyson Parrelli
ddc01b539f Use megaphones for PIN reminders. 2020-02-07 21:14:20 -05:00
Greyson Parrelli
38e4733433 Improve network reliability when setting PINs. 2020-02-07 01:06:41 -05:00
Greyson Parrelli
cbd7160e23 Show profile creation before PIN creation. 2020-02-07 00:47:52 -05:00
Greyson Parrelli
f2b3acb0c9 Update string for creating PIN. 2020-02-07 00:15:42 -05:00
Greyson Parrelli
49c7b5c442 Bump version to 4.55.4 2020-02-06 11:19:46 -05:00
Greyson Parrelli
7b7d180207 Updated language translations. 2020-02-06 11:19:46 -05:00
Alex Hart
21cf390d0e Clip reactions mask if the view falls below the input panel. 2020-02-06 11:19:46 -05:00
Greyson Parrelli
5e59f77f83 Update PIN feature flags. 2020-02-06 11:19:46 -05:00
Greyson Parrelli
62814490b3 Only store remote values for flags in a whitelist. 2020-02-06 11:19:46 -05:00
Alan Evans
e5fedb8163 Add pull request trigger. 2020-02-06 07:52:32 -05:00
Greyson Parrelli
cfc74c1080 Bump version to 4.55.3 2020-02-05 20:50:25 -05:00
Greyson Parrelli
eaf53ad3b9 Updated language translations. 2020-02-05 20:49:59 -05:00
Greyson Parrelli
a1f0c198a7 Auto-show keyboard for PIN entry. 2020-02-05 18:49:31 -05:00
Greyson Parrelli
6b1e48e485 Don't show the full-screen PIN megaphone. 2020-02-05 18:49:31 -05:00
Greyson Parrelli
83ea919434 Remove "(You)" from the reactions details.
This reverts commit ed2b049ad4.
2020-02-05 11:36:57 -05:00
Greyson Parrelli
c94e93b916 Fix issue with reactions pill not appearing. 2020-02-05 11:32:20 -05:00
Alex Hart
49d418bb39 Fix RTL support for reaction sending. 2020-02-05 11:20:36 -05:00
Greyson Parrelli
23adda1817 Update reaction pill padding. 2020-02-05 10:40:08 -05:00
Alex Hart
7c729c2c4e Add AdaptiveActionsToolbar for better context bar controls. 2020-02-05 08:51:05 -04:00
Greyson Parrelli
ecf7a416eb Bump version to 4.55.2 2020-02-04 20:10:36 -05:00
Greyson Parrelli
2f87f2bb62 Updated language translations. 2020-02-04 20:10:00 -05:00
Greyson Parrelli
39f4102e81 Fix issue where quoted replies showed a 'save' option. 2020-02-04 19:21:01 -05:00
Greyson Parrelli
4fcd6b15ed Improved feel of reaction popover. 2020-02-04 19:21:01 -05:00
Greyson Parrelli
8f9ed4bc40 Fix the shape of reaction pills. 2020-02-04 19:21:01 -05:00
Alex Hart
73dedd79d2 Fix crash in reactions overlay OnTouch. 2020-02-04 19:21:01 -05:00
Greyson Parrelli
2754b397d5 Darken the background more when selecting a message. 2020-02-04 19:21:01 -05:00
Greyson Parrelli
7d949ee8fd Prevent crash in megaphones after backup restore. 2020-02-04 19:21:01 -05:00
Greyson Parrelli
28e2f22550 Fix layout issues with reaction badges. 2020-02-04 19:21:01 -05:00
Alex Hart
eaa1760511 Animate toolbar in reactions overlay. 2020-02-04 19:21:01 -05:00
Alex Hart
53dfd3f4c0 Fix wrong avatar displaying in reactions fragment. 2020-02-04 19:21:01 -05:00
Alex Hart
ed2b049ad4 Add You to reactions you've sent in the bottom dialog fragment. 2020-02-04 10:35:45 -04:00
Alex Hart
092fb40333 Check if target is attached to window before trying to mask it. 2020-02-04 09:47:26 -04:00
Alan Evans
36a4225858 Always return passphrase without spaces. 2020-02-04 08:10:58 -05:00
Greyson Parrelli
e551ea8bd9 Bump version to 4.55.1 2020-02-04 00:59:45 -05:00
Greyson Parrelli
5a28b1bf1c Updated language translations. 2020-02-04 00:59:15 -05:00
Greyson Parrelli
4cd1129c92 Show snooze snackbars for longer. 2020-02-04 00:55:38 -05:00
Greyson Parrelli
a5d7bc4efc Force custom emoji for reactions. 2020-02-04 00:42:41 -05:00
Greyson Parrelli
1ff5b2af2a Capitalize 'PIN' in strings. 2020-02-04 00:01:07 -05:00
Greyson Parrelli
82446ce30a Hide attachment keyboard after selecting an action. 2020-02-04 00:00:06 -05:00
Alan Evans
6465248483 Fix megaphone snooze. 2020-02-03 22:44:43 -05:00
Greyson Parrelli
48e7f82466 Fix issue with view-once toggle breaking video controls. 2020-02-03 18:26:49 -05:00
Greyson Parrelli
6fef21ebc0 Update permission copy in attachment keyboard. 2020-02-03 15:32:20 -05:00
Greyson Parrelli
837e594607 Bump version to 4.55.0 2020-02-03 15:05:05 -05:00
Greyson Parrelli
ab0cb55b80 Updated language translations. 2020-02-03 15:05:05 -05:00
Greyson Parrelli
2d24c8c525 Update conditions for PIN megaphone.
Handles additional corner cases.
- Shows megaphone when you register with a v1 pin.
- Show fullscreen when you fail to set a PIN during registration.
2020-02-03 15:04:53 -05:00
Alan Evans
40383f3733 Handle presenting KBS account locked cases. 2020-02-03 15:04:53 -05:00
Alex Hart
e14861d79d CreatePinActivity naming update and copy fixes. 2020-02-03 15:04:53 -05:00
Alan Evans
b29b3d0432 Require at least 4 digits during registration. 2020-02-03 15:04:53 -05:00
Greyson Parrelli
c21d4861c0 Clear text entry after changing PIN types. 2020-02-03 15:04:53 -05:00
Greyson Parrelli
a6786e5c2b Fix strings in KBS PIN flow. 2020-02-03 15:04:53 -05:00
Greyson Parrelli
77caa9e9d4 Fix crash in getIntentForPinCreate(), show 'Create' in prefs. 2020-02-03 15:04:53 -05:00
Greyson Parrelli
835ef02872 Add an 'All' tab to reaction details. 2020-02-03 15:04:53 -05:00
Alex Hart
279dcb1428 Apply KBS Lock fixes and pluralization 2020-02-03 15:04:53 -05:00
Alan Evans
4a8c312e0a Clear pin on confirm screen on submit. 2020-02-03 15:04:53 -05:00
Alan Evans
c2bc376f87 Hide PIN from summary when feature flag set. 2020-02-03 15:04:37 -05:00
Greyson Parrelli
73160d4d26 Update reactions UI. 2020-02-03 14:20:08 -05:00
Alan Evans
1dd2a4e9c5 Allow backup passphrase verification. 2020-02-03 14:20:08 -05:00
Alan Evans
ed0c4b8de5 Remove KBS feature flag. 2020-02-03 14:20:08 -05:00
Greyson Parrelli
4f921d761d Enable reaction sending. 2020-02-03 14:20:08 -05:00
Greyson Parrelli
37f85d6deb Delete unused megaphones from the database. 2020-02-03 14:20:08 -05:00
Alex Hart
e1b75c78ab Add Pins for All Megaphone Kill Switch. 2020-02-03 14:20:08 -05:00
Alan Evans
5e83206e6e Fix group timer message. 2020-02-03 14:20:08 -05:00
Alan Evans
1ea6838db6 Bring KBS fragment source into RegistrationLockFragment and handle account locked. 2020-02-03 14:20:08 -05:00
Alex Hart
fb82420376 Implement new PIN UX. 2020-02-01 12:42:29 -04:00
Greyson Parrelli
109d67956f Implement new attachment keyboard.
Such beauty. Such grace.
2020-02-01 12:38:53 -04:00
Greyson Parrelli
9f7b2e2cfd Track the first time a megaphone appeared. 2020-01-30 11:40:22 -05:00
Greyson Parrelli
22f9bfeceb Add support for creating Megaphones. Includes reactions megaphone. 2020-01-29 19:15:02 -05:00
Greyson Parrelli
ef4c7e96da Bump version to 4.54.3 2020-01-29 18:31:07 -05:00
Greyson Parrelli
02865f99a9 Limit impact of crash on unexpected SMS receive. 2020-01-29 18:28:59 -05:00
Greyson Parrelli
ef6019f13b Fix reddit link previews. 2020-01-29 18:26:16 -05:00
Greyson Parrelli
33d02bb7b8 Bump version to 4.54.2 2020-01-28 15:31:11 -05:00
Greyson Parrelli
d34df2c1cf Updated language translations. 2020-01-28 15:30:11 -05:00
Alex Hart
7fdf540742 Implement new reaction notifications. 2020-01-28 15:48:24 -04:00
Alex Hart
f916aabb98 Fix NPE when retrieving display name of unknown recipient. 2020-01-28 15:22:41 -04:00
Alex Hart
4ae7d56db4 Fix NPE when returning to profile from background.
Also generally improves saved-state management for Profile editor.
2020-01-28 14:57:17 -04:00
Alex Hart
e3878ffde7 Change profile preference screen to use toolbar. 2020-01-28 13:41:06 -04:00
Alex Hart
5221b6fb43 Fix expiration timer display issue on devices with modified font sizes.
Fixes #9335
2020-01-27 14:54:04 -04:00
Alex Hart
5e0fe86858 Add SM-G920F and BLK-L09 to LegacyCameraModels. 2020-01-27 14:32:48 -04:00
Alex Hart
c86ced0911 Add back arrow to profile editor. 2020-01-27 14:30:53 -04:00
Alex Hart
0aad82d3d7 Check content disposition flag in carrier config before parsing PDU.
Fixes #9081
2020-01-27 12:15:32 -04:00
1255 changed files with 86552 additions and 27874 deletions

View File

@@ -1,6 +1,11 @@
name: Android CI
on: [push]
on:
pull_request:
push:
branches:
- 'master'
- '4.**'
jobs:
build:
@@ -9,9 +14,17 @@ jobs:
steps:
- uses: actions/checkout@v1
- name: set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Install NDK
run: echo "y" | sudo /usr/local/lib/android/sdk/tools/bin/sdkmanager --install "ndk;20.0.5594570" --sdk_root=${ANDROID_SDK_ROOT}
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
- name: Build with Gradle
run: ./gradlew qa

View File

@@ -1,74 +0,0 @@
Building Signal
===============
Basics
------
Signal uses [Gradle](http://gradle.org) to build the project and to maintain
dependencies. However, you needn't install it yourself; the
"gradle wrapper" `gradlew`, mentioned below, will do that for you.
Building Signal
---------------
The following steps should help you (re)build Signal from the command line.
1. Checkout the Signal-Android project source with the command:
git clone https://github.com/signalapp/Signal-Android.git
2. Make sure you have the [Android SDK](https://developer.android.com/sdk/index.html) installed.
3. Ensure that the following packages are installed from the Android SDK manager:
* Android SDK Build Tools (see buildToolsVersion in build.gradle)
* SDK Platform (All API levels)
* Android Support Repository
* Google Repository
4. Create a local.properties file at the root of your source checkout and add an sdk.dir entry to it. For example:
sdk.dir=/Application/android-sdk-macosx
5. Using Java 8
6. Execute Gradle:
./gradlew build
Visual assets
----------------------
Source assets tend to be large binary blobs, which are best stored outside of git repositories. Some source files are SVGs that can be auto-colored and sized using a tool like [android-res-utils](https://github.com/sebkur/android-res-utils).
Sample command for generating our audio placeholder image:
```bash
pngs_from_svg.py ic_audio.svg /path/to/Signal/res/ 150 --color #000 --opacity 0.54 --suffix _light
pngs_from_svg.py ic_audio.svg /path/to/Signal/res/ 150 --color #fff --opacity 1.00 --suffix _light
```
Setting up a development environment
------------------------------------
[Android Studio](https://developer.android.com/sdk/installing/studio.html) is the recommended development environment.
1. Install Android Studio.
2. Open Android Studio. On a new installation, the Quickstart panel will appear. If you have open projects, close them using "File > Close Project" to see the Quickstart panel.
3. From the Quickstart panel, choose "Configure" then "SDK Manager".
4. In the SDK Tools tab of the SDK Manager, make sure that the "Android Support Repository" is installed, and that the latest "Android SDK build-tools" are installed. Click "OK" to return to the Quickstart panel.
5. From the Quickstart panel, choose "Checkout from Version Control" then "git".
6. Paste the URL for the Signal-Android project when prompted (https://github.com/signalapp/Signal-Android.git).
7. Android studio should detect the presence of a project file and ask you whether to open it. Click "yes".
9. Default config options should be good enough.
9. Project initialisation and build should proceed.
Contributing code
-----------------
Code contributions should be sent via github as pull requests, from feature branches [as explained here](https://help.github.com/articles/using-pull-requests).
Mailing list
------------
Development discussion happens on the whispersystems mailing list.
[To join](https://lists.riseup.net/www/info/whispersystems)
Send emails to whispersystems@lists.riseup.net

View File

@@ -27,7 +27,6 @@ Interested in helping to translate Signal? Contribute here:
https://www.transifex.com/projects/p/signal-android/
## Contributing Code
Instructions on how to setup your development environment and build Signal can be found in [BUILDING.md](https://github.com/signalapp/Signal-Android/blob/master/BUILDING.md).
If you're new to the Signal codebase, we recommend going through our issues and picking out a simple bug to fix (check the "easy" label in our issues) in order to get yourself familiar. Also please have a look at the [CONTRIBUTING.md](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md), that might answer some of your questions.

View File

@@ -1,6 +1,6 @@
[main]
host = https://www.transifex.com
lang_map = da_DK:da-rDK,he:iw,id:in,kn_IN:kn-rIN,pt_BR:pt-rBR,pt_PT:pt,qu_EC:qu-rEC,sv_SE:sv-rSE,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW
lang_map = da_DK:da-rDK,fil:tl,he:iw,id:in,kn_IN:kn-rIN,pa_PK:pa-rPK,pt_BR:pt-rBR,pt_PT:pt,qu_EC:qu-rEC,sv_SE:sv-rSE,zh_CN:zh-rCN,zh_HK:zh-rHK,zh_TW:zh-rTW
[signal-android.master]
file_filter = src/main/res/values-<lang>/strings.xml

View File

@@ -15,7 +15,7 @@ buildscript {
}
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.1'
classpath 'com.android.tools.build:gradle:3.6.3'
classpath 'androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.10'
}
@@ -80,116 +80,8 @@ protobuf {
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0-beta01'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.google.android.material:material:1.0.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.navigation:navigation-fragment:2.1.0'
implementation 'androidx.navigation:navigation-ui:2.1.0'
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.camera:camera-core:1.0.0-alpha06"
implementation "androidx.camera:camera-camera2:1.0.0-alpha06"
implementation('com.google.firebase:firebase-messaging:17.3.4') {
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'
}
implementation 'com.google.android.gms:play-services-maps:16.1.0'
implementation 'com.google.android.gms:play-services-auth:16.0.1'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.0.0'
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation project(':libsignal-service')
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:0.3.3'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation 'com.github.bumptech.glide:glide:4.9.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
annotationProcessor 'androidx.annotation:annotation:1.1.0'
implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0'
implementation 'pl.tajchert:waitingdots:0.1.0'
implementation 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
implementation 'com.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0'
implementation 'mobi.upod:time-duration-picker:1.1.3'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.zxing:core:3.2.1'
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
exclude group: 'com.squareup.okhttp', module: 'okhttp'
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation 'com.annimon:stream:1.1.8'
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
implementation 'com.airbnb.android:lottie:3.0.7'
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 ('com.googlecode.ez-vcard:ez-vcard:0.9.11') {
exclude group: 'com.fasterxml.jackson.core'
exclude group: 'org.freemarker'
}
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'org.mockito:mockito-core:1.9.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'org.robolectric:robolectric:4.2'
testImplementation 'org.robolectric:shadows-multidex:4.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
dependencyVerification {
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
}
def canonicalVersionCode = 594
def canonicalVersionName = "4.54.1"
def canonicalVersionCode = 631
def canonicalVersionName = "4.60.0"
def postFixSize = 10
def abiPostFix = ['universal' : 0,
@@ -223,16 +115,18 @@ android {
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api.directory.signal.org\""
buildConfigField "String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\""
buildConfigField "String", "CONTENT_PROXY_HOST", "\"contentproxy.signal.org\""
buildConfigField "int", "CONTENT_PROXY_PORT", "443"
buildConfigField "String", "SIGNAL_AGENT", "\"OWA\""
buildConfigField "String", "MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
buildConfigField "String", "KEY_BACKUP_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
buildConfigField "String", "CDS_MRENCLAVE", "\"cd6cfc342937b23b1bdd3bbf9721aa5615ac9ff50a75c5527d441cd3276826c9\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"fe7c1bfae98f9b073d220366ea31163ee82f6d04bead774f71ca8e5c40847bfe\""
buildConfigField "String", "KBS_MRENCLAVE", "\"a3baab19ef6ce6f34ab9ebb25ba722725ae44a8872dc0ff08ad6d83a9489de87\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
@@ -267,6 +161,7 @@ android {
exclude 'META-INF/LICENSE'
exclude 'META-INF/NOTICE'
exclude 'META-INF/proguard/androidx-annotations.pro'
exclude 'lib/*/libzkgroup.so' // TODO: GV2 Remove line to include .so when used
}
buildTypes {
@@ -300,11 +195,17 @@ android {
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service-staging.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn-staging.signal.org\""
buildConfigField "String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\""
buildConfigField "String", "SIGNAL_CONTACT_DISCOVERY_URL", "\"https://api-staging.directory.signal.org\""
buildConfigField "String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\""
buildConfigField "String", "MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
buildConfigField "String", "KEY_BACKUP_ENCLAVE_NAME", "\"a1e9c1d3f352b5c4f0fc7a421b98119e60e5ff703c28fbea85c66bfa7306deab\""
buildConfigField "String", "CDS_MRENCLAVE", "\"ba4ebb438bc07713819ee6c98d94037747006d7df63fc9e44d2d6f1fec962a79\""
buildConfigField "String", "KBS_ENCLAVE_NAME", "\"823a3b2c037ff0cbe305cc48928cfcc97c9ed4a8ca6d49af6f7d6981fb60a4e9\""
buildConfigField "String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\""
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"\""
}
flipper {
initWith debug
minifyEnabled false
}
release {
minifyEnabled true
@@ -353,6 +254,129 @@ android {
}
}
dependencies {
lintChecks project(':lintchecks')
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.0.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.preference:preference:1.0.0'
implementation 'androidx.legacy:legacy-preference-v14:1.0.0'
implementation 'androidx.gridlayout:gridlayout:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.navigation:navigation-fragment:2.1.0'
implementation 'androidx.navigation:navigation-ui:2.1.0'
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.camera:camera-core:1.0.0-beta01"
implementation "androidx.camera:camera-camera2:1.0.0-beta01"
implementation "androidx.camera:camera-lifecycle:1.0.0-beta01"
implementation "androidx.concurrent:concurrent-futures:1.0.0"
implementation "androidx.autofill:autofill:1.0.0"
implementation "androidx.paging:paging-common:2.1.2"
implementation "androidx.paging:paging-runtime:2.1.2"
implementation('com.google.firebase:firebase-messaging:17.3.4') {
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'
}
implementation 'com.google.android.gms:play-services-maps:16.1.0'
implementation 'com.google.android.gms:play-services-auth:16.0.1'
implementation 'com.google.android.exoplayer:exoplayer-core:2.9.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.9.1'
implementation 'org.conscrypt:conscrypt-android:2.0.0'
implementation 'org.signal:aesgcmprovider:0.0.3'
implementation project(':libsignal-service')
implementation 'org.signal:zkgroup-android:0.7.0'
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.0.1'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
implementation 'com.jpardogo.materialtabstrip:library:1.0.9'
implementation 'org.apache.httpcomponents:httpclient-android:4.3.5'
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
implementation 'com.github.bumptech.glide:glide:4.11.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
annotationProcessor 'androidx.annotation:annotation:1.1.0'
implementation 'com.makeramen:roundedimageview:2.1.0'
implementation 'com.pnikosis:materialish-progress:1.5'
implementation 'org.greenrobot:eventbus:3.0.0'
implementation 'pl.tajchert:waitingdots:0.1.0'
implementation 'com.melnykov:floatingactionbutton:1.3.0'
implementation 'com.google.zxing:android-integration:3.1.0'
implementation 'mobi.upod:time-duration-picker:1.1.3'
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.zxing:core:3.2.1'
implementation ('com.davemorrissey.labs:subsampling-scale-image-view:3.6.0') {
exclude group: 'com.android.support', module: 'support-annotations'
}
implementation ('cn.carbswang.android:NumberPickerView:1.0.9') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.tomergoldst.android:tooltips:1.0.6') {
exclude group: 'com.android.support', module: 'appcompat-v7'
}
implementation ('com.klinkerapps:android-smsmms:4.0.1') {
exclude group: 'com.squareup.okhttp', module: 'okhttp'
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation 'com.annimon:stream:1.1.8'
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
implementation 'com.airbnb.android:lottie:3.0.7'
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 ('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'
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:3.11.1'
testImplementation 'org.mockito:mockito-core:1.9.5'
testImplementation 'org.powermock:powermock-api-mockito:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4:1.6.1'
testImplementation 'org.powermock:powermock-module-junit4-rule:1.6.1'
testImplementation 'org.powermock:powermock-classloading-xstream:1.6.1'
testImplementation 'androidx.test:core:1.2.0'
testImplementation ('org.robolectric:robolectric:4.2') {
exclude group: 'com.google.protobuf', module: 'protobuf-java'
}
testImplementation 'org.robolectric:shadows-multidex:4.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
dependencyVerification {
configuration = '(play|website)(Debug|Release)RuntimeClasspath'
}
def assembleWebsiteDescriptor = { variant, file ->
if (file.exists()) {
MessageDigest md = MessageDigest.getInstance("SHA-256");

View File

@@ -17,9 +17,17 @@
<issue id="ButtonOrder" severity="error" />
<issue id="ExtraTranslation" severity="warning" />
<!-- Custom lints -->
<issue id="LogNotSignal" severity="error" />
<issue id="LogNotAppSignal" severity="error" />
<issue id="LogTagInlined" severity="error" />
<issue id="RestrictedApi" severity="error">
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/VideoCapture.java" />
<ignore path="*/org/thoughtcrime/securesms/mediasend/camerax/CameraXModule.java" />
<ignore path="*/org/thoughtcrime/securesms/conversation/*.java" />
<ignore path="*/org/thoughtcrime/securesms/lock/v2/CreateKbsPinViewModel.java" />
<ignore path="*/org/thoughtcrime/securesms/jobs/StickerPackDownloadJob.java" />
</issue>
</lint>

View File

@@ -0,0 +1,29 @@
{
"data": [
{
"name": "Ottttooooooooo Ocataaaaaaaavius",
"number": "+1 (555) 555-5555",
"label": "Mobile"
},
{
"name": "Victor Von Doom Phd",
"number": "+1 (555) 123-4567",
"label": "Home"
},
{
"name": "Flash Thompson",
"number": "+1 (555) 435-1261",
"label": "Work"
},
{
"name": "Dr. Curtis Connors",
"number": "+1 (555) 992-1567",
"label": "Mobile"
},
{
"name": "Billy Russo",
"number": "+1 (555) 234-1516",
"label": "Mobile"
}
]
}

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms">
<application
android:name=".FlipperApplicationContext"
tools:replace="android:name"/>
</manifest>

View File

@@ -0,0 +1,27 @@
package org.thoughtcrime.securesms;
import com.facebook.flipper.android.AndroidFlipperClient;
import com.facebook.flipper.core.FlipperClient;
import com.facebook.flipper.plugins.databases.DatabasesFlipperPlugin;
import com.facebook.flipper.plugins.inspector.DescriptorMapping;
import com.facebook.flipper.plugins.inspector.InspectorFlipperPlugin;
import com.facebook.flipper.plugins.sharedpreferences.SharedPreferencesFlipperPlugin;
import com.facebook.soloader.SoLoader;
import org.thoughtcrime.securesms.database.FlipperSqlCipherAdapter;
public class FlipperApplicationContext extends ApplicationContext {
@Override
public void onCreate() {
super.onCreate();
SoLoader.init(this, false);
FlipperClient client = AndroidFlipperClient.getInstance(this);
client.addPlugin(new InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()));
client.addPlugin(new DatabasesFlipperPlugin(new FlipperSqlCipherAdapter(this)));
client.addPlugin(new SharedPreferencesFlipperPlugin(this));
client.start();
}
}

View File

@@ -0,0 +1,245 @@
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.flipper.plugins.databases.DatabaseDescriptor;
import com.facebook.flipper.plugins.databases.DatabaseDriver;
import net.sqlcipher.DatabaseUtils;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteStatement;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver}
* and made to work with SqlCipher. Unfortunately I couldn't use it directly, nor subclass it.
*/
public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdapter.Descriptor> {
public FlipperSqlCipherAdapter(Context context) {
super(context);
}
@Override
public List<Descriptor> getDatabases() {
return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext())));
}
@Override
public List<String> getTableNames(Descriptor descriptor) {
SQLiteDatabase db = descriptor.getReadable();
List<String> tableNames = new ArrayList<>();
try (Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", new String[] { "table", "view" })) {
while (cursor != null && cursor.moveToNext()) {
tableNames.add(cursor.getString(0));
}
}
return tableNames;
}
@Override
public DatabaseGetTableDataResponse getTableData(Descriptor descriptor, String table, String order, boolean reverse, int start, int count) {
SQLiteDatabase db = descriptor.getReadable();
long total = DatabaseUtils.queryNumEntries(db, table);
String orderBy = order != null ? order + (reverse ? " DESC" : " ASC") : null;
String limitBy = start + ", " + count;
try (Cursor cursor = db.query(table, null, null, null, null, null, orderBy, limitBy)) {
String[] columnNames = cursor.getColumnNames();
List<List<Object>> rows = cursorToList(cursor);
return new DatabaseGetTableDataResponse(Arrays.asList(columnNames), rows, start, rows.size(), total);
}
}
@Override
public DatabaseGetTableStructureResponse getTableStructure(Descriptor descriptor, String table) {
SQLiteDatabase db = descriptor.getReadable();
Map<String, String> foreignKeyValues = new HashMap<>();
try(Cursor cursor = db.rawQuery("PRAGMA foreign_key_list(" + table + ")", null)) {
while (cursor != null && cursor.moveToNext()) {
String from = cursor.getString(cursor.getColumnIndex("from"));
String to = cursor.getString(cursor.getColumnIndex("to"));
String tableName = cursor.getString(cursor.getColumnIndex("table")) + "(" + to + ")";
foreignKeyValues.put(from, tableName);
}
}
List<String> structureColumns = Arrays.asList("column_name", "data_type", "nullable", "default", "primary_key", "foreign_key");
List<List<Object>> structureValues = new ArrayList<>();
try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) {
while (cursor != null && cursor.moveToNext()) {
String columnName = cursor.getString(cursor.getColumnIndex("name"));
String foreignKey = foreignKeyValues.containsKey(columnName) ? foreignKeyValues.get(columnName) : null;
structureValues.add(Arrays.asList(columnName,
cursor.getString(cursor.getColumnIndex("type")),
cursor.getInt(cursor.getColumnIndex("notnull")) == 0,
getObjectFromColumnIndex(cursor, cursor.getColumnIndex("dflt_value")),
cursor.getInt(cursor.getColumnIndex("pk")) == 1,
foreignKey));
}
}
List<String> indexesColumns = Arrays.asList("index_name", "unique", "indexed_column_name");
List<List<Object>> indexesValues = new ArrayList<>();
try (Cursor indexesCursor = db.rawQuery("PRAGMA index_list(" + table + ")", null)) {
List<String> indexedColumnNames = new ArrayList<>();
String indexName = indexesCursor.getString(indexesCursor.getColumnIndex("name"));
try(Cursor indexInfoCursor = db.rawQuery("PRAGMA index_info(" + indexName + ")", null)) {
while (indexInfoCursor.moveToNext()) {
indexedColumnNames.add(indexInfoCursor.getString(indexInfoCursor.getColumnIndex("name")));
}
}
indexesValues.add(Arrays.asList(indexName,
indexesCursor.getInt(indexesCursor.getColumnIndex("unique")) == 1,
TextUtils.join(",", indexedColumnNames)));
}
return new DatabaseGetTableStructureResponse(structureColumns, structureValues, indexesColumns, indexesValues);
}
@Override
public DatabaseGetTableInfoResponse getTableInfo(Descriptor databaseDescriptor, String table) {
SQLiteDatabase db = databaseDescriptor.getReadable();
try (Cursor cursor = db.rawQuery("SELECT sql FROM sqlite_master WHERE name = ?", new String[] { table })) {
cursor.moveToFirst();
return new DatabaseGetTableInfoResponse(cursor.getString(cursor.getColumnIndex("sql")));
}
}
@Override
public DatabaseExecuteSqlResponse executeSQL(Descriptor descriptor, String query) {
SQLiteDatabase db = descriptor.getWritable();
String firstWordUpperCase = getFirstWord(query).toUpperCase();
switch (firstWordUpperCase) {
case "UPDATE":
case "DELETE":
return executeUpdateDelete(db, query);
case "INSERT":
return executeInsert(db, query);
case "SELECT":
case "PRAGMA":
case "EXPLAIN":
return executeSelect(db, query);
default:
return executeRawQuery(db, query);
}
}
private static String getFirstWord(String s) {
s = s.trim();
int firstSpace = s.indexOf(' ');
return firstSpace >= 0 ? s.substring(0, firstSpace) : s;
}
private static DatabaseExecuteSqlResponse executeUpdateDelete(SQLiteDatabase database, String query) {
SQLiteStatement statement = database.compileStatement(query);
int count = statement.executeUpdateDelete();
return DatabaseExecuteSqlResponse.successfulUpdateDelete(count);
}
private static DatabaseExecuteSqlResponse executeInsert(SQLiteDatabase database, String query) {
SQLiteStatement statement = database.compileStatement(query);
long insertedId = statement.executeInsert();
return DatabaseExecuteSqlResponse.successfulInsert(insertedId);
}
private static DatabaseExecuteSqlResponse executeSelect(SQLiteDatabase database, String query) {
try (Cursor cursor = database.rawQuery(query, null)) {
String[] columnNames = cursor.getColumnNames();
List<List<Object>> rows = cursorToList(cursor);
return DatabaseExecuteSqlResponse.successfulSelect(Arrays.asList(columnNames), rows);
}
}
private static DatabaseExecuteSqlResponse executeRawQuery(SQLiteDatabase database, String query) {
database.execSQL(query);
return DatabaseExecuteSqlResponse.successfulRawQuery();
}
private static @NonNull List<List<Object>> cursorToList(Cursor cursor) {
List<List<Object>> rows = new ArrayList<>();
int numColumns = cursor.getColumnCount();
while (cursor.moveToNext()) {
List<Object> values = new ArrayList<>(numColumns);
for (int column = 0; column < numColumns; column++) {
values.add(getObjectFromColumnIndex(cursor, column));
}
rows.add(values);
}
return rows;
}
private static @Nullable Object getObjectFromColumnIndex(Cursor cursor, int column) {
switch (cursor.getType(column)) {
case Cursor.FIELD_TYPE_NULL:
return null;
case Cursor.FIELD_TYPE_INTEGER:
return cursor.getLong(column);
case Cursor.FIELD_TYPE_FLOAT:
return cursor.getDouble(column);
case Cursor.FIELD_TYPE_BLOB:
return cursor.getBlob(column);
case Cursor.FIELD_TYPE_STRING:
default:
return cursor.getString(column);
}
}
static class Descriptor implements DatabaseDescriptor {
private final SQLCipherOpenHelper sqlCipherOpenHelper;
Descriptor(@NonNull SQLCipherOpenHelper sqlCipherOpenHelper) {
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
}
@Override
public String name() {
return sqlCipherOpenHelper.getDatabaseName();
}
public @NonNull SQLiteDatabase getReadable() {
return sqlCipherOpenHelper.getReadableDatabase();
}
public @NonNull SQLiteDatabase getWritable() {
return sqlCipherOpenHelper.getWritableDatabase();
}
}
}

View File

@@ -0,0 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<resources>
<string name="app_name">Signal (Flipper)</string>
</resources>

View File

@@ -3,7 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
package="org.thoughtcrime.securesms">
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2"/>
<uses-sdk tools:overrideLibrary="androidx.camera.core,androidx.camera.camera2,androidx.camera.lifecycle" />
<permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"
android:label="Access to TextSecure Secrets"
@@ -117,7 +117,9 @@
android:theme="@style/TextSecure.LightTheme.WebRTCCall"
android:excludeFromRecents="true"
android:screenOrientation="portrait"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|screenSize|fontScale"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:launchMode="singleTask"/>
<activity android:name=".InviteActivity"
@@ -148,7 +150,7 @@
<activity android:name=".preferences.MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ShareActivity"
<activity android:name=".sharing.ShareActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:excludeFromRecents="true"
android:launchMode="singleTask"
@@ -156,7 +158,6 @@
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/>
@@ -169,8 +170,15 @@
<data android:mimeType="*/*"/>
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value=".service.DirectShareService" />
</activity>
@@ -245,6 +253,13 @@
android:windowSoftInputMode="stateVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".groups.ui.pendingmemberinvites.PendingMemberInvitesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.LightNoActionBar" />
<activity android:name=".groups.ui.managegroup.ManageGroupActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".DatabaseMigrationActivity"
android:theme="@style/NoAnimation.Theme.AppCompat.Light.DarkActionBar"
android:launchMode="singleTask"
@@ -255,12 +270,7 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ExperienceUpgradeActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".PassphraseCreateActivity"
<activity android:name=".PassphraseCreateActivity"
android:label="@string/AndroidManifest__create_passphrase"
android:windowSoftInputMode="stateUnchanged"
android:theme="@style/TextSecure.LightNoActionBar"
@@ -331,12 +341,12 @@
android:label="@string/AndroidManifest__linked_devices"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".LogSubmitActivity"
<activity android:name=".logsubmit.SubmitDebugLogActivity"
android:label="@string/AndroidManifest__log_submit"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".MediaPreviewActivity"
<activity android:name=".MediaPreviewActivity"
android:label="@string/AndroidManifest__media_preview"
android:windowSoftInputMode="stateHidden"
android:launchMode="singleTask"
@@ -398,6 +408,10 @@
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".mediasend.AvatarSelectionActivity"
android:theme="@style/TextSecure.FullScreenMedia"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".BlockedContactsActivity"
android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -406,19 +420,25 @@
android:theme="@style/TextSecure.DarkTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
android:theme="@style/TextSecure.DarkTheme"/>
<activity android:name=".profiles.edit.EditProfileActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".lock.v2.CreateKbsPinActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateVisible"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".lock.v2.KbsMigrationActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ClearProfileAvatarActivity"
android:theme="@style/Theme.AppCompat.Dialog.Alert"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:icon="@drawable/clear_profile_avatar"
android:label="@string/AndroidManifest_remove_photo">
android:theme="@style/Theme.AppCompat.Dialog.Alert"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:icon="@drawable/clear_profile_avatar"
android:label="@string/AndroidManifest_remove_photo">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"/>
@@ -426,6 +446,11 @@
</intent-filter>
</activity>
<activity android:name=".messagerequests.MessageRequestMegaphoneActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contactshare.ContactShareEditActivity"
android:theme="@style/TextSecure.LightTheme"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -454,6 +479,16 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".pin.PinRestoreActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".groups.ui.creategroup.CreateGroupActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
<activity android:name=".groups.ui.creategroup.details.AddGroupDetailsActivity"
android:theme="@style/TextSecure.LightNoActionBar" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
@@ -608,11 +643,6 @@
android:authorities="org.thoughtcrime.securesms.database.stickerpack"
android:exported="false" />
<provider android:name="androidx.camera.camera2.impl.Camera2Initializer"
android:authorities="${applicationId}.camerax-init"
android:exported="false"
android:enabled="false" />
<receiver android:name=".service.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
@@ -668,17 +698,6 @@
</intent-filter>
</receiver>
<receiver android:name=".ExperienceUpgradeActivity$AppUpgradeReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
<data android:scheme="package" />
</intent-filter>
<intent-filter>
<action android:name="org.thoughtcrime.securesms.ExperienceUpgradeActivity.DISMISS_ACTION"/>
</intent-filter>
</receiver>
<receiver
android:name=".service.PanicResponderListener"
android:exported="true">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 211 KiB

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 447 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 415 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 307 KiB

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 KiB

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 494 KiB

After

Width:  |  Height:  |  Size: 622 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 599 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 952 KiB

After

Width:  |  Height:  |  Size: 559 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 KiB

After

Width:  |  Height:  |  Size: 643 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 647 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 567 KiB

After

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 376 KiB

After

Width:  |  Height:  |  Size: 384 KiB

View File

@@ -0,0 +1,20 @@
package org.thoughtcrime.securesms;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
public final class AppCapabilities {
private AppCapabilities() {
}
private static final boolean UUID_CAPABLE = false;
/**
* @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 SignalServiceProfile.Capabilities getCapabilities(boolean storageCapable) {
return new SignalServiceProfile.Capabilities(UUID_CAPABLE, FeatureFlags.groupsV2(), storageCapable);
}
}

View File

@@ -0,0 +1,55 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.insights.InsightsOptOut;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
/**
* Rule of thumb: if there's something you want to do on the first app launch that involves
* persisting state to the database, you'll almost certainly *also* want to do it post backup
* restore, since a backup restore will wipe the current state of the database.
*/
public final class AppInitialization {
private static final String TAG = Log.tag(AppInitialization.class);
private AppInitialization() {}
public static void onFirstEverAppLaunch(@NonNull Context context) {
Log.i(TAG, "onFirstEverAppLaunch()");
InsightsOptOut.userRequestedOptOut(context);
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.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()));
}
public static void onPostBackupRestore(@NonNull Context context) {
Log.i(TAG, "onPostBackupRestore()");
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.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()));
}
}

View File

@@ -17,24 +17,22 @@
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.camera.camera2.Camera2AppConfig;
import androidx.camera.core.CameraX;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.multidex.MultiDexApplication;
import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.Conscrypt;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.ringrtc.CallConnectionFactory;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -42,39 +40,35 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.gcm.FcmJobService;
import org.thoughtcrime.securesms.insights.InsightsOptOut;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.logging.AndroidLogger;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.logging.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
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.IncomingMessageObserver;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.revealable.ViewOnceMessageManager;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.stickers.BlessedPacks;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.webrtc.voiceengine.WebRtcAudioManager;
@@ -134,9 +128,12 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
initializePendingMessages();
initializeBlobProvider();
initializeCleanup();
initializeCameraX();
FeatureFlags.init();
NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync();
RegistrationUtil.markRegistrationPossiblyComplete();
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
if (Build.VERSION.SDK_INT < 21) {
@@ -150,11 +147,12 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
public void onStart(@NonNull LifecycleOwner owner) {
isAppVisible = true;
Log.i(TAG, "App is now visible.");
FeatureFlags.refresh();
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
}
@Override
@@ -242,14 +240,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
if (!SQLCipherOpenHelper.databaseFileExists(this)) {
Log.i(TAG, "First ever app launch!");
InsightsOptOut.userRequestedOptOut(this);
TextSecurePreferences.setAppMigrationVersion(this, ApplicationMigrations.CURRENT_VERSION);
TextSecurePreferences.setJobManagerVersion(this, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastExperienceVersionCode(this, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(this, true);
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppInitialization.onFirstEverAppLaunch(this);
}
Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE);
@@ -330,9 +321,9 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true);
}
CallConnectionFactory.initialize(this, new RingRtcLogger());
CallManager.initialize(this, new RingRtcLogger());
} catch (UnsatisfiedLinkError e) {
Log.w(TAG, e);
throw new AssertionError("Unable to load ringrtc library", e);
}
}
@@ -386,19 +377,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
});
}
@SuppressLint("RestrictedApi")
private void initializeCameraX() {
if (CameraXUtil.isSupported()) {
new Thread(() -> {
try {
CameraX.init(this, Camera2AppConfig.create(this));
} catch (Throwable t) {
Log.w(TAG, "Failed to initialize CameraX.");
}
}, "signal-camerax-initialization").start();
}
}
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(DynamicLanguageContextWrapper.updateContext(base, TextSecurePreferences.getLanguage(base)));

View File

@@ -29,6 +29,7 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.Preference;
import org.thoughtcrime.securesms.help.HelpFragment;
import org.thoughtcrime.securesms.preferences.AdvancedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.AppProtectionPreferenceFragment;
import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
@@ -66,6 +67,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
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 final DynamicTheme dynamicTheme = new DynamicTheme();
@@ -154,6 +156,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
.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));
@@ -240,6 +244,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
case PREFERENCE_CATEGORY_ADVANCED:
fragment = new AdvancedPreferenceFragment();
break;
case PREFERENCE_CATEGORY_HELP:
fragment = new HelpFragment();
break;
default:
throw new AssertionError();
}

View File

@@ -1,55 +0,0 @@
package org.thoughtcrime.securesms;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
public class BasicIntroFragment extends Fragment {
private static final String ARG_DRAWABLE = "drawable";
private static final String ARG_TEXT = "text";
private static final String ARG_SUBTEXT = "subtext";
private int drawable;
private int text;
private int subtext;
public static BasicIntroFragment newInstance(int drawable, int text, int subtext) {
BasicIntroFragment fragment = new BasicIntroFragment();
Bundle args = new Bundle();
args.putInt(ARG_DRAWABLE, drawable);
args.putInt(ARG_TEXT, text);
args.putInt(ARG_SUBTEXT, subtext);
fragment.setArguments(args);
return fragment;
}
public BasicIntroFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (getArguments() != null) {
drawable = getArguments().getInt(ARG_DRAWABLE);
text = getArguments().getInt(ARG_TEXT );
subtext = getArguments().getInt(ARG_SUBTEXT );
}
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.color_fragment, container, false);
((ImageView)v.findViewById(R.id.watermark)).setImageResource(drawable);
((TextView)v.findViewById(R.id.blurb)).setText(text);
((TextView)v.findViewById(R.id.subblurb)).setText(subtext);
return v;
}
}

View File

@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -45,5 +46,6 @@ public interface BindableConversationItem extends Unbindable {
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
void onReactionClicked(long messageId, boolean isMms);
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
}
}

View File

@@ -0,0 +1,128 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.res.Resources;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.Lifecycle;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
/**
* This should be used whenever we want to prompt the user to block/unblock a recipient.
*/
public final class BlockUnblockDialog {
private BlockUnblockDialog() { }
public static void showBlockFor(@NonNull Context context,
@NonNull Lifecycle lifecycle,
@NonNull Recipient recipient,
@NonNull Runnable onBlock)
{
SimpleTask.run(lifecycle,
() -> buildBlockFor(context, recipient, onBlock, null),
AlertDialog.Builder::show);
}
public static void showBlockAndDeleteFor(@NonNull Context context,
@NonNull Lifecycle lifecycle,
@NonNull Recipient recipient,
@NonNull Runnable onBlock,
@NonNull Runnable onBlockAndDelete)
{
SimpleTask.run(lifecycle,
() -> buildBlockFor(context, recipient, onBlock, onBlockAndDelete),
AlertDialog.Builder::show);
}
public static void showUnblockFor(@NonNull Context context,
@NonNull Lifecycle lifecycle,
@NonNull Recipient recipient,
@NonNull Runnable onUnblock)
{
SimpleTask.run(lifecycle,
() -> buildUnblockFor(context, recipient, onUnblock),
AlertDialog.Builder::show);
}
@WorkerThread
private static AlertDialog.Builder buildBlockFor(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull Runnable onBlock,
@Nullable Runnable onBlockAndDelete)
{
recipient = recipient.resolve();
AlertDialog.Builder builder = new AlertDialog.Builder(context);
Resources resources = context.getResources();
if (recipient.isGroup()) {
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_group_members_wont_be_able_to_add_you);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_block, ((dialog, which) -> onBlock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
}
} else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
if (onBlockAndDelete != 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());
} else {
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
}
}
return builder;
}
@WorkerThread
private static AlertDialog.Builder buildUnblockFor(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull Runnable onUnblock)
{
recipient = recipient.resolve();
AlertDialog.Builder builder = new AlertDialog.Builder(context);
Resources resources = context.getResources();
if (recipient.isGroup()) {
if (DatabaseFactory.getGroupDatabase(context).isActive(recipient.requireGroupId())) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
} else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
}
} else {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
builder.setNegativeButton(android.R.string.cancel, null);
}
return builder;
}
}

View File

@@ -1,15 +1,8 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.ListFragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.cursoradapter.widget.CursorAdapter;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
@@ -17,6 +10,13 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cursoradapter.widget.CursorAdapter;
import androidx.fragment.app.ListFragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.loaders.BlockedContactsLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
@@ -25,21 +25,18 @@ import org.thoughtcrime.securesms.preferences.BlockedContactListItem;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity {
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private final DynamicTheme dynamicTheme = new DynamicTheme();
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
public void onCreate(Bundle bundle, boolean ready) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@@ -51,16 +48,12 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home: finish(); return true;
}
return false;
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
public static class BlockedContactsFragment
@@ -76,14 +69,14 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setListAdapter(new BlockedContactAdapter(getActivity(), GlideApp.with(this), null));
getLoaderManager().initLoader(0, null, this);
setListAdapter(new BlockedContactAdapter(requireActivity(), GlideApp.with(this), null));
LoaderManager.getInstance(this).initLoader(0, null, this);
}
@Override
public void onStart() {
super.onStart();
getLoaderManager().restartLoader(0, null, this);
LoaderManager.getInstance(this).restartLoader(0, null, this);
}
@Override
@@ -114,10 +107,10 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Recipient recipient = ((BlockedContactListItem)view).getRecipient();
Intent intent = new Intent(getActivity(), RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, recipient.getId());
startActivity(intent);
BlockUnblockDialog.showUnblockFor(requireContext(), getLifecycle(), recipient, () -> {
RecipientUtil.unblock(requireContext(), recipient);
LoaderManager.getInstance(this).restartLoader(0, null, this);
});
}
private static class BlockedContactAdapter extends CursorAdapter {
@@ -143,7 +136,5 @@ public class BlockedContactsActivity extends PassphraseRequiredActionBarActivity
((BlockedContactListItem) view).set(glideRequests, recipient);
}
}
}
}

View File

@@ -7,12 +7,26 @@ import androidx.appcompat.app.AlertDialog;
public class ClearProfileAvatarActivity extends Activity {
private static final String ARG_TITLE = "arg_title";
public static Intent createForUserProfilePhoto() {
return new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
}
public static Intent createForGroupProfilePhoto() {
Intent intent = new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
intent.putExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_group_photo);
return intent;
}
@Override
public void onResume() {
super.onResume();
int titleId = getIntent().getIntExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo);
new AlertDialog.Builder(this)
.setTitle(R.string.ClearProfileActivity_remove_profile_photo)
.setTitle(titleId)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
Intent result = new Intent();
@@ -20,6 +34,7 @@ public class ClearProfileAvatarActivity extends Activity {
setResult(Activity.RESULT_OK, result);
finish();
})
.setOnCancelListener(dialog -> finish())
.show();
}

View File

@@ -50,6 +50,8 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
{
private static final String TAG = ContactSelectionActivity.class.getSimpleName();
public static final String EXTRA_LAYOUT_RES_ID = "layout_res_id";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@@ -67,11 +69,11 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
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_PUSH | DisplayMode.FLAG_ACTIVE_GROUPS | DisplayMode.FLAG_INACTIVE_GROUPS | DisplayMode.FLAG_SELF;
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
}
setContentView(R.layout.contact_selection_activity);
setContentView(getIntent().getIntExtra(EXTRA_LAYOUT_RES_ID, R.layout.contact_selection_activity));
initializeToolbar();
initializeResources();
@@ -90,7 +92,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActionB
}
private void initializeToolbar() {
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
this.toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
assert getSupportActionBar() != null;

View File

@@ -28,6 +28,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.HorizontalScrollView;
import android.widget.TextView;
import android.widget.Toast;
@@ -35,26 +36,33 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.google.android.material.chip.ChipGroup;
import com.pnikosis.materialishprogress.ProgressWheel;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
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.SelectedContact;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
@@ -66,9 +74,7 @@ import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* Fragment for selecting a one or more contacts from a list.
@@ -87,8 +93,9 @@ public final class ContactSelectionListFragment extends Fragment
public static final String REFRESHABLE = "refreshable";
public static final String RECENTS = "recents";
private final Debouncer scrollDebounce = new Debouncer(100);
private TextView emptyText;
private Set<SelectedContact> selectedContacts;
private OnContactSelectedListener onContactSelectedListener;
private SwipeRefreshLayout swipeRefresh;
private View showContactsLayout;
@@ -99,16 +106,20 @@ public final class ContactSelectionListFragment extends Fragment
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@Nullable private InviteCallback inviteCallback;
@Nullable private ListCallback listCallback;
private GlideRequests glideRequests;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (context instanceof InviteCallback) {
inviteCallback = (InviteCallback) context;
if (context instanceof ListCallback) {
listCallback = (ListCallback) context;
}
}
@@ -130,14 +141,16 @@ public final class ContactSelectionListFragment extends Fragment
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
handleContactPermissionGranted();
} else {
this.getLoaderManager().initLoader(0, null, this);
LoaderManager.getInstance(this).initLoader(0, null, this);
}
})
.onAnyDenied(() -> {
getActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
FragmentActivity activity = requireActivity();
if (getActivity().getIntent().getBooleanExtra(RECENTS, false)) {
getLoaderManager().initLoader(0, null, ContactSelectionListFragment.this);
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
if (activity.getIntent().getBooleanExtra(RECENTS, false)) {
LoaderManager.getInstance(this).initLoader(0, null, ContactSelectionListFragment.this);
} else {
initializeNoContactsPermission();
}
@@ -149,17 +162,22 @@ public final class ContactSelectionListFragment extends Fragment
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
emptyText = ViewUtil.findById(view, android.R.id.empty);
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
showContactsLayout = view.findViewById(R.id.show_contacts_container);
showContactsButton = view.findViewById(R.id.show_contacts_button);
showContactsDescription = view.findViewById(R.id.show_contacts_description);
showContactsProgress = view.findViewById(R.id.progress);
emptyText = ViewUtil.findById(view, android.R.id.empty);
recyclerView = ViewUtil.findById(view, R.id.recycler_view);
swipeRefresh = ViewUtil.findById(view, R.id.swipe_refresh);
fastScroller = ViewUtil.findById(view, R.id.fast_scroller);
showContactsLayout = view.findViewById(R.id.show_contacts_container);
showContactsButton = view.findViewById(R.id.show_contacts_button);
showContactsDescription = view.findViewById(R.id.show_contacts_description);
showContactsProgress = view.findViewById(R.id.progress);
chipGroup = view.findViewById(R.id.chipGroup);
chipGroupScrollContainer = view.findViewById(R.id.chipGroupScrollContainer);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
swipeRefresh.setEnabled(getActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
swipeRefresh.setEnabled(requireActivity().getIntent().getBooleanExtra(REFRESHABLE, true));
autoScrollOnNewItem();
return view;
}
@@ -170,31 +188,38 @@ public final class ContactSelectionListFragment extends Fragment
}
public @NonNull List<SelectedContact> getSelectedContacts() {
List<SelectedContact> selected = new LinkedList<>();
if (selectedContacts != null) {
selected.addAll(selectedContacts);
}
return cursorRecyclerViewAdapter.getSelectedContacts();
}
return selected;
public int getSelectedContactsCount() {
return cursorRecyclerViewAdapter.getSelectedContactsCount();
}
private boolean isMulti() {
return getActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
return requireActivity().getIntent().getBooleanExtra(MULTI_SELECT, false);
}
private void initializeCursor() {
glideRequests = GlideApp.with(this);
cursorRecyclerViewAdapter = new ContactSelectionListAdapter(requireContext(),
GlideApp.with(this),
glideRequests,
null,
new ListClickListener(),
isMulti());
selectedContacts = cursorRecyclerViewAdapter.getSelectedContacts();
RecyclerViewConcatenateAdapterStickyHeader concatenateAdapter = new RecyclerViewConcatenateAdapterStickyHeader();
if (listCallback != null && FeatureFlags.newGroupUI()) {
headerAdapter = new FixedViewsAdapter(createNewGroupItem(listCallback));
headerAdapter.hide();
concatenateAdapter.addAdapter(headerAdapter);
}
concatenateAdapter.addAdapter(cursorRecyclerViewAdapter);
if (inviteCallback != null) {
footerAdapter = new FixedViewsAdapter(createInviteActionView(inviteCallback));
if (listCallback != null) {
footerAdapter = new FixedViewsAdapter(createInviteActionView(listCallback));
footerAdapter.hide();
concatenateAdapter.addAdapter(footerAdapter);
}
@@ -203,10 +228,17 @@ public final class ContactSelectionListFragment extends Fragment
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true));
}
private View createInviteActionView(@NonNull InviteCallback inviteCallback) {
private View createInviteActionView(@NonNull ListCallback listCallback) {
View view = LayoutInflater.from(requireContext())
.inflate(R.layout.contact_selection_invite_action_item, (ViewGroup) requireView(), false);
view.setOnClickListener(v -> inviteCallback.onInvite());
view.setOnClickListener(v -> listCallback.onInvite());
return view;
}
private View createNewGroupItem(@NonNull ListCallback listCallback) {
View view = LayoutInflater.from(requireContext())
.inflate(R.layout.contact_selection_new_group_item, (ViewGroup) requireView(), false);
view.setOnClickListener(v -> listCallback.onNewGroup());
return view;
}
@@ -247,7 +279,7 @@ public final class ContactSelectionListFragment extends Fragment
}
public void reset() {
selectedContacts.clear();
cursorRecyclerViewAdapter.clearSelectedContacts();
if (!isDetached() && !isRemoving() && getActivity() != null && !getActivity().isFinishing()) {
getLoaderManager().restartLoader(0, null, this);
@@ -272,6 +304,10 @@ public final class ContactSelectionListFragment extends Fragment
footerAdapter.show();
}
if (headerAdapter != null) {
headerAdapter.show();
}
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
boolean useFastScroller = data != null && data.getCount() > 20;
recyclerView.setVerticalScrollBarEnabled(!useFastScroller);
@@ -336,7 +372,7 @@ public final class ContactSelectionListFragment extends Fragment
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
if (!isMulti() || !selectedContacts.contains(selectedContact)) {
if (!isMulti() || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (contact.isUsernameType()) {
AlertDialog loadingDialog = SimpleProgressDialog.show(requireContext());
@@ -346,8 +382,8 @@ public final class ContactSelectionListFragment extends Fragment
loadingDialog.dismiss();
if (uuid.isPresent()) {
Recipient recipient = Recipient.externalUsername(requireContext(), uuid.get(), contact.getNumber());
selectedContacts.add(SelectedContact.forUsername(recipient.getId(), contact.getNumber()));
contact.setChecked(true);
SelectedContact selected = SelectedContact.forUsername(recipient.getId(), contact.getNumber());
markContactSelected(selected, contact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactSelected(Optional.of(recipient.getId()), null);
@@ -361,24 +397,66 @@ public final class ContactSelectionListFragment extends Fragment
}
});
} else {
selectedContacts.add(selectedContact);
contact.setChecked(true);
markContactSelected(selectedContact, contact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactSelected(contact.getRecipientId(), contact.getNumber());
}
}
} else {
selectedContacts.remove(selectedContact);
contact.setChecked(false);
markContactUnselected(selectedContact, contact);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
}
}}
}
private void markContactSelected(@NonNull SelectedContact selectedContact, @NonNull ContactSelectionListItem listItem) {
cursorRecyclerViewAdapter.addSelectedContact(selectedContact);
listItem.setChecked(true);
if (isMulti() && FeatureFlags.newGroupUI()) {
chipGroup.addView(newChipForContact(listItem, selectedContact));
}
}
private void markContactUnselected(@NonNull SelectedContact selectedContact, @NonNull ContactSelectionListItem listItem) {
cursorRecyclerViewAdapter.removeFromSelectedContacts(selectedContact);
listItem.setChecked(false);
removeChipForContact(selectedContact);
}
private void removeChipForContact(@NonNull SelectedContact contact) {
for (int i = chipGroup.getChildCount() - 1; i >= 0; i--) {
View v = chipGroup.getChildAt(i);
if (v instanceof ContactChip && contact.matches(((ContactChip) v).getContact())) {
chipGroup.removeView(v);
}
}
}
private View newChipForContact(@NonNull ContactSelectionListItem contact, @NonNull SelectedContact selectedContact) {
final ContactChip chip = new ContactChip(requireContext());
chip.setText(contact.getChipName());
chip.setContact(selectedContact);
LiveRecipient recipient = contact.getRecipient();
if (recipient != null) {
recipient.observe(getViewLifecycleOwner(), resolved -> {
chip.setAvatar(glideRequests, resolved);
chip.setText(resolved.getShortDisplayName(chip.getContext()));
}
);
}
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(view -> {
markContactUnselected(selectedContact, contact);
chipGroup.removeView(chip);
});
return chip;
}
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
this.onContactSelectedListener = onContactSelectedListener;
}
@@ -387,12 +465,26 @@ public final class ContactSelectionListFragment extends Fragment
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
}
private void autoScrollOnNewItem() {
chipGroup.addOnLayoutChangeListener((view1, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
if (right > oldRight) {
scrollDebounce.publish(this::smoothScrollChipsToEnd);
}
});
}
private void smoothScrollChipsToEnd() {
int x = chipGroupScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? chipGroup.getWidth() : 0;
chipGroupScrollContainer.smoothScrollTo(x, 0);
}
public interface OnContactSelectedListener {
void onContactSelected(Optional<RecipientId> recipientId, String number);
void onContactDeselected(Optional<RecipientId> recipientId, String number);
}
public interface InviteCallback {
public interface ListCallback {
void onInvite();
void onNewGroup();
}
}

View File

@@ -1,101 +0,0 @@
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.ListFragment;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import org.thoughtcrime.securesms.database.loaders.CountryListLoader;
import java.util.ArrayList;
import java.util.Map;
public class CountrySelectionFragment extends ListFragment implements LoaderManager.LoaderCallbacks<ArrayList<Map<String, String>>> {
private EditText countryFilter;
private CountrySelectedListener listener;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
return inflater.inflate(R.layout.country_selection_fragment, container, false);
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
this.countryFilter = (EditText)getView().findViewById(R.id.country_search);
this.countryFilter.addTextChangedListener(new FilterWatcher());
getLoaderManager().initLoader(0, null, this).forceLoad();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.listener = (CountrySelectedListener)activity;
}
@Override
public void onListItemClick(ListView listView, View view, int position, long id) {
Map<String, String> item = (Map<String, String>)this.getListAdapter().getItem(position);
if (this.listener != null) {
this.listener.countrySelected(item.get("country_name"),
Integer.parseInt(item.get("country_code").substring(1)));
}
}
@Override
public @NonNull Loader<ArrayList<Map<String, String>>> onCreateLoader(int arg0, Bundle arg1) {
return new CountryListLoader(getActivity());
}
@Override
public void onLoadFinished(@NonNull Loader<ArrayList<Map<String, String>>> loader,
ArrayList<Map<String, String>> results)
{
String[] from = {"country_name", "country_code"};
int[] to = {R.id.country_name, R.id.country_code};
this.setListAdapter(new SimpleAdapter(getActivity(), results, R.layout.country_list_item, from, to));
if (this.countryFilter != null && this.countryFilter.getText().length() != 0) {
((SimpleAdapter)getListAdapter()).getFilter().filter(this.countryFilter.getText().toString());
}
}
@Override
public void onLoaderReset(@NonNull Loader<ArrayList<Map<String, String>>> arg0) {
this.setListAdapter(null);
}
public interface CountrySelectedListener {
public void countrySelected(String countryName, int countryCode);
}
private class FilterWatcher implements TextWatcher {
@Override
public void afterTextChanged(Editable s) {
if (getListAdapter() != null) {
((SimpleAdapter)getListAdapter()).getFilter().filter(s.toString());
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
}
}

View File

@@ -1,322 +0,0 @@
package org.thoughtcrime.securesms;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.viewpager.widget.ViewPager;
import com.melnykov.fab.FloatingActionButton;
import org.thoughtcrime.securesms.IntroPagerAdapter.IntroPage;
import org.thoughtcrime.securesms.experienceupgrades.StickersIntroFragment;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
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.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collections;
import java.util.List;
public class ExperienceUpgradeActivity extends BaseActionBarActivity
implements TypingIndicatorIntroFragment.Controller,
LinkPreviewsIntroFragment.Controller,
StickersIntroFragment.Controller
{
private static final String TAG = ExperienceUpgradeActivity.class.getSimpleName();
private static final String DISMISS_ACTION = "org.thoughtcrime.securesms.ExperienceUpgradeActivity.DISMISS_ACTION";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private enum ExperienceUpgrade {
SIGNAL_REBRANDING(157,
new IntroPage(0xFF2090EA,
BasicIntroFragment.newInstance(R.drawable.splash_logo,
R.string.ExperienceUpgradeActivity_welcome_to_signal_dgaf,
R.string.ExperienceUpgradeActivity_textsecure_is_now_called_signal)),
R.string.ExperienceUpgradeActivity_welcome_to_signal_excited,
R.string.ExperienceUpgradeActivity_textsecure_is_now_signal,
R.string.ExperienceUpgradeActivity_textsecure_is_now_signal_long,
null,
false),
VIDEO_CALLS(245,
new IntroPage(0xFF2090EA,
BasicIntroFragment.newInstance(R.drawable.video_splash,
R.string.ExperienceUpgradeActivity_say_hello_to_video_calls,
R.string.ExperienceUpgradeActivity_signal_now_supports_secure_video_calls)),
R.string.ExperienceUpgradeActivity_say_hello_to_video_calls,
R.string.ExperienceUpgradeActivity_signal_now_supports_secure_video_calling,
R.string.ExperienceUpgradeActivity_signal_now_supports_secure_video_calling_long,
null,
false),
PROFILES(286,
new IntroPage(0xFF2090EA,
BasicIntroFragment.newInstance(R.drawable.profile_splash,
R.string.ExperienceUpgradeActivity_ready_for_your_closeup,
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal)),
R.string.ExperienceUpgradeActivity_signal_profiles_are_here,
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
R.string.ExperienceUpgradeActivity_now_you_can_share_a_profile_photo_and_name_with_friends_on_signal,
EditProfileActivity.class,
false),
READ_RECEIPTS(299,
new IntroPage(0xFF2090EA,
ReadReceiptsIntroFragment.newInstance()),
R.string.experience_upgrade_preference_fragment__read_receipts_are_here,
R.string.experience_upgrade_preference_fragment__optionally_see_and_share_when_messages_have_been_read,
R.string.experience_upgrade_preference_fragment__optionally_see_and_share_when_messages_have_been_read,
null,
false),
TYPING_INDICATORS(432,
new IntroPage(0xFF2090EA,
TypingIndicatorIntroFragment.newInstance()),
R.string.ExperienceUpgradeActivity_introducing_typing_indicators,
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
R.string.ExperienceUpgradeActivity_now_you_can_optionally_see_and_share_when_messages_are_being_typed,
null,
true),
LINK_PREVIEWS(449,
new IntroPage(0xFF2090EA, LinkPreviewsIntroFragment.newInstance()),
R.string.ExperienceUpgradeActivity_introducing_link_previews,
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
R.string.ExperienceUpgradeActivity_optional_link_previews_are_now_supported,
null,
true),
STICKERS(580,
new IntroPage(0xFF2090EA, StickersIntroFragment.newInstance()),
R.string.ExperienceUpgradeActivity_introducing_stickers,
R.string.ExperienceUpgradeActivity_why_use_words_when_you_can_use_stickers,
R.string.ExperienceUpgradeActivity_why_use_words_when_you_can_use_stickers,
null,
true);
private int version;
private List<IntroPage> pages;
private @StringRes int notificationTitle;
private @StringRes int notificationText;
private @StringRes int notificationBigText;
private @Nullable Class nextIntent;
private boolean handlesNavigation;
ExperienceUpgrade(int version,
@NonNull List<IntroPage> pages,
@StringRes int notificationTitle,
@StringRes int notificationText,
@StringRes int notificationBigText,
@Nullable Class nextIntent,
boolean handlesNavigation)
{
this.version = version;
this.pages = pages;
this.notificationTitle = notificationTitle;
this.notificationText = notificationText;
this.notificationBigText = notificationBigText;
this.nextIntent = nextIntent;
this.handlesNavigation = handlesNavigation;
}
ExperienceUpgrade(int version,
@NonNull IntroPage page,
@StringRes int notificationTitle,
@StringRes int notificationText,
@StringRes int notificationBigText,
@Nullable Class nextIntent,
boolean handlesNavigation)
{
this(version, Collections.singletonList(page), notificationTitle, notificationText, notificationBigText, nextIntent, handlesNavigation);
}
public int getVersion() {
return version;
}
public List<IntroPage> getPages() {
return pages;
}
public IntroPage getPage(int i) {
return pages.get(i);
}
public int getNotificationTitle() {
return notificationTitle;
}
public int getNotificationText() {
return notificationText;
}
public int getNotificationBigText() {
return notificationBigText;
}
public boolean handlesNavigation() {
return handlesNavigation;
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
dynamicTheme.onCreate(this);
final Optional<ExperienceUpgrade> upgrade = getExperienceUpgrade(this);
if (!upgrade.isPresent()) {
onContinue(upgrade);
return;
}
setContentView(R.layout.experience_upgrade_activity);
final ViewPager pager = ViewUtil.findById(this, R.id.pager);
final FloatingActionButton fab = ViewUtil.findById(this, R.id.fab);
pager.setAdapter(new IntroPagerAdapter(getSupportFragmentManager(), upgrade.get().getPages()));
if (upgrade.get().handlesNavigation()) {
fab.setVisibility(View.GONE);
} else {
fab.setVisibility(View.VISIBLE);
fab.setOnClickListener(v -> onContinue(upgrade));
}
getWindow().setBackgroundDrawable(new ColorDrawable(upgrade.get().getPage(0).backgroundColor));
ServiceUtil.getNotificationManager(this).cancel(NotificationIds.EXPERIENCE_UPGRADE);
}
@Override
protected void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
private void onContinue(Optional<ExperienceUpgrade> seenUpgrade) {
ServiceUtil.getNotificationManager(this).cancel(NotificationIds.EXPERIENCE_UPGRADE);
int latestVersion = seenUpgrade.isPresent() ? seenUpgrade.get().getVersion()
: Util.getCanonicalVersionCode();
TextSecurePreferences.setLastExperienceVersionCode(this, latestVersion);
if (seenUpgrade.isPresent() && seenUpgrade.get().nextIntent != null) {
Intent intent = new Intent(this, seenUpgrade.get().nextIntent);
// TODO [greyson] Navigation
Intent nextIntent = new Intent(this, MainActivity.class);
intent.putExtra("next_intent", nextIntent);
startActivity(intent);
} else {
startActivity(getIntent().getParcelableExtra("next_intent"));
}
finish();
}
public static boolean isUpdate(Context context) {
return getExperienceUpgrade(context).isPresent();
}
public static Optional<ExperienceUpgrade> getExperienceUpgrade(Context context) {
final int currentVersionCode = Util.getCanonicalVersionCode();
final int lastSeenVersion = TextSecurePreferences.getLastExperienceVersionCode(context);
Log.i(TAG, "getExperienceUpgrade(" + lastSeenVersion + ")");
if (lastSeenVersion >= currentVersionCode) {
TextSecurePreferences.setLastExperienceVersionCode(context, currentVersionCode);
return Optional.absent();
}
Optional<ExperienceUpgrade> eligibleUpgrade = Optional.absent();
for (ExperienceUpgrade upgrade : ExperienceUpgrade.values()) {
if (lastSeenVersion < upgrade.getVersion()) eligibleUpgrade = Optional.of(upgrade);
}
return eligibleUpgrade;
}
@Override
public void onTypingIndicatorsFinished() {
onContinue(Optional.of(ExperienceUpgrade.TYPING_INDICATORS));
}
@Override
public void onLinkPreviewsFinished() {
onContinue(Optional.of(ExperienceUpgrade.LINK_PREVIEWS));
}
@Override
public void onStickersFinished() {
onContinue(Optional.of(ExperienceUpgrade.STICKERS));
}
public static class AppUpgradeReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if (Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction()) &&
intent.getData().getSchemeSpecificPart().equals(context.getPackageName()))
{
if (TextSecurePreferences.getLastExperienceVersionCode(context) < 339 &&
!TextSecurePreferences.isPasswordDisabled(context))
{
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.OTHER)
.setSmallIcon(R.drawable.icon_notification)
.setColor(context.getResources().getColor(R.color.signal_primary))
.setContentTitle(context.getString(R.string.ExperienceUpgradeActivity_unlock_to_complete_update))
.setContentText(context.getString(R.string.ExperienceUpgradeActivity_please_unlock_signal_to_complete_update))
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.ExperienceUpgradeActivity_please_unlock_signal_to_complete_update)))
.setAutoCancel(true)
.setContentIntent(PendingIntent.getActivity(context, 0,
context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()),
PendingIntent.FLAG_UPDATE_CURRENT))
.build();
ServiceUtil.getNotificationManager(context).notify(NotificationIds.EXPERIENCE_UPGRADE, notification);
}
Optional<ExperienceUpgrade> experienceUpgrade = getExperienceUpgrade(context);
if (!experienceUpgrade.isPresent()) {
return;
}
if (experienceUpgrade.get().getVersion() == TextSecurePreferences.getExperienceDismissedVersionCode(context)) {
return;
}
Intent targetIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName());
Intent dismissIntent = new Intent(context, AppUpgradeReceiver.class);
dismissIntent.setAction(DISMISS_ACTION);
Notification notification = new NotificationCompat.Builder(context, NotificationChannels.OTHER)
.setSmallIcon(R.drawable.icon_notification)
.setColor(context.getResources().getColor(R.color.signal_primary))
.setContentTitle(context.getString(experienceUpgrade.get().getNotificationTitle()))
.setContentText(context.getString(experienceUpgrade.get().getNotificationText()))
.setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(experienceUpgrade.get().getNotificationBigText())))
.setAutoCancel(true)
.setContentIntent(PendingIntent.getActivity(context, 0,
targetIntent,
PendingIntent.FLAG_UPDATE_CURRENT))
.setDeleteIntent(PendingIntent.getBroadcast(context, 0,
dismissIntent,
PendingIntent.FLAG_UPDATE_CURRENT))
.build();
ServiceUtil.getNotificationManager(context).notify(NotificationIds.EXPERIENCE_UPGRADE, notification);
} else if (DISMISS_ACTION.equals(intent.getAction())) {
TextSecurePreferences.setExperienceDismissedVersionCode(context, Util.getCanonicalVersionCode());
}
}
}
}

View File

@@ -18,20 +18,14 @@
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import android.text.TextUtils;
import org.thoughtcrime.securesms.avatar.AvatarSelection;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.logging.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -42,6 +36,10 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
@@ -52,28 +50,35 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
import org.thoughtcrime.securesms.contacts.RecipientsEditor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupManager.GroupActionResult;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter;
import org.thoughtcrime.securesms.util.SelectedRecipientsAdapter.OnRecipientDeletedListener;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
@@ -81,7 +86,7 @@ import java.util.List;
import java.util.Set;
/**
* Activity to create and update groups
* Activity to create and update {@link GroupId.V1} groups
*
* @author Jake McGinty
*/
@@ -92,27 +97,31 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
private final static String TAG = GroupCreateActivity.class.getSimpleName();
public static final String GROUP_ID_EXTRA = "group_id";
public static final String GROUP_THREAD_EXTRA = "group_thread";
private static final String GROUP_ID_EXTRA = "group_id";
private static final String GROUP_THREAD_EXTRA = "group_thread";
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private final DynamicTheme dynamicTheme = new DynamicTheme();
private static final int PICK_CONTACT = 1;
public static final int AVATAR_SIZE = 210;
private static final short REQUEST_CODE_SELECT_AVATAR = 26165;
private static final int PICK_CONTACT = 1;
private EditText groupName;
private ListView lv;
private ImageView avatar;
private TextView creatingText;
private Bitmap avatarBmp;
private EditText groupName;
private ListView listView;
private ImageView avatar;
private TextView creatingText;
private Bitmap avatarBmp;
@NonNull private Optional<GroupData> groupToUpdate = Optional.absent();
public static Intent newEditGroupIntent(@NonNull Context context, @NonNull GroupId.V1 groupId) {
Intent intent = new Intent(context, GroupCreateActivity.class);
intent.putExtra(GroupCreateActivity.GROUP_ID_EXTRA, groupId.toString());
return intent;
}
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
@@ -128,7 +137,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
updateViewState();
}
@@ -185,27 +193,42 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
private void initializeResources() {
RecipientsEditor recipientsEditor = ViewUtil.findById(this, R.id.recipients_text);
PushRecipientsPanel recipientsPanel = ViewUtil.findById(this, R.id.recipients);
lv = ViewUtil.findById(this, R.id.selected_contacts_list);
avatar = ViewUtil.findById(this, R.id.avatar);
groupName = ViewUtil.findById(this, R.id.group_name);
creatingText = ViewUtil.findById(this, R.id.creating_group_text);
RecipientsEditor recipientsEditor = findViewById(R.id.recipients_text);
PushRecipientsPanel recipientsPanel = findViewById(R.id.recipients);
listView = findViewById(R.id.selected_contacts_list);
avatar = findViewById(R.id.avatar);
groupName = findViewById(R.id.group_name);
creatingText = findViewById(R.id.creating_group_text);
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(this);
adapter.setOnRecipientDeletedListener(this);
lv.setAdapter(adapter);
listView.setAdapter(adapter);
recipientsEditor.setHint(R.string.recipients_panel__add_members);
recipientsPanel.setPanelChangeListener(this);
findViewById(R.id.contacts_button).setOnClickListener(new AddRecipientButtonListener());
avatar.setImageDrawable(new ResourceContactPhoto(R.drawable.ic_group_outline_40, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this)));
avatar.setOnClickListener(view -> AvatarSelection.startAvatarSelection(this, false, false));
avatar.setImageDrawable(getDefaultGroupAvatar());
avatar.setOnClickListener(view -> AvatarSelectionBottomSheetDialogFragment.create(avatarBmp != null, false, REQUEST_CODE_SELECT_AVATAR, true).show(getSupportFragmentManager(), null));
}
private Drawable getDefaultGroupAvatar() {
return new ResourceContactPhoto(R.drawable.ic_group_outline_34, R.drawable.ic_group_outline_20).asDrawable(this, ContactColors.UNKNOWN_COLOR.toConversationColor(this));
}
private void initializeExistingGroup() {
final String groupId = getIntent().getStringExtra(GROUP_ID_EXTRA);
final GroupId groupId = GroupId.parseNullableOrThrow(getIntent().getStringExtra(GROUP_ID_EXTRA));
if (groupId != null) {
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupId);
GroupId.V1 groupIdV1 = groupId.requireV1();
new FillExistingGroupInfoAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, groupIdV1);
if (FeatureFlags.newGroupUI()) {
avatar.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(this, groupIdV1)));
}
}
}
@@ -260,8 +283,8 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
private void handleGroupUpdate() {
new UpdateSignalGroupTask(this, groupToUpdate.get().id, avatarBmp,
getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
new UpdateSignalGroupV1Task(this, groupToUpdate.get().id, avatarBmp,
getGroupName(), getAdapter().getRecipients()).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
private void handleOpenConversation(long threadId, Recipient recipient) {
@@ -274,7 +297,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
private SelectedRecipientsAdapter getAdapter() {
return (SelectedRecipientsAdapter)lv.getAdapter();
return (SelectedRecipientsAdapter) listView.getAdapter();
}
private @Nullable String getGroupName() {
@@ -284,7 +307,6 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
@Override
public void onActivityResult(int reqCode, int resultCode, final Intent data) {
super.onActivityResult(reqCode, resultCode, data);
Uri outputFile = Uri.fromFile(new File(getCacheDir(), "cropped"));
if (data == null || resultCode != Activity.RESULT_OK)
return;
@@ -299,23 +321,27 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
break;
case REQUEST_CODE_SELECT_AVATAR:
if (data.getBooleanExtra("delete", false)) {
avatarBmp = null;
avatar.setImageDrawable(getDefaultGroupAvatar());
return;
}
final Media result = data.getParcelableExtra(AvatarSelectionActivity.EXTRA_MEDIA);
final DecryptableUri decryptableUri = new DecryptableUri(result.getUri());
case AvatarSelection.REQUEST_CODE_AVATAR:
AvatarSelection.circularCropImage(this, data.getData(), outputFile, R.string.CropImageActivity_group_avatar);
break;
case AvatarSelection.REQUEST_CODE_CROP_IMAGE:
final Uri resultUri = AvatarSelection.getResultUri(data);
GlideApp.with(this)
.asBitmap()
.load(resultUri)
.load(decryptableUri)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.centerCrop()
.override(AVATAR_SIZE, AVATAR_SIZE)
.override(AvatarHelper.AVATAR_DIMENSIONS, AvatarHelper.AVATAR_DIMENSIONS)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, Transition<? super Bitmap> transition) {
setAvatar(resultUri, resource);
setAvatar(decryptableUri, resource);
}
});
}
@@ -352,7 +378,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
memberAddresses.add(Recipient.self().getId());
String groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateGroupForMembers(memberAddresses, true);
GroupId.Mms groupId = DatabaseFactory.getGroupDatabase(activity).getOrCreateMmsGroupForMembers(memberAddresses);
RecipientId groupRecipientId = DatabaseFactory.getRecipientDatabase(activity).getOrInsertFromGroupId(groupId);
Recipient groupRecipient = Recipient.resolved(groupRecipientId);
long threadId = DatabaseFactory.getThreadDatabase(activity).getThreadIdFor(groupRecipient, ThreadDatabase.DistributionTypes.DEFAULT);
@@ -416,7 +442,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
@Override
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
return Optional.of(GroupManager.createGroup(activity, members, avatar, name, false));
return Optional.of(GroupManager.createGroupV1(activity, members, BitmapUtil.toByteArray(avatar), name, false));
}
@Override
@@ -433,11 +459,11 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
}
private static class UpdateSignalGroupTask extends SignalGroupTask {
private String groupId;
private static class UpdateSignalGroupV1Task extends SignalGroupTask {
private final GroupId.V1 groupId;
public UpdateSignalGroupTask(GroupCreateActivity activity, String groupId,
Bitmap avatar, String name, Set<Recipient> members)
UpdateSignalGroupV1Task(GroupCreateActivity activity, GroupId.V1 groupId,
Bitmap avatar, String name, Set<Recipient> members)
{
super(activity, avatar, name, members);
this.groupId = groupId;
@@ -445,11 +471,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
@Override
protected Optional<GroupActionResult> doInBackground(Void... aVoid) {
try {
return Optional.of(GroupManager.updateGroup(activity, groupId, members, avatar, name));
} catch (InvalidNumberException e) {
return Optional.absent();
}
return Optional.fromNullable(GroupManager.updateGroup(activity, groupId, members, BitmapUtil.toByteArray(avatar), name));
}
@Override
@@ -458,7 +480,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
if (!activity.isFinishing()) {
Intent intent = activity.getIntent();
intent.putExtra(GROUP_THREAD_EXTRA, result.get().getThreadId());
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId());
intent.putExtra(GROUP_ID_EXTRA, result.get().getGroupRecipient().requireGroupId().toString());
activity.setResult(RESULT_OK, intent);
activity.finish();
}
@@ -525,7 +547,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
}
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<String,Void,Optional<GroupData>> {
private static class FillExistingGroupInfoAsyncTask extends ProgressDialogAsyncTask<GroupId.V1, Void, Optional<GroupData>> {
private GroupCreateActivity activity;
public FillExistingGroupInfoAsyncTask(GroupCreateActivity activity) {
@@ -536,18 +558,24 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
@Override
protected Optional<GroupData> doInBackground(String... groupIds) {
protected Optional<GroupData> doInBackground(GroupId.V1... groupIds) {
final GroupDatabase db = DatabaseFactory.getGroupDatabase(activity);
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], false);
final List<Recipient> recipients = db.getGroupMembers(groupIds[0], GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
final Optional<GroupRecord> group = db.getGroup(groupIds[0]);
final Set<Recipient> existingContacts = new HashSet<>(recipients.size());
existingContacts.addAll(recipients);
if (group.isPresent()) {
Bitmap avatar = null;
try {
avatar = BitmapFactory.decodeStream(AvatarHelper.getAvatar(getContext(), group.get().getRecipientId()));
} catch (IOException e) {
Log.w(TAG, "Failed to read avatar.");
}
return Optional.of(new GroupData(groupIds[0],
existingContacts,
BitmapUtil.fromByteArray(group.get().getAvatar()),
group.get().getAvatar(),
avatar,
BitmapUtil.toByteArray(avatar),
group.get().getTitle()));
} else {
return Optional.absent();
@@ -567,7 +595,7 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
SelectedRecipientsAdapter adapter = new SelectedRecipientsAdapter(activity, group.get().recipients);
adapter.setOnRecipientDeletedListener(activity);
activity.lv.setAdapter(adapter);
activity.listView.setAdapter(adapter);
activity.updateViewState();
}
}
@@ -584,13 +612,13 @@ public class GroupCreateActivity extends PassphraseRequiredActionBarActivity
}
private static class GroupData {
String id;
GroupId.V1 id;
Set<Recipient> recipients;
Bitmap avatarBmp;
byte[] avatarBytes;
String name;
public GroupData(String id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
GroupData(GroupId.V1 id, Set<Recipient> recipients, Bitmap avatarBmp, byte[] avatarBytes, String name) {
this.id = id;
this.recipients = recipients;
this.avatarBmp = avatarBmp;

View File

@@ -1,131 +1,57 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import java.util.LinkedList;
import java.util.List;
public class GroupMembersDialog extends AsyncTask<Void, Void, List<Recipient>> {
public final class GroupMembersDialog {
private static final String TAG = GroupMembersDialog.class.getSimpleName();
private final FragmentActivity fragmentActivity;
private final Recipient groupRecipient;
private final Recipient recipient;
private final Context context;
public GroupMembersDialog(Context context, Recipient recipient) {
this.recipient = recipient;
this.context = context;
}
@Override
public void onPreExecute() {}
@Override
protected List<Recipient> doInBackground(Void... params) {
return DatabaseFactory.getGroupDatabase(context).getGroupMembers(recipient.requireGroupId(), true);
}
@Override
public void onPostExecute(List<Recipient> members) {
GroupMembers groupMembers = new GroupMembers(members);
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.ConversationActivity_group_members);
builder.setIconAttribute(R.attr.group_members_dialog_icon);
builder.setCancelable(true);
builder.setItems(groupMembers.getRecipientStrings(), new GroupMembersOnClickListener(context, groupMembers));
builder.setPositiveButton(android.R.string.ok, null);
builder.show();
public GroupMembersDialog(@NonNull FragmentActivity activity,
@NonNull Recipient groupRecipient)
{
this.fragmentActivity = activity;
this.groupRecipient = groupRecipient;
}
public void display() {
executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
AlertDialog dialog = new AlertDialog.Builder(fragmentActivity)
.setTitle(R.string.ConversationActivity_group_members)
.setIconAttribute(R.attr.group_members_dialog_icon)
.setCancelable(true)
.setView(R.layout.dialog_group_members)
.setPositiveButton(android.R.string.ok, null)
.show();
GroupMemberListView memberListView = dialog.findViewById(R.id.list_members);
LiveGroup liveGroup = new LiveGroup(groupRecipient.requireGroupId());
LiveData<List<GroupMemberEntry.FullMember>> fullMembers = liveGroup.getFullMembers();
//noinspection ConstantConditions
fullMembers.observe(fragmentActivity, memberListView::setMembers);
dialog.setOnDismissListener(d -> fullMembers.removeObservers(fragmentActivity));
memberListView.setRecipientClickListener(recipient -> {
dialog.dismiss();
contactClick(recipient);
});
}
private static class GroupMembersOnClickListener implements DialogInterface.OnClickListener {
private final GroupMembers groupMembers;
private final Context context;
public GroupMembersOnClickListener(Context context, GroupMembers members) {
this.context = context;
this.groupMembers = members;
}
@Override
public void onClick(DialogInterface dialogInterface, int item) {
Recipient recipient = groupMembers.get(item);
if (recipient.getContactUri() != null) {
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, recipient.getId());
context.startActivity(intent);
} else {
context.startActivity(RecipientExporter.export(recipient).asAddContactIntent());
}
}
}
/**
* Wraps a List of Recipient (just like @class Recipients),
* but with focus on the order of the Recipients.
* So that the order of the RecipientStrings[] matches
* the internal order.
*
* @author Christoph Haefner
*/
private class GroupMembers {
private final String TAG = GroupMembers.class.getSimpleName();
private final LinkedList<Recipient> members = new LinkedList<>();
public GroupMembers(List<Recipient> recipients) {
for (Recipient recipient : recipients) {
if (recipient.isLocalNumber()) {
members.push(recipient);
} else {
members.add(recipient);
}
}
}
public String[] getRecipientStrings() {
List<String> recipientStrings = new LinkedList<>();
for (Recipient recipient : members) {
if (recipient.isLocalNumber()) {
recipientStrings.add(context.getString(R.string.GroupMembersDialog_me));
} else {
String name = getRecipientName(recipient);
recipientStrings.add(name);
}
}
return recipientStrings.toArray(new String[members.size()]);
}
private String getRecipientName(Recipient recipient) {
if (FeatureFlags.profileDisplay()) return recipient.getDisplayName(context);
String name = recipient.toShortString(context);
if (recipient.getName(context) == null && !recipient.getProfileName().isEmpty()) {
name += " ~" + recipient.getProfileName().toString();
}
return name;
}
public Recipient get(int index) {
return members.get(index);
}
private void contactClick(@NonNull Recipient recipient) {
RecipientBottomSheetDialogFragment.create(recipient.getId(), groupRecipient.requireGroupId())
.show(fragmentActivity.getSupportFragmentManager(), "BOTTOM");
}
}

View File

@@ -1,38 +0,0 @@
package org.thoughtcrime.securesms;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
import java.util.List;
public class IntroPagerAdapter extends FragmentStatePagerAdapter {
public static class IntroPage {
final int backgroundColor;
final Fragment fragment;
public IntroPage(int backgroundColor, Fragment fragment) {
this.backgroundColor = backgroundColor;
this.fragment = fragment;
}
}
private List<IntroPage> pages;
public IntroPagerAdapter(FragmentManager fm, List<IntroPage> pages) {
super(fm);
this.pages = pages;
}
@Override
public Fragment getItem(int i) {
IntroPage page = pages.get(i);
return page.fragment;
}
@Override
public int getCount() {
return pages.size();
}
}

View File

@@ -22,8 +22,6 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.components.ContactFilterToolbar;
import org.thoughtcrime.securesms.components.ContactFilterToolbar.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
@@ -42,6 +40,7 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
import java.util.concurrent.ExecutionException;
public class InviteActivity extends PassphraseRequiredActionBarActivity implements ContactSelectionListFragment.OnContactSelectedListener {
@@ -135,14 +134,15 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
contactsFragment.getSelectedContacts()
.toArray(new SelectedContact[contactsFragment.getSelectedContacts().size()]));
.toArray(new SelectedContact[0]));
}
private void updateSmsButtonText() {
List<SelectedContact> selectedContacts = contactsFragment.getSelectedContacts();
smsSendButton.setText(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_to_friends,
contactsFragment.getSelectedContacts().size(),
contactsFragment.getSelectedContacts().size()));
smsSendButton.setEnabled(!contactsFragment.getSelectedContacts().isEmpty());
selectedContacts.size(),
selectedContacts.size()));
smsSendButton.setEnabled(!selectedContacts.isEmpty());
}
@Override public void onBackPressed() {
@@ -175,17 +175,17 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
}
private void setPrimaryColorsToolbarForSms() {
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.signal_primary));
primaryToolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.core_ultramarine));
primaryToolbar.getNavigationIcon().setColorFilter(ThemeUtil.getThemedColor(this, R.attr.conversation_subtitle_color), PorterDuff.Mode.SRC_IN);
primaryToolbar.setTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
if (Build.VERSION.SDK_INT >= 23) {
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.signal_primary));
getWindow().setStatusBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
WindowUtil.clearLightStatusBar(getWindow());
}
if (Build.VERSION.SDK_INT >= 27) {
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.signal_primary));
getWindow().setNavigationBarColor(ContextCompat.getColor(this, R.color.core_ultramarine));
WindowUtil.clearLightNavigationBar(getWindow());
}
}

View File

@@ -1,63 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class LinkPreviewsIntroFragment extends Fragment {
private Controller controller;
public static LinkPreviewsIntroFragment newInstance() {
LinkPreviewsIntroFragment fragment = new LinkPreviewsIntroFragment();
Bundle args = new Bundle();
fragment.setArguments(args);
return fragment;
}
public LinkPreviewsIntroFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (!(getActivity() instanceof Controller)) {
throw new IllegalStateException("Parent activity must implement the Controller interface.");
}
controller = (Controller) getActivity();
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.experience_upgrade_link_previews_fragment, container, false);
view.findViewById(R.id.experience_ok_button).setOnClickListener(v -> {
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(requireContext()),
TextSecurePreferences.isLinkPreviewsEnabled(requireContext())));
controller.onLinkPreviewsFinished();
});
return view;
}
public interface Controller {
void onLinkPreviewsFinished();
}
}

View File

@@ -1,78 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.FragmentTransaction;
import org.thoughtcrime.securesms.logging.Log;
import android.view.MenuItem;
import android.widget.Toast;
import org.thoughtcrime.securesms.logsubmit.SubmitLogFragment;
import org.thoughtcrime.securesms.util.DynamicTheme;
/**
* Activity for submitting logcat logs to a pastebin service.
*/
public class LogSubmitActivity extends BaseActionBarActivity implements SubmitLogFragment.OnLogSubmittedListener {
private static final String TAG = LogSubmitActivity.class.getSimpleName();
private DynamicTheme dynamicTheme = new DynamicTheme();
@Override
protected void onCreate(Bundle icicle) {
dynamicTheme.onCreate(this);
super.onCreate(icicle);
setContentView(R.layout.log_submit_activity);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
SubmitLogFragment fragment = SubmitLogFragment.newInstance();
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(R.id.fragment_container, fragment);
transaction.commit();
}
@Override
protected void onResume() {
dynamicTheme.onResume(this);
super.onResume();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
}
return false;
}
@Override
public void onSuccess() {
Toast.makeText(getApplicationContext(), R.string.log_submit_activity__thanks, Toast.LENGTH_LONG).show();
finish();
}
@Override
public void onFailure() {
Toast.makeText(getApplicationContext(), R.string.log_submit_activity__log_fetch_failed, Toast.LENGTH_LONG).show();
finish();
}
@Override
public void onCancel() {
finish();
}
@Override
public void startActivity(Intent intent) {
try {
super.startActivity(intent);
} catch (ActivityNotFoundException e) {
Log.w(TAG, e);
Toast.makeText(this, R.string.log_submit_activity__no_browser_installed, Toast.LENGTH_LONG).show();
}
}
}

View File

@@ -55,8 +55,8 @@ public class MainNavigator {
return false;
}
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, long lastSeen, int startingPosition) {
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, lastSeen, startingPosition);
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition) {
Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, startingPosition);
activity.startActivity(intent);
activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);

View File

@@ -64,6 +64,7 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sharing.ShareActivity;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
@@ -116,6 +117,20 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
private boolean showThread;
private MediaDatabase.Sorting sorting;
public static @NonNull Intent intentFromMediaRecord(@NonNull Context context,
@NonNull MediaRecord mediaRecord,
boolean leftIsRecent)
{
Intent intent = new Intent(context, MediaPreviewActivity.class);
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId());
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, mediaRecord.getAttachment().getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
return intent;
}
@SuppressWarnings("ConstantConditions")
@Override
protected void onCreate(Bundle bundle, boolean ready) {

View File

@@ -31,6 +31,7 @@ import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.Loader;
import org.thoughtcrime.securesms.conversation.ConversationItem;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.logging.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -61,6 +62,7 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.whispersystems.libsignal.util.guava.Optional;
import java.lang.ref.WeakReference;
@@ -368,7 +370,7 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
List<GroupReceiptInfo> receiptInfoList = DatabaseFactory.getGroupReceiptDatabase(context).getGroupReceiptInfo(messageRecord.getId());
if (receiptInfoList.isEmpty()) {
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), false);
List<Recipient> group = DatabaseFactory.getGroupDatabase(context).getGroupMembers(messageRecord.getRecipient().requireGroupId(), GroupDatabase.MemberSet.FULL_MEMBERS_EXCLUDING_SELF);
for (Recipient recipient : group) {
recipients.add(new RecipientDeliveryStatus(recipient, RecipientDeliveryStatus.Status.UNKNOWN, false, -1));
@@ -438,8 +440,8 @@ public class MessageDetailsActivity extends PassphraseRequiredActionBarActivity
}
private void onResendClicked(View v) {
MessageSender.resend(MessageDetailsActivity.this, messageRecord);
resendButton.setVisibility(View.GONE);
SignalExecutors.BOUNDED.execute(() -> MessageSender.resend(MessageDetailsActivity.this, messageRecord));
}
}
}

View File

@@ -1,8 +1,10 @@
package org.thoughtcrime.securesms;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import java.util.concurrent.TimeUnit;
@@ -23,6 +25,10 @@ public class MuteDialog extends AlertDialog {
}
public static void show(final Context context, final @NonNull MuteSelectionListener listener) {
show(context, listener, null);
}
public static void show(final Context context, final @NonNull MuteSelectionListener listener, @Nullable Runnable cancelListener) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.MuteDialog_mute_notifications);
builder.setItems(R.array.mute_durations, new DialogInterface.OnClickListener() {
@@ -43,6 +49,13 @@ public class MuteDialog extends AlertDialog {
}
});
if (cancelListener != null) {
builder.setOnCancelListener(dialog -> {
cancelListener.run();
dialog.dismiss();
});
}
builder.show();
}

View File

@@ -22,29 +22,15 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.Toast;
import org.thoughtcrime.securesms.conversation.ConversationActivity;
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.libsignal.util.guava.Optional;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.io.IOException;
import java.util.UUID;
/**
* Activity container for starting a new conversation.
@@ -53,7 +39,7 @@ import java.util.UUID;
*
*/
public class NewConversationActivity extends ContactSelectionActivity
implements ContactSelectionListFragment.InviteCallback
implements ContactSelectionListFragment.ListCallback
{
@SuppressWarnings("unused")
@@ -112,7 +98,7 @@ public class NewConversationActivity extends ContactSelectionActivity
}
private void handleCreateGroup() {
startActivity(new Intent(this, GroupCreateActivity.class));
startActivity(CreateGroupActivity.newIntent(this));
}
private void handleInvite() {
@@ -120,10 +106,10 @@ public class NewConversationActivity extends ContactSelectionActivity
}
@Override
protected boolean onPrepareOptionsPanel(View view, Menu menu) {
MenuInflater inflater = this.getMenuInflater();
public boolean onPrepareOptionsMenu(Menu menu) {
menu.clear();
inflater.inflate(R.menu.new_conversation_activity, menu);
getMenuInflater().inflate(R.menu.new_conversation_activity, menu);
super.onPrepareOptionsMenu(menu);
return true;
}
@@ -131,5 +117,12 @@ public class NewConversationActivity extends ContactSelectionActivity
@Override
public void onInvite() {
handleInvite();
finish();
}
@Override
public void onNewGroup() {
handleCreateGroup();
finish();
}
}

View File

@@ -41,6 +41,8 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class PassphraseChangeActivity extends PassphraseActivity {
private static final String TAG = Log.tag(PassphraseChangeActivity.class);
private DynamicTheme dynamicTheme = new DynamicTheme();
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
@@ -145,7 +147,7 @@ public class PassphraseChangeActivity extends PassphraseActivity {
return masterSecret;
} catch (InvalidPassphraseException e) {
Log.w(PassphraseChangeActivity.class.getSimpleName(), e);
Log.w(TAG, e);
return null;
}
}

View File

@@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
import org.thoughtcrime.securesms.util.DynamicIntroTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -164,7 +165,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
}
private void handleLogSubmit() {
Intent intent = new Intent(this, LogSubmitActivity.class);
Intent intent = new Intent(this, SubmitDebugLogActivity.class);
startActivity(intent);
}
@@ -237,7 +238,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
EditorInfo.IME_ACTION_DONE);
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
lockScreenButton.setOnClickListener(v -> resumeScreenLock());
}
@@ -357,7 +358,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
handleAuthenticated();
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
}
}).start();
}
@@ -380,7 +381,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
@Override
public void onAnimationEnd(Animation animation) {
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_primary), PorterDuff.Mode.SRC_IN);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
}
@Override

View File

@@ -14,10 +14,15 @@ import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -27,14 +32,17 @@ import java.util.Locale;
public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarActivity implements MasterSecretListener {
private static final String TAG = PassphraseRequiredActionBarActivity.class.getSimpleName();
public static final String LOCALE_EXTRA = "locale_extra";
public static final String LOCALE_EXTRA = "locale_extra";
public static final String NEXT_INTENT_EXTRA = "next_intent";
private static final int STATE_NORMAL = 0;
private static final int STATE_CREATE_PASSPHRASE = 1;
private static final int STATE_PROMPT_PASSPHRASE = 2;
private static final int STATE_UI_BLOCKING_UPGRADE = 3;
private static final int STATE_EXPERIENCE_UPGRADE = 4;
private static final int STATE_WELCOME_PUSH_SCREEN = 5;
private static final int STATE_WELCOME_PUSH_SCREEN = 4;
private static final int STATE_ENTER_SIGNAL_PIN = 5;
private static final int STATE_CREATE_PROFILE_NAME = 6;
private static final int STATE_CREATE_SIGNAL_PIN = 7;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -149,7 +157,9 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
case STATE_PROMPT_PASSPHRASE: return getPromptPassphraseIntent();
case STATE_UI_BLOCKING_UPGRADE: return getUiBlockingUpgradeIntent();
case STATE_WELCOME_PUSH_SCREEN: return getPushRegistrationIntent();
case STATE_EXPERIENCE_UPGRADE: return getExperienceUpgradeIntent();
case STATE_ENTER_SIGNAL_PIN: return getEnterSignalPinIntent();
case STATE_CREATE_SIGNAL_PIN: return getCreateSignalPinIntent();
case STATE_CREATE_PROFILE_NAME: return getCreateProfileNameIntent();
default: return null;
}
}
@@ -163,13 +173,25 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
return STATE_WELCOME_PUSH_SCREEN;
} else if (ExperienceUpgradeActivity.isUpdate(this)) {
return STATE_EXPERIENCE_UPGRADE;
} else if (SignalStore.storageServiceValues().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;
} else if (userMustCreateSignalPin()) {
return STATE_CREATE_SIGNAL_PIN;
} else {
return STATE_NORMAL;
}
}
private boolean userMustCreateSignalPin() {
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.kbsValues().hasPin() && !SignalStore.kbsValues().lastPinCreateFailed();
}
private boolean userMustSetProfileName() {
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
}
private Intent getCreatePassphraseIntent() {
return getRoutedIntent(PassphraseCreateActivity.class, getIntent());
}
@@ -185,14 +207,30 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
: getPushRegistrationIntent());
}
private Intent getExperienceUpgradeIntent() {
return getRoutedIntent(ExperienceUpgradeActivity.class, getIntent());
}
private Intent getPushRegistrationIntent() {
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
}
private Intent getEnterSignalPinIntent() {
return getRoutedIntent(PinRestoreActivity.class, getIntent());
}
private Intent getCreateSignalPinIntent() {
final Intent intent;
if (userMustSetProfileName()) {
intent = getCreateProfileNameIntent();
} else {
intent = getIntent();
}
return getRoutedIntent(CreateKbsPinActivity.class, intent);
}
private Intent getCreateProfileNameIntent() {
return getRoutedIntent(EditProfileActivity.class, getIntent());
}
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
final Intent intent = new Intent(this, destination);
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
@@ -223,4 +261,12 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
clearKeyReceiver = null;
}
}
/**
* Puts an extra in {@code intent} so that {@code nextIntent} will be shown after it.
*/
public static @NonNull Intent chainIntent(@NonNull Intent intent, @NonNull Intent nextIntent) {
intent.putExtra(NEXT_INTENT_EXTRA, nextIntent);
return intent;
}
}

View File

@@ -1,50 +0,0 @@
package org.thoughtcrime.securesms;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.appcompat.widget.SwitchCompat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
public class ReadReceiptsIntroFragment extends Fragment {
public static ReadReceiptsIntroFragment newInstance() {
ReadReceiptsIntroFragment fragment = new ReadReceiptsIntroFragment();
Bundle args = new Bundle();
fragment.setArguments(args);
return fragment;
}
public ReadReceiptsIntroFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.experience_upgrade_preference_fragment, container, false);
SwitchCompat preference = ViewUtil.findById(v, R.id.preference);
preference.setChecked(TextSecurePreferences.isReadReceiptsEnabled(getContext()));
preference.setOnCheckedChangeListener((buttonView, isChecked) -> {
TextSecurePreferences.setReadReceiptsEnabled(getContext(), isChecked);
ApplicationDependencies.getJobManager()
.add(new MultiDeviceConfigurationUpdateJob(isChecked,
TextSecurePreferences.isTypingIndicatorsEnabled(requireContext()),
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
});
return v;
}
}

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
@@ -25,7 +24,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
@@ -48,9 +46,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
@@ -72,7 +68,6 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.Dialogs;
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
@@ -82,7 +77,6 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -93,8 +87,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
{
private static final String TAG = RecipientPreferenceActivity.class.getSimpleName();
public static final String RECIPIENT_ID = "recipient_address";
public static final String CAN_HAVE_SAFETY_NUMBER_EXTRA = "can_have_safety_number";
public static final String RECIPIENT_ID = "recipient";
private static final String PREFERENCE_MUTED = "pref_key_recipient_mute";
private static final String PREFERENCE_MESSAGE_TONE = "pref_key_recipient_ringtone";
@@ -117,6 +110,13 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private ThreadPhotoRailView threadPhotoRailView;
private CollapsingToolbarLayout toolbarLayout;
public static @NonNull Intent getLaunchIntent(@NonNull Context context, @NonNull RecipientId id) {
Intent intent = new Intent(context, RecipientPreferenceActivity.class);
intent.putExtra(RecipientPreferenceActivity.RECIPIENT_ID, id);
return intent;
}
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
@@ -164,12 +164,6 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
return false;
}
@Override
public void onBackPressed() {
finish();
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
}
private void initializeToolbar() {
this.toolbarLayout = ViewUtil.findById(this, R.id.collapsing_toolbar);
this.avatar = ViewUtil.findById(this, R.id.avatar);
@@ -179,16 +173,10 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
this.toolbarLayout.setExpandedTitleColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
this.toolbarLayout.setCollapsedTitleTextColor(ThemeUtil.getThemedColor(this, R.attr.conversation_title_color));
this.threadPhotoRailView.setListener(mediaRecord -> {
Intent intent = new Intent(RecipientPreferenceActivity.this, MediaPreviewActivity.class);
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId());
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, mediaRecord.getAttachment().getCaption());
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR);
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
startActivity(intent);
});
this.threadPhotoRailView.setListener(mediaRecord ->
startActivity(MediaPreviewActivity.intentFromMediaRecord(RecipientPreferenceActivity.this,
mediaRecord,
ViewCompat.getLayoutDirection(threadPhotoRailView) == ViewCompat.LAYOUT_DIRECTION_LTR)));
SimpleTask.run(
() -> DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipientId),
@@ -218,7 +206,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
}
private void setHeader(@NonNull Recipient recipient) {
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(this)))
ContactPhoto contactPhoto = recipient.isLocalNumber() ? new ProfileContactPhoto(recipient, recipient.getProfileAvatar())
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isLocalNumber() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();
@@ -275,8 +263,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
initializeRecipients();
this.canHaveSafetyNumber = getActivity().getIntent()
.getBooleanExtra(RecipientPreferenceActivity.CAN_HAVE_SAFETY_NUMBER_EXTRA, false);
this.canHaveSafetyNumber = recipient.get().isRegistered() && !recipient.get().isLocalNumber();
Preference customNotificationsPref = this.findPreference(PREFERENCE_CUSTOM_NOTIFICATIONS);
@@ -442,7 +429,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
aboutPreference.setSummary(recipient.getCustomLabel());
}
aboutPreference.setSecure(recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED);
aboutPreference.setState(recipient.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED, recipient.isBlocked());
IdentityUtil.getRemoteIdentityKey(getActivity(), recipient).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
@Override
@@ -684,11 +671,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
@Override
public boolean onPreferenceClick(Preference preference) {
Intent verifyIdentityIntent = new Intent(preference.getContext(), VerifyIdentityActivity.class);
verifyIdentityIntent.putExtra(VerifyIdentityActivity.RECIPIENT_EXTRA, recipient.getId());
verifyIdentityIntent.putExtra(VerifyIdentityActivity.IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey.getIdentityKey()));
verifyIdentityIntent.putExtra(VerifyIdentityActivity.VERIFIED_EXTRA, identityKey.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
startActivity(verifyIdentityIntent);
startActivity(VerifyIdentityActivity.newIntent(preference.getContext(), identityKey));
return true;
}
@@ -697,72 +680,15 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
private class BlockClickedListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
if (recipient.get().isBlocked()) handleUnblock(preference.getContext());
else handleBlock(preference.getContext());
Context context = preference.getContext();
return true;
}
private void handleBlock(@NonNull final Context context) {
new AsyncTask<Void, Void, Pair<Integer, Integer>>() {
@Override
protected Pair<Integer, Integer> doInBackground(Void... voids) {
int titleRes = R.string.RecipientPreferenceActivity_block_this_contact_question;
int bodyRes = R.string.RecipientPreferenceActivity_you_will_no_longer_receive_messages_and_calls_from_this_contact;
if (recipient.get().isGroup()) {
bodyRes = R.string.RecipientPreferenceActivity_block_and_leave_group_description;
if (recipient.get().isGroup() && DatabaseFactory.getGroupDatabase(context).isActive(recipient.get().requireGroupId())) {
titleRes = R.string.RecipientPreferenceActivity_block_and_leave_group;
} else {
titleRes = R.string.RecipientPreferenceActivity_block_group;
}
}
return new Pair<>(titleRes, bodyRes);
}
@Override
protected void onPostExecute(Pair<Integer, Integer> titleAndBody) {
new AlertDialog.Builder(context)
.setTitle(titleAndBody.first)
.setMessage(titleAndBody.second)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_block, (dialog, which) -> {
setBlocked(context, recipient.get(), true);
}).show();
}
}.execute();
}
private void handleUnblock(@NonNull Context context) {
int titleRes = R.string.RecipientPreferenceActivity_unblock_this_contact_question;
int bodyRes = R.string.RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact;
if (recipient.resolve().isGroup()) {
titleRes = R.string.RecipientPreferenceActivity_unblock_this_group_question;
bodyRes = R.string.RecipientPreferenceActivity_unblock_this_group_description;
if (recipient.get().isBlocked()) {
BlockUnblockDialog.showUnblockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.unblock(context, recipient.get()));
} else {
BlockUnblockDialog.showBlockFor(context, getLifecycle(), recipient.get(), () -> RecipientUtil.block(context, recipient.get()));
}
new AlertDialog.Builder(context)
.setTitle(titleRes)
.setMessage(bodyRes)
.setCancelable(true)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, (dialog, which) -> setBlocked(context, recipient.get(), false)).show();
}
private void setBlocked(@NonNull final Context context, final Recipient recipient, final boolean blocked) {
SignalExecutors.BOUNDED.execute(() -> {
if (blocked) {
RecipientUtil.block(context, recipient);
} else {
RecipientUtil.unblock(context, recipient);
}
});
return true;
}
}
@@ -785,16 +711,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
@Override
public void onInSecureCallClicked() {
try {
Intent dialIntent = new Intent(Intent.ACTION_DIAL,
Uri.parse("tel:" + recipient.get().requireE164()));
startActivity(dialIntent);
} catch (ActivityNotFoundException anfe) {
Log.w(TAG, anfe);
Dialogs.showAlertDialog(getContext(),
getString(R.string.ConversationActivity_calls_not_supported),
getString(R.string.ConversationActivity_this_device_does_not_appear_to_support_dial_actions));
}
CommunicationActions.startInsecureCall(requireActivity(), recipient.get());
}
}

View File

@@ -109,7 +109,7 @@ public class TransportOptions {
public static @NonNull TransportOption getPushTransportOption(@NonNull Context context) {
return new TransportOption(Type.TEXTSECURE,
R.drawable.ic_send_lock_24,
context.getResources().getColor(R.color.textsecure_primary),
context.getResources().getColor(R.color.core_ultramarine),
context.getString(R.string.ConversationActivity_transport_signal),
context.getString(R.string.conversation_activity__type_message_push),
new PushCharacterCalculator());

View File

@@ -1,73 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.thoughtcrime.securesms.components.TypingIndicatorView;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceConfigurationUpdateJob;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class TypingIndicatorIntroFragment extends Fragment {
private Controller controller;
public static TypingIndicatorIntroFragment newInstance() {
TypingIndicatorIntroFragment fragment = new TypingIndicatorIntroFragment();
Bundle args = new Bundle();
fragment.setArguments(args);
return fragment;
}
public TypingIndicatorIntroFragment() {}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (!(getActivity() instanceof Controller)) {
throw new IllegalStateException("Parent activity must implement the Controller interface.");
}
controller = (Controller) getActivity();
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.experience_upgrade_typing_indicators_fragment, container, false);
View yesButton = view.findViewById(R.id.experience_yes_button);
View noButton = view.findViewById(R.id.experience_no_button);
((TypingIndicatorView) view.findViewById(R.id.typing_indicator)).startAnimation();
yesButton.setOnClickListener(v -> onButtonClicked(true));
noButton.setOnClickListener(v -> onButtonClicked(false));
return view;
}
private void onButtonClicked(boolean typingEnabled) {
TextSecurePreferences.setTypingIndicatorsEnabled(getContext(), typingEnabled);
ApplicationDependencies.getJobManager().add(new MultiDeviceConfigurationUpdateJob(TextSecurePreferences.isReadReceiptsEnabled(requireContext()),
typingEnabled,
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(getContext()),
TextSecurePreferences.isLinkPreviewsEnabled(getContext())));
controller.onTypingIndicatorsFinished();
}
public interface Controller {
void onTypingIndicatorsFinished();
}
}

View File

@@ -34,17 +34,9 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.appcompat.widget.SwitchCompat;
import android.text.Html;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
@@ -62,13 +54,22 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.camera.CameraView;
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceVerifiedUpdateJob;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.qr.QrCode;
import org.thoughtcrime.securesms.qr.ScanListener;
@@ -76,8 +77,8 @@ import org.thoughtcrime.securesms.qr.ScanningThread;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.DynamicDarkActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.IdentityUtil;
@@ -105,22 +106,53 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
@SuppressLint("StaticFieldLeak")
public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity implements ScanListener, View.OnClickListener {
private static final String TAG = VerifyIdentityActivity.class.getSimpleName();
private static final String TAG = Log.tag(VerifyIdentityActivity.class);
public static final String RECIPIENT_EXTRA = "recipient_id";
public static final String IDENTITY_EXTRA = "recipient_identity";
public static final String VERIFIED_EXTRA = "verified_state";
private static final String RECIPIENT_EXTRA = "recipient_id";
private static final String IDENTITY_EXTRA = "recipient_identity";
private static final String VERIFIED_EXTRA = "verified_state";
private final DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
private final DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
private VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
private VerifyScanFragment scanFragment = new VerifyScanFragment();
private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityDatabase.IdentityRecord identityRecord)
{
return newIntent(context,
identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
identityRecord.getVerifiedStatus() == IdentityDatabase.VerifiedStatus.VERIFIED);
}
public static Intent newIntent(@NonNull Context context,
@NonNull IdentityDatabase.IdentityRecord identityRecord,
boolean verified)
{
return newIntent(context,
identityRecord.getRecipientId(),
identityRecord.getIdentityKey(),
verified);
}
public static Intent newIntent(@NonNull Context context,
@NonNull RecipientId recipientId,
@NonNull IdentityKey identityKey,
boolean verified)
{
Intent intent = new Intent(context, VerifyIdentityActivity.class);
intent.putExtra(RECIPIENT_EXTRA, recipientId);
intent.putExtra(IDENTITY_EXTRA, new IdentityKeyParcelable(identityKey));
intent.putExtra(VERIFIED_EXTRA, verified);
return intent;
}
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
dynamicLanguage.onCreate(this);
}
@Override
@@ -143,7 +175,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
scanFragment.setScanListener(this);
displayFragment.setClickListener(this);
initFragment(android.R.id.content, displayFragment, dynamicLanguage.getCurrentLocale(), extras);
initFragment(android.R.id.content, displayFragment, Locale.getDefault(), extras);
}
@Override
@@ -604,6 +636,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActionBarActivity
remoteIdentity,
isChecked ? VerifiedStatus.VERIFIED :
VerifiedStatus.DEFAULT));
StorageSyncHelper.scheduleSyncForDataChange();
IdentityUtil.markIdentityVerified(getActivity(), recipient.get(), isChecked, false);
}

View File

@@ -18,42 +18,58 @@
package org.thoughtcrime.securesms;
import android.Manifest;
import android.app.Activity;
import android.app.PictureInPictureParams;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.logging.Log;
import android.view.View;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.util.Rational;
import android.view.Window;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProviders;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAnswerDeclineButton;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallControls;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallScreen;
import org.thoughtcrime.securesms.components.TooltipPopup;
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.crypto.storage.TextSecureIdentityKeyStore;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.VerifySpan;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.SurfaceViewRenderer;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.libsignal.SignalProtocolAddress;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
public class WebRtcCallActivity extends Activity {
public class WebRtcCallActivity extends AppCompatActivity {
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
@@ -66,8 +82,10 @@ public class WebRtcCallActivity extends Activity {
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
private WebRtcCallScreen callScreen;
private boolean enableVideoIfAvailable;
private WebRtcCallView callScreen;
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
private boolean enableVideoIfAvailable;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -78,10 +96,12 @@ public class WebRtcCallActivity extends Activity {
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
getSupportActionBar().hide();
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
initializeResources();
initializeViewModel();
processIntent(getIntent());
@@ -89,18 +109,21 @@ public class WebRtcCallActivity extends Activity {
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
}
@Override
public void onResume() {
Log.i(TAG, "onResume()");
super.onResume();
initializeScreenshotSecurity();
EventBus.getDefault().register(this);
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this);
}
}
@Override
public void onNewIntent(Intent intent){
Log.i(TAG, "onNewIntent");
super.onNewIntent(intent);
processIntent(intent);
}
@@ -108,6 +131,17 @@ public class WebRtcCallActivity extends Activity {
public void onPause() {
Log.i(TAG, "onPause");
super.onPause();
if (!isInPipMode()) {
EventBus.getDefault().unregister(this);
}
}
@Override
protected void onStop() {
Log.i(TAG, "onStop");
super.onStop();
EventBus.getDefault().unregister(this);
}
@@ -121,9 +155,31 @@ public class WebRtcCallActivity extends Activity {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
}
@Override
protected void onUserLeaveHint() {
if (deviceSupportsPipMode()) {
PictureInPictureParams params = new PictureInPictureParams.Builder()
.setAspectRatio(new Rational(16, 9))
.build();
setPictureInPictureParams(params);
//noinspection deprecation
enterPictureInPictureMode();
}
}
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
viewModel.setIsInPipMode(isInPictureInPictureMode);
}
private boolean isInPipMode() {
return deviceSupportsPipMode() && isInPictureInPictureMode();
}
private void processIntent(@NonNull Intent intent) {
if (ANSWER_ACTION.equals(intent.getAction())) {
handleAnswerCall();
handleAnswerWithAudio();
} else if (DENY_ACTION.equals(intent.getAction())) {
handleDenyCall();
} else if (END_CALL_ACTION.equals(intent.getAction())) {
@@ -141,13 +197,61 @@ public class WebRtcCallActivity extends Activity {
private void initializeResources() {
callScreen = ViewUtil.findById(this, R.id.callScreen);
callScreen.setHangupButtonListener(new HangupButtonListener());
callScreen.setIncomingCallActionListener(new IncomingCallActionListener());
callScreen.setAudioMuteButtonListener(new AudioMuteButtonListener());
callScreen.setVideoMuteButtonListener(new VideoMuteButtonListener());
callScreen.setCameraFlipButtonListener(new CameraFlipButtonListener());
callScreen.setSpeakerButtonListener(new SpeakerButtonListener());
callScreen.setBluetoothButtonListener(new BluetoothButtonListener());
callScreen.setControlsListener(new ControlsListener());
}
private void initializeViewModel() {
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
viewModel.setIsInPipMode(isInPipMode());
viewModel.getRemoteVideoEnabled().observe(this,callScreen::setRemoteVideoEnabled);
viewModel.getBluetoothEnabled().observe(this, callScreen::setBluetoothEnabled);
viewModel.getAudioOutput().observe(this, callScreen::setAudioOutput);
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
viewModel.getCameraDirection().observe(this, callScreen::setCameraDirection);
viewModel.getLocalRenderState().observe(this, callScreen::setLocalRenderState);
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
viewModel.getEvents().observe(this, this::handleViewModelEvent);
viewModel.getCallTime().observe(this, this::handleCallTime);
viewModel.displaySquareCallCard().observe(this, callScreen::showCallCard);
viewModel.isMoreThanOneCameraAvailable().observe(this, callScreen::showCameraToggleButton);
}
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
if (isInPipMode()) {
return;
}
switch (event) {
case SHOW_VIDEO_TOOLTIP:
if (videoTooltip == null) {
videoTooltip = TooltipPopup.forTarget(callScreen.getVideoTooltipTarget())
.setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
.setTextColor(ContextCompat.getColor(this, R.color.core_white))
.setText(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video)
.setOnDismissListener(() -> viewModel.onDismissedVideoTooltip())
.show(TooltipPopup.POSITION_ABOVE);
return;
}
break;
case DISMISS_VIDEO_TOOLTIP:
if (videoTooltip != null) {
videoTooltip.dismiss();
videoTooltip = null;
}
break;
default:
throw new IllegalArgumentException("Unknown event: " + event);
}
}
private void handleCallTime(long callTime) {
EllapsedTimeFormatter ellapsedTimeFormatter = EllapsedTimeFormatter.fromDurationMillis(callTime);
if (ellapsedTimeFormatter == null) {
return;
}
callScreen.setStatus(getString(R.string.WebRtcCallActivity__signal_s, ellapsedTimeFormatter.toString()));
}
private void handleSetAudioSpeaker(boolean enabled) {
@@ -172,10 +276,24 @@ public class WebRtcCallActivity extends Activity {
}
private void handleSetMuteVideo(boolean muted) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_SET_MUTE_VIDEO);
intent.putExtra(WebRtcCallService.EXTRA_MUTE, muted);
startService(intent);
Recipient recipient = viewModel.getRecipient().get();
if (!recipient.equals(Recipient.UNKNOWN)) {
String recipientDisplayName = recipient.getDisplayName(this);
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName), R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName))
.onAllGranted(() -> {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_SET_ENABLE_VIDEO);
intent.putExtra(WebRtcCallService.EXTRA_ENABLE, !muted);
startService(intent);
})
.execute();
}
}
private void handleFlipCamera() {
@@ -184,21 +302,22 @@ public class WebRtcCallActivity extends Activity {
startService(intent);
}
private void handleAnswerCall() {
WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
private void handleAnswerWithAudio() {
Recipient recipient = viewModel.getRecipient().get();
if (event != null) {
if (!recipient.equals(Recipient.UNKNOWN)) {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, event.getRecipient().toShortString(this)),
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
R.drawable.ic_mic_solid_24)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_answering), event.getLocalRenderer());
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_answering));
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ANSWER_CALL);
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
startService(intent);
})
.onAnyDenied(this::handleDenyCall)
@@ -206,15 +325,42 @@ public class WebRtcCallActivity extends Activity {
}
}
private void handleDenyCall() {
WebRtcViewModel event = EventBus.getDefault().getStickyEvent(WebRtcViewModel.class);
private void handleAnswerWithVideo() {
Recipient recipient = viewModel.getRecipient().get();
if (event != null) {
if (!recipient.equals(Recipient.UNKNOWN)) {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_from_s_give_signal_access_to_your_microphone, recipient.getDisplayName(this)),
R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_answering));
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ACCEPT_CALL);
intent.putExtra(WebRtcCallService.EXTRA_ANSWER_WITH_VIDEO, true);
startService(intent);
handleSetMuteVideo(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
}
}
private void handleDenyCall() {
Recipient recipient = viewModel.getRecipient().get();
if (!recipient.equals(Recipient.UNKNOWN)) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_DENY_CALL);
startService(intent);
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_ending_call), event.getLocalRenderer());
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_ending_call));
delayedFinish();
}
}
@@ -227,44 +373,54 @@ public class WebRtcCallActivity extends Activity {
}
private void handleIncomingCall(@NonNull WebRtcViewModel event) {
callScreen.setIncomingCall(event.getRecipient());
callScreen.setRecipient(event.getRecipient());
}
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_dialing), event.getLocalRenderer());
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.WebRtcCallActivity__calling));
}
private void handleTerminate(@NonNull Recipient recipient, @NonNull SurfaceViewRenderer localRenderer /*, int terminationType */) {
Log.i(TAG, "handleTerminate called");
private void handleTerminate(@NonNull Recipient recipient, @NonNull HangupMessage.Type hangupType) {
Log.i(TAG, "handleTerminate called: " + hangupType.name());
callScreen.setRecipient(recipient);
callScreen.setStatusFromHangupType(hangupType);
callScreen.setActiveCall(recipient, getString(R.string.RedPhone_ending_call), localRenderer);
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
delayedFinish();
}
private void handleCallRinging(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_ringing), event.getLocalRenderer());
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_ringing));
}
private void handleCallBusy(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_busy), event.getLocalRenderer());
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_busy));
delayedFinish(BUSY_SIGNAL_DELAY_FINISH);
}
private void handleCallConnected(@NonNull WebRtcViewModel event) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_connected), "", event.getLocalRenderer(), event.getRemoteRenderer());
callScreen.setRecipient(event.getRecipient());
}
private void handleRecipientUnavailable(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_recipient_unavailable), event.getLocalRenderer());
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_recipient_unavailable));
delayedFinish();
}
private void handleServerFailure(@NonNull WebRtcViewModel event) {
callScreen.setActiveCall(event.getRecipient(), getString(R.string.RedPhone_network_failed), event.getLocalRenderer());
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setRecipient(event.getRecipient());
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish();
}
@@ -278,44 +434,63 @@ public class WebRtcCallActivity extends Activity {
dialog.setPositiveButton(R.string.RedPhone_got_it, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), event.getLocalRenderer());
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), event.getLocalRenderer());
WebRtcCallActivity.this.handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL);
}
});
dialog.show();
}
private void handleUntrustedIdentity(@NonNull WebRtcViewModel event) {
final IdentityKey theirIdentity = event.getIdentityKey();
final Recipient recipient = event.getRecipient();
final IdentityKey theirKey = event.getIdentityKey();
final Recipient recipient = event.getRecipient();
callScreen.setUntrustedIdentity(recipient, theirIdentity);
callScreen.setAcceptIdentityListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
synchronized (SESSION_LOCK) {
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirIdentity, true);
}
if (theirKey == null) {
handleTerminate(recipient, HangupMessage.Type.NORMAL);
}
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.putExtra(WebRtcCallService.EXTRA_REMOTE_RECIPIENT, recipient.getId());
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL);
startService(intent);
}
});
String name = recipient.getDisplayName(this);
String introduction = getString(R.string.WebRtcCallScreen_new_safety_numbers, name, name);
SpannableString spannableString = new SpannableString(introduction + " " + getString(R.string.WebRtcCallScreen_you_may_wish_to_verify_this_contact));
callScreen.setCancelIdentityButton(new View.OnClickListener() {
@Override
public void onClick(View v) {
handleTerminate(recipient, event.getLocalRenderer());
}
});
spannableString.setSpan(new VerifySpan(this, recipient.getId(), theirKey), introduction.length() + 1, spannableString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
AppCompatTextView untrustedIdentityExplanation = new AppCompatTextView(this);
untrustedIdentityExplanation.setText(spannableString);
untrustedIdentityExplanation.setMovementMethod(LinkMovementMethod.getInstance());
new AlertDialog.Builder(this)
.setView(untrustedIdentityExplanation)
.setPositiveButton(R.string.WebRtcCallScreen_accept, (d, w) -> {
synchronized (SESSION_LOCK) {
TextSecureIdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(WebRtcCallActivity.this);
identityKeyStore.saveIdentity(new SignalProtocolAddress(recipient.requireServiceId(), 1), theirKey, true);
}
d.dismiss();
Intent intent = new Intent(WebRtcCallActivity.this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_OUTGOING_CALL)
.putExtra(WebRtcCallService.EXTRA_REMOTE_PEER, new RemotePeer(recipient.getId()));
startService(intent);
})
.setNegativeButton(R.string.WebRtcCallScreen_end_call, (d, w) -> {
d.dismiss();
handleTerminate(recipient, HangupMessage.Type.NORMAL);
})
.show();
}
private boolean deviceSupportsPipMode() {
return Build.VERSION.SDK_INT >= 26 &&
FeatureFlags.callingPip() &&
getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
}
private void delayedFinish() {
@@ -323,34 +498,35 @@ public class WebRtcCallActivity extends Activity {
}
private void delayedFinish(int delayMillis) {
callScreen.postDelayed(new Runnable() {
public void run() {
WebRtcCallActivity.this.finish();
}
}, delayMillis);
callScreen.postDelayed(WebRtcCallActivity.this::finish, delayMillis);
}
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(final WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event);
viewModel.setRecipient(event.getRecipient());
switch (event.getState()) {
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(event); break;
case CALL_RINGING: handleCallRinging(event); break;
case CALL_DISCONNECTED: handleTerminate(event.getRecipient(), event.getLocalRenderer()); break;
case NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
case CALL_INCOMING: handleIncomingCall(event); break;
case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
case CALL_CONNECTED: handleCallConnected(event); break;
case NETWORK_FAILURE: handleServerFailure(event); break;
case CALL_RINGING: handleCallRinging(event); 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 NO_SUCH_USER: handleNoSuchUser(event); break;
case RECIPIENT_UNAVAILABLE: handleRecipientUnavailable(event); break;
case CALL_INCOMING: handleIncomingCall(event); break;
case CALL_OUTGOING: handleOutgoingCall(event); break;
case CALL_BUSY: handleCallBusy(event); break;
case UNTRUSTED_IDENTITY: handleUntrustedIdentity(event); break;
}
callScreen.setRemoteVideoEnabled(event.isRemoteVideoEnabled());
callScreen.updateAudioState(event.isBluetoothAvailable(), event.isMicrophoneEnabled());
callScreen.setControlsEnabled(event.getState() != WebRtcViewModel.State.CALL_INCOMING);
callScreen.setLocalVideoState(event.getLocalCameraState(), event.getLocalRenderer());
callScreen.setLocalRenderer(event.getLocalRenderer());
callScreen.setRemoteRenderer(event.getRemoteRenderer());
viewModel.updateFromWebRtcViewModel(event);
if (event.getLocalCameraState().getCameraCount() > 0 && enableVideoIfAvailable) {
enableVideoIfAvailable = false;
@@ -358,57 +534,75 @@ public class WebRtcCallActivity extends Activity {
}
}
private class HangupButtonListener implements WebRtcCallScreen.HangupButtonListener {
public void onClick() {
private final class ControlsListener implements WebRtcCallView.ControlsListener {
@Override
public void onControlsFadeOut() {
if (videoTooltip != null) {
videoTooltip.dismiss();
}
}
@Override
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
switch (audioOutput) {
case HANDSET:
handleSetAudioSpeaker(false);
break;
case HEADSET:
handleSetAudioBluetooth(true);
break;
case SPEAKER:
handleSetAudioSpeaker(true);
break;
default:
throw new IllegalStateException("Unknown output: " + audioOutput);
}
}
@Override
public void onVideoChanged(boolean isVideoEnabled) {
handleSetMuteVideo(!isVideoEnabled);
}
@Override
public void onMicChanged(boolean isMicEnabled) {
handleSetMuteAudio(!isMicEnabled);
}
@Override
public void onCameraDirectionChanged() {
handleFlipCamera();
}
@Override
public void onEndCallPressed() {
handleEndCall();
}
}
private class AudioMuteButtonListener implements WebRtcCallControls.MuteButtonListener {
@Override
public void onToggle(boolean isMuted) {
WebRtcCallActivity.this.handleSetMuteAudio(isMuted);
}
}
private class VideoMuteButtonListener implements WebRtcCallControls.MuteButtonListener {
@Override
public void onToggle(boolean isMuted) {
WebRtcCallActivity.this.handleSetMuteVideo(isMuted);
}
}
private class CameraFlipButtonListener implements WebRtcCallControls.CameraFlipButtonListener {
@Override
public void onToggle() {
WebRtcCallActivity.this.handleFlipCamera();
}
}
private class SpeakerButtonListener implements WebRtcCallControls.SpeakerButtonListener {
@Override
public void onSpeakerChange(boolean isSpeaker) {
WebRtcCallActivity.this.handleSetAudioSpeaker(isSpeaker);
}
}
private class BluetoothButtonListener implements WebRtcCallControls.BluetoothButtonListener {
@Override
public void onBluetoothChange(boolean isBluetooth) {
WebRtcCallActivity.this.handleSetAudioBluetooth(isBluetooth);
}
}
private class IncomingCallActionListener implements WebRtcAnswerDeclineButton.AnswerDeclineListener {
@Override
public void onAnswered() {
WebRtcCallActivity.this.handleAnswerCall();
public void onDenyCallPressed() {
handleDenyCall();
}
@Override
public void onDeclined() {
WebRtcCallActivity.this.handleDenyCall();
public void onAcceptCallWithVoiceOnlyPressed() {
handleAnswerWithAudio();
}
@Override
public void onAcceptCallPressed() {
if (viewModel.isAnswerWithVideoAvailable()) {
handleAnswerWithVideo();
} else {
handleAnswerWithAudio();
}
}
@Override
public void onDownCaretPressed() {
}
}
}
}

View File

@@ -0,0 +1,32 @@
package org.thoughtcrime.securesms.animation;
import android.animation.Animator;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
public final class AnimationRepeatListener implements Animator.AnimatorListener {
private final Consumer<Animator> animationConsumer;
public AnimationRepeatListener(@NonNull Consumer<Animator> animationConsumer) {
this.animationConsumer = animationConsumer;
}
@Override
public final void onAnimationStart(Animator animation) {
}
@Override
public final void onAnimationEnd(Animator animation) {
}
@Override
public final void onAnimationCancel(Animator animation) {
}
@Override
public final void onAnimationRepeat(Animator animation) {
this.animationConsumer.accept(animation);
}
}

View File

@@ -19,6 +19,8 @@ public abstract class Attachment {
@Nullable
private final String fileName;
private final int cdnNumber;
@Nullable
private final String location;
@@ -35,10 +37,10 @@ public abstract class Attachment {
private final String fastPreflightId;
private final boolean voiceNote;
private final int width;
private final int height;
private final int width;
private final int height;
private final boolean quote;
private final long uploadTimestamp;
@Nullable
private final String caption;
@@ -53,15 +55,17 @@ public abstract class Attachment {
private final TransformProperties transformProperties;
public Attachment(@NonNull String contentType, int transferState, long size, @Nullable String fileName,
@Nullable String location, @Nullable String key, @Nullable String relay,
int cdnNumber, @Nullable String location, @Nullable String key, @Nullable String relay,
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
int width, int height, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
int width, int height, boolean quote, long uploadTimestamp, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
@Nullable TransformProperties transformProperties)
{
this.contentType = contentType;
this.transferState = transferState;
this.size = size;
this.fileName = fileName;
this.cdnNumber = cdnNumber;
this.location = location;
this.key = key;
this.relay = relay;
@@ -71,6 +75,7 @@ public abstract class Attachment {
this.width = width;
this.height = height;
this.quote = quote;
this.uploadTimestamp = uploadTimestamp;
this.stickerLocator = stickerLocator;
this.caption = caption;
this.blurHash = blurHash;
@@ -106,6 +111,10 @@ public abstract class Attachment {
return contentType;
}
public int getCdnNumber() {
return cdnNumber;
}
@Nullable
public String getLocation() {
return location;
@@ -147,6 +156,10 @@ public abstract class Attachment {
return quote;
}
public long getUploadTimestamp() {
return uploadTimestamp;
}
public boolean isSticker() {
return stickerLocator != null;
}

View File

@@ -2,11 +2,9 @@ package org.thoughtcrime.securesms.attachments;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@@ -24,13 +22,14 @@ public class DatabaseAttachment extends Attachment {
public DatabaseAttachment(AttachmentId attachmentId, long mmsId,
boolean hasData, boolean hasThumbnail,
String contentType, int transferProgress, long size,
String fileName, String location, String key, String relay,
String fileName, int cdnNumber, String location, String key, String relay,
byte[] digest, String fastPreflightId, boolean voiceNote,
int width, int height, boolean quote, @Nullable String caption,
@Nullable StickerLocator stickerLocator, @Nullable BlurHash blurHash,
@Nullable TransformProperties transformProperties, int displayOrder)
@Nullable TransformProperties transformProperties, int displayOrder,
long uploadTimestamp)
{
super(contentType, transferProgress, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, caption, stickerLocator, blurHash, transformProperties);
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, quote, uploadTimestamp, caption, stickerLocator, blurHash, transformProperties);
this.attachmentId = attachmentId;
this.hasData = hasData;
this.hasThumbnail = hasThumbnail;

View File

@@ -10,7 +10,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, null, null, null, null, null, false, 0, 0, false, null, null, null, null);
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, false, 0, 0, false, 0, null, null, null, null);
}
@Nullable

View File

@@ -18,13 +18,13 @@ import java.util.List;
public class PointerAttachment extends Attachment {
private PointerAttachment(@NonNull String contentType, int transferState, long size,
@Nullable String fileName, @NonNull String location,
@Nullable String fileName, int cdnNumber, @NonNull String location,
@Nullable String key, @Nullable String relay,
@Nullable byte[] digest, @Nullable String fastPreflightId, boolean voiceNote,
int width, int height, @Nullable String caption, @Nullable StickerLocator stickerLocator,
int width, int height, long uploadTimestamp, @Nullable String caption, @Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash)
{
super(contentType, transferState, size, fileName, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, caption, stickerLocator, blurHash, null);
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, fastPreflightId, voiceNote, width, height, false, uploadTimestamp, caption, stickerLocator, blurHash, null);
}
@Nullable
@@ -93,13 +93,15 @@ public class PointerAttachment extends Attachment {
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().or(0),
pointer.get().asPointer().getFileName().orNull(),
String.valueOf(pointer.get().asPointer().getId()),
pointer.get().asPointer().getCdnNumber(),
pointer.get().asPointer().getRemoteId().toString(),
encodedKey, null,
pointer.get().asPointer().getDigest().orNull(),
fastPreflightId,
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().getWidth(),
pointer.get().asPointer().getHeight(),
pointer.get().asPointer().getUploadTimestamp(),
pointer.get().asPointer().getCaption().orNull(),
stickerLocator,
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orNull())));
@@ -113,7 +115,8 @@ public class PointerAttachment extends Attachment {
AttachmentDatabase.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().or(0) : 0,
pointer.getFileName(),
String.valueOf(thumbnail != null ? thumbnail.asPointer().getId() : 0),
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeBytes(thumbnail.asPointer().getKey()) : null,
null,
thumbnail != null ? thumbnail.asPointer().getDigest().orNull() : null,
@@ -121,6 +124,7 @@ public class PointerAttachment extends Attachment {
false,
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
thumbnail != null ? thumbnail.asPointer().getCaption().orNull() : null,
null,
null));

View File

@@ -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, null, null, null, null, null, false, 0, 0, quote, null, null, null, null);
super(contentType, AttachmentDatabase.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, false, 0, 0, quote, 0, null, null, null, null);
}
@Override

View File

@@ -1,11 +1,11 @@
package org.thoughtcrime.securesms.attachments;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.AttachmentDatabase.TransformProperties;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@@ -27,7 +27,7 @@ public class UriAttachment extends Attachment {
boolean voiceNote, boolean quote, @Nullable String caption, @Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash, @Nullable TransformProperties transformProperties)
{
super(contentType, transferState, size, fileName, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, caption, stickerLocator, blurHash, transformProperties);
super(contentType, transferState, size, fileName, 0, null, null, null, null, fastPreflightId, voiceNote, width, height, quote, 0, caption, stickerLocator, blurHash, transformProperties);
this.dataUri = dataUri;
this.thumbnailUri = thumbnailUri;
}

View File

@@ -1,149 +0,0 @@
package org.thoughtcrime.securesms.avatar;
import android.Manifest;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.provider.MediaStore;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import com.theartofdev.edmodo.cropper.CropImage;
import com.theartofdev.edmodo.cropper.CropImageView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.FileProviderUtil;
import org.thoughtcrime.securesms.util.IntentUtils;
import java.io.File;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import static android.provider.MediaStore.EXTRA_OUTPUT;
public final class AvatarSelection {
private static final String TAG = AvatarSelection.class.getSimpleName();
private AvatarSelection() {
}
public static final int REQUEST_CODE_CROP_IMAGE = CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE;
public static final int REQUEST_CODE_AVATAR = REQUEST_CODE_CROP_IMAGE + 1;
/**
* Returns result on {@link #REQUEST_CODE_CROP_IMAGE}
*/
public static void circularCropImage(Activity activity, Uri inputFile, Uri outputFile, @StringRes int title) {
CropImage.activity(inputFile)
.setGuidelines(CropImageView.Guidelines.ON)
.setAspectRatio(1, 1)
.setCropShape(CropImageView.CropShape.OVAL)
.setOutputUri(outputFile)
.setAllowRotation(true)
.setAllowFlipping(true)
.setBackgroundColor(ContextCompat.getColor(activity, R.color.avatar_background))
.setActivityTitle(activity.getString(title))
.start(activity);
}
/**
* Returns result on {@link #REQUEST_CODE_CROP_IMAGE}
*/
public static void circularCropImage(Fragment fragment, Uri inputFile, Uri outputFile, @StringRes int title) {
CropImage.activity(inputFile)
.setGuidelines(CropImageView.Guidelines.ON)
.setAspectRatio(1, 1)
.setCropShape(CropImageView.CropShape.OVAL)
.setOutputUri(outputFile)
.setAllowRotation(true)
.setAllowFlipping(true)
.setBackgroundColor(ContextCompat.getColor(fragment.requireContext(), R.color.avatar_background))
.setActivityTitle(fragment.requireContext().getString(title))
.start(fragment.requireContext(), fragment);
}
public static Uri getResultUri(Intent data) {
return CropImage.getActivityResult(data).getUri();
}
/**
* Returns result on {@link #REQUEST_CODE_AVATAR}
*
* @return Temporary capture file if created.
*/
public static File startAvatarSelection(Activity activity, boolean includeClear, boolean attemptToIncludeCamera) {
File captureFile = attemptToIncludeCamera ? getCaptureFile(activity) : null;
Intent chooserIntent = createAvatarSelectionIntent(activity, captureFile, includeClear);
activity.startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
return captureFile;
}
/**
* Returns result on {@link #REQUEST_CODE_AVATAR}
*
* @return Temporary capture file if created.
*/
public static File startAvatarSelection(Fragment fragment, boolean includeClear, boolean attemptToIncludeCamera) {
File captureFile = attemptToIncludeCamera ? getCaptureFile(fragment.requireContext()) : null;
Intent chooserIntent = createAvatarSelectionIntent(fragment.requireContext(), captureFile, includeClear);
fragment.startActivityForResult(chooserIntent, REQUEST_CODE_AVATAR);
return captureFile;
}
private static @Nullable File getCaptureFile(@NonNull Context context) {
if (!Permissions.hasAll(context, Manifest.permission.CAMERA)) {
return null;
}
try {
return File.createTempFile("capture", "jpg", context.getExternalCacheDir());
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}
private static Intent createAvatarSelectionIntent(Context context, @Nullable File tempCaptureFile, boolean includeClear) {
List<Intent> extraIntents = new LinkedList<>();
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
if (!IntentUtils.isResolvable(context, galleryIntent)) {
galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
galleryIntent.setType("image/*");
}
if (tempCaptureFile != null) {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (cameraIntent.resolveActivity(context.getPackageManager()) != null) {
cameraIntent.putExtra(EXTRA_OUTPUT, FileProviderUtil.getUriFor(context, tempCaptureFile));
extraIntents.add(cameraIntent);
}
}
if (includeClear) {
extraIntents.add(new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"));
}
Intent chooserIntent = Intent.createChooser(galleryIntent, context.getString(R.string.CreateProfileActivity_profile_photo));
if (!extraIntents.isEmpty()) {
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, extraIntents.toArray(new Intent[0]));
}
return chooserIntent;
}
}

View File

@@ -4,19 +4,25 @@ package org.thoughtcrime.securesms.backup;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
import org.thoughtcrime.securesms.registration.fragments.RestoreBackupFragment;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
public class BackupDialog {
@@ -83,4 +89,35 @@ public class BackupDialog {
.create()
.show();
}
public static void showVerifyBackupPassphraseDialog(@NonNull Context context) {
View view = LayoutInflater.from(context).inflate(R.layout.enter_backup_passphrase_dialog, null);
EditText prompt = view.findViewById(R.id.restore_passphrase_input);
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.BackupDialog_enter_backup_passphrase_to_verify)
.setView(view)
.setPositiveButton(R.string.BackupDialog_verify, null)
.setNegativeButton(android.R.string.cancel, null)
.show();
Button positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
positiveButton.setEnabled(false);
RestoreBackupFragment.PassphraseAsYouTypeFormatter formatter = new RestoreBackupFragment.PassphraseAsYouTypeFormatter();
prompt.addTextChangedListener(new AfterTextChanged(editable -> {
formatter.afterTextChanged(editable);
positiveButton.setEnabled(editable.length() == BackupUtil.PASSPHRASE_LENGTH);
}));
positiveButton.setOnClickListener(v -> {
String passphrase = prompt.getText().toString();
if (passphrase.equals(BackupPassphrase.get(context))) {
Toast.makeText(context, R.string.BackupDialog_you_successfully_entered_your_backup_passphrase, Toast.LENGTH_SHORT).show();
dialog.dismiss();
} else {
Toast.makeText(context, R.string.BackupDialog_passphrase_was_not_correct, Toast.LENGTH_SHORT).show();
}
});
}
}

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.backup;
import android.content.Context;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -12,26 +13,30 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
* Allows the getting and setting of the backup passphrase, which is stored encrypted on API >= 23.
*/
public class BackupPassphrase {
public final class BackupPassphrase {
private BackupPassphrase() {
}
private static final String TAG = BackupPassphrase.class.getSimpleName();
public static String get(@NonNull Context context) {
public static @Nullable String get(@NonNull Context context) {
String passphrase = TextSecurePreferences.getBackupPassphrase(context);
String encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
if (Build.VERSION.SDK_INT < 23 || (passphrase == null && encryptedPassphrase == null)) {
return passphrase;
return stripSpaces(passphrase);
}
if (encryptedPassphrase == null) {
Log.i(TAG, "Migrating to encrypted passphrase.");
set(context, passphrase);
encryptedPassphrase = TextSecurePreferences.getEncryptedBackupPassphrase(context);
if (encryptedPassphrase == null) throw new AssertionError("Passphrase migration failed");
}
KeyStoreHelper.SealedData data = KeyStoreHelper.SealedData.fromString(encryptedPassphrase);
return new String(KeyStoreHelper.unseal(data));
return stripSpaces(new String(KeyStoreHelper.unseal(data)));
}
public static void set(@NonNull Context context, @Nullable String passphrase) {
@@ -44,4 +49,8 @@ public class BackupPassphrase {
TextSecurePreferences.setBackupPassphrase(context, null);
}
}
private static String stripSpaces(@Nullable String passphrase) {
return passphrase != null ? passphrase.replace(" ", "") : null;
}
}

View File

@@ -21,9 +21,9 @@ 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.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
import org.thoughtcrime.securesms.database.MmsDatabase;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase;
@@ -48,7 +48,6 @@ import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@@ -74,7 +73,8 @@ public class FullBackupExporter extends FullBackupBase {
SearchDatabase.MMS_FTS_TABLE_NAME,
JobDatabase.JOBS_TABLE_NAME,
JobDatabase.CONSTRAINTS_TABLE_NAME,
JobDatabase.DEPENDENCIES_TABLE_NAME
JobDatabase.DEPENDENCIES_TABLE_NAME,
KeyValueDatabase.TABLE_NAME
);
public static void export(@NonNull Context context,
@@ -116,9 +116,11 @@ public class FullBackupExporter extends FullBackupBase {
stopwatch.split("prefs");
for (File avatar : AvatarHelper.getAvatarFiles(context)) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
outputStream.write(avatar.getName(), new FileInputStream(avatar), avatar.length());
for (AvatarHelper.Avatar avatar : AvatarHelper.getAvatars(context)) {
if (avatar != null) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count));
outputStream.write(avatar.getFilename(), avatar.getInputStream(), avatar.getLength());
}
}
stopwatch.split("avatars");

View File

@@ -165,6 +165,7 @@ public class FullBackupImporter extends FullBackupBase {
ContentValues contentValues = new ContentValues();
contentValues.put(StickerDatabase.FILE_PATH, dataFile.getAbsolutePath());
contentValues.put(StickerDatabase.FILE_LENGTH, sticker.getLength());
contentValues.put(StickerDatabase.FILE_RANDOM, output.first);
db.update(StickerDatabase.TABLE_NAME, contentValues,
@@ -175,7 +176,7 @@ public class FullBackupImporter extends FullBackupBase {
private static void processAvatar(@NonNull Context context, @NonNull SQLiteDatabase db, @NonNull BackupProtos.Avatar avatar, @NonNull BackupRecordInputStream inputStream) throws IOException {
if (avatar.hasRecipientId()) {
RecipientId recipientId = RecipientId.from(avatar.getRecipientId());
inputStream.readAttachmentTo(new FileOutputStream(AvatarHelper.getAvatarFile(context, recipientId)), avatar.getLength());
inputStream.readAttachmentTo(AvatarHelper.getOutputStream(context, recipientId), avatar.getLength());
} else {
if (avatar.hasName() && SqlUtil.tableExists(db, "recipient_preferences")) {
Log.w(TAG, "Avatar is missing a recipientId. Clearing signal_profile_avatar (legacy) so it can be fetched later.");

View File

@@ -26,6 +26,7 @@ public enum MaterialColor {
PLUM (R.color.conversation_plumb, R.color.conversation_plumb_tint, R.color.conversation_plumb_shade, "pink"),
TAUPE (R.color.conversation_taupe, R.color.conversation_taupe_tint, R.color.conversation_taupe_shade, "blue_grey"),
STEEL (R.color.conversation_steel, R.color.conversation_steel_tint, R.color.conversation_steel_shade, "grey"),
ULTRAMARINE(R.color.conversation_ultramarine, R.color.conversation_ultramarine_tint, R.color.conversation_ultramarine_shade, "ultramarine"),
GROUP (R.color.conversation_group, R.color.conversation_group_tint, R.color.conversation_group_shade, "blue");
private static final Map<String, MaterialColor> COLOR_MATCHES = new HashMap<String, MaterialColor>() {{
@@ -48,6 +49,7 @@ public enum MaterialColor {
put("lime", WINTERGREEN);
put("blue_grey", TAUPE);
put("grey", STEEL);
put("ultramarine", ULTRAMARINE);
put("group_color", GROUP);
}};

View File

@@ -11,18 +11,19 @@ import java.util.List;
public class MaterialColors {
public static final MaterialColorList CONVERSATION_PALETTE = new MaterialColorList(new ArrayList<>(Arrays.asList(
MaterialColor.PLUM,
MaterialColor.CRIMSON,
MaterialColor.VERMILLION,
MaterialColor.VIOLET,
MaterialColor.BLUE,
MaterialColor.INDIGO,
MaterialColor.FOREST,
MaterialColor.WINTERGREEN,
MaterialColor.TEAL,
MaterialColor.BURLAP,
MaterialColor.TAUPE,
MaterialColor.STEEL
MaterialColor.PLUM,
MaterialColor.CRIMSON,
MaterialColor.VERMILLION,
MaterialColor.VIOLET,
MaterialColor.INDIGO,
MaterialColor.TAUPE,
MaterialColor.ULTRAMARINE,
MaterialColor.BLUE,
MaterialColor.TEAL,
MaterialColor.FOREST,
MaterialColor.WINTERGREEN,
MaterialColor.BURLAP,
MaterialColor.STEEL
)));
public static class MaterialColorList {
@@ -61,9 +62,6 @@ public class MaterialColors {
return results;
}
}
}

View File

@@ -2,20 +2,14 @@ package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.os.Build;
import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.widget.ToggleButton;
public class AccessibleToggleButton extends ToggleButton {
import androidx.appcompat.widget.AppCompatToggleButton;
public class AccessibleToggleButton extends AppCompatToggleButton {
private OnCheckedChangeListener listener;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public AccessibleToggleButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public AccessibleToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}

View File

@@ -1,292 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.Manifest;
import android.animation.Animator;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.loader.app.LoaderManager;
import android.util.Pair;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewAnimationUtils;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.ViewUtil;
public class AttachmentTypeSelector extends PopupWindow {
public static final int ADD_GALLERY = 1;
public static final int ADD_DOCUMENT = 2;
public static final int ADD_SOUND = 3;
public static final int ADD_CONTACT_INFO = 4;
public static final int TAKE_PHOTO = 5;
public static final int ADD_LOCATION = 6;
public static final int ADD_GIF = 7;
private static final int ANIMATION_DURATION = 300;
@SuppressWarnings("unused")
private static final String TAG = AttachmentTypeSelector.class.getSimpleName();
private final @NonNull LoaderManager loaderManager;
private final @NonNull RecentPhotoViewRail recentRail;
private final @NonNull ImageView imageButton;
private final @NonNull ImageView audioButton;
private final @NonNull ImageView documentButton;
private final @NonNull ImageView contactButton;
private final @NonNull ImageView cameraButton;
private final @NonNull ImageView locationButton;
private final @NonNull ImageView gifButton;
private final @NonNull ImageView closeButton;
private @Nullable View currentAnchor;
private @Nullable AttachmentClickedListener listener;
public AttachmentTypeSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener) {
super(context);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true);
this.listener = listener;
this.loaderManager = loaderManager;
this.recentRail = ViewUtil.findById(layout, R.id.recent_photos);
this.imageButton = ViewUtil.findById(layout, R.id.gallery_button);
this.audioButton = ViewUtil.findById(layout, R.id.audio_button);
this.documentButton = ViewUtil.findById(layout, R.id.document_button);
this.contactButton = ViewUtil.findById(layout, R.id.contact_button);
this.cameraButton = ViewUtil.findById(layout, R.id.camera_button);
this.locationButton = ViewUtil.findById(layout, R.id.location_button);
this.gifButton = ViewUtil.findById(layout, R.id.giphy_button);
this.closeButton = ViewUtil.findById(layout, R.id.close_button);
this.imageButton.setOnClickListener(new PropagatingClickListener(ADD_GALLERY));
this.audioButton.setOnClickListener(new PropagatingClickListener(ADD_SOUND));
this.documentButton.setOnClickListener(new PropagatingClickListener(ADD_DOCUMENT));
this.contactButton.setOnClickListener(new PropagatingClickListener(ADD_CONTACT_INFO));
this.cameraButton.setOnClickListener(new PropagatingClickListener(TAKE_PHOTO));
this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION));
this.gifButton.setOnClickListener(new PropagatingClickListener(ADD_GIF));
this.closeButton.setOnClickListener(new CloseClickListener());
this.recentRail.setListener(new RecentPhotoSelectedListener());
setContentView(layout);
setWidth(LinearLayout.LayoutParams.MATCH_PARENT);
setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
setBackgroundDrawable(new BitmapDrawable());
setAnimationStyle(0);
setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
setFocusable(true);
setTouchable(true);
loaderManager.initLoader(1, null, recentRail);
}
public void show(@NonNull Activity activity, final @NonNull View anchor) {
if (Permissions.hasAll(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
recentRail.setVisibility(View.VISIBLE);
loaderManager.restartLoader(1, null, recentRail);
} else {
recentRail.setVisibility(View.GONE);
}
this.currentAnchor = anchor;
showAtLocation(anchor, Gravity.BOTTOM, 0, 0);
getContentView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowInCircular(anchor, getContentView());
} else {
animateWindowInTranslate(getContentView());
}
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateButtonIn(imageButton, ANIMATION_DURATION / 2);
animateButtonIn(cameraButton, ANIMATION_DURATION / 2);
animateButtonIn(audioButton, ANIMATION_DURATION / 3);
animateButtonIn(locationButton, ANIMATION_DURATION / 3);
animateButtonIn(documentButton, ANIMATION_DURATION / 4);
animateButtonIn(gifButton, ANIMATION_DURATION / 4);
animateButtonIn(contactButton, 0);
animateButtonIn(closeButton, 0);
}
}
@Override
public void dismiss() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
animateWindowOutCircular(currentAnchor, getContentView());
} else {
animateWindowOutTranslate(getContentView());
}
}
public void setListener(@Nullable AttachmentClickedListener listener) {
this.listener = listener;
}
private void animateButtonIn(View button, int delay) {
AnimationSet animation = new AnimationSet(true);
Animation scale = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f);
animation.addAnimation(scale);
animation.setInterpolator(new OvershootInterpolator(1));
animation.setDuration(ANIMATION_DURATION);
animation.setStartOffset(delay);
button.startAnimation(animation);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) {
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
Animator animator = ViewAnimationUtils.createCircularReveal(contentView,
coordinates.first,
coordinates.second,
0,
Math.max(contentView.getWidth(), contentView.getHeight()));
animator.setDuration(ANIMATION_DURATION);
animator.start();
}
private void animateWindowInTranslate(@NonNull View contentView) {
Animation animation = new TranslateAnimation(0, 0, contentView.getHeight(), 0);
animation.setDuration(ANIMATION_DURATION);
getContentView().startAnimation(animation);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) {
Pair<Integer, Integer> coordinates = getClickOrigin(anchor, contentView);
Animator animator = ViewAnimationUtils.createCircularReveal(getContentView(),
coordinates.first,
coordinates.second,
Math.max(getContentView().getWidth(), getContentView().getHeight()),
0);
animator.setDuration(ANIMATION_DURATION);
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
AttachmentTypeSelector.super.dismiss();
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
}
private void animateWindowOutTranslate(@NonNull View contentView) {
Animation animation = new TranslateAnimation(0, 0, 0, contentView.getTop() + contentView.getHeight());
animation.setDuration(ANIMATION_DURATION);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
AttachmentTypeSelector.super.dismiss();
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
getContentView().startAnimation(animation);
}
private Pair<Integer, Integer> getClickOrigin(@Nullable View anchor, @NonNull View contentView) {
if (anchor == null) return new Pair<>(0, 0);
final int[] anchorCoordinates = new int[2];
anchor.getLocationOnScreen(anchorCoordinates);
anchorCoordinates[0] += anchor.getWidth() / 2;
anchorCoordinates[1] += anchor.getHeight() / 2;
final int[] contentCoordinates = new int[2];
contentView.getLocationOnScreen(contentCoordinates);
int x = anchorCoordinates[0] - contentCoordinates[0];
int y = anchorCoordinates[1] - contentCoordinates[1];
return new Pair<>(x, y);
}
private class RecentPhotoSelectedListener implements RecentPhotoViewRail.OnItemClickedListener {
@Override
public void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) {
animateWindowOutTranslate(getContentView());
if (listener != null) listener.onQuickAttachment(uri, mimeType, bucketId, dateTaken, width, height, size);
}
}
private class PropagatingClickListener implements View.OnClickListener {
private final int type;
private PropagatingClickListener(int type) {
this.type = type;
}
@Override
public void onClick(View v) {
animateWindowOutTranslate(getContentView());
if (listener != null) listener.onClick(type);
}
}
private class CloseClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {
dismiss();
}
}
public interface AttachmentClickedListener {
void onClick(int type);
void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size);
}
}

View File

@@ -6,7 +6,6 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.provider.ContactsContract;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
@@ -16,12 +15,15 @@ import androidx.appcompat.widget.AppCompatImageView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.RecipientPreferenceActivity;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientExporter;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.Objects;
@@ -49,10 +51,11 @@ public final class AvatarImageView extends AppCompatImageView {
DARK_THEME_OUTLINE_PAINT.setAntiAlias(true);
}
private int size;
private boolean inverted;
private Paint outlinePaint;
private OnClickListener listener;
private int size;
private boolean inverted;
private Paint outlinePaint;
private OnClickListener listener;
private Recipient.FallbackPhotoProvider fallbackPhotoProvider;
private @Nullable RecipientContactPhoto recipientContactPhoto;
private @NonNull Drawable unknownRecipientDrawable;
@@ -102,6 +105,19 @@ public final class AvatarImageView extends AppCompatImageView {
super.setOnClickListener(listener);
}
public void setFallbackPhotoProvider(Recipient.FallbackPhotoProvider fallbackPhotoProvider) {
this.fallbackPhotoProvider = fallbackPhotoProvider;
}
public void setRecipient(@NonNull Recipient recipient) {
if (recipient.isLocalNumber()) {
setAvatar(GlideApp.with(this), null, false);
AvatarUtil.loadIconIntoImageView(recipient, this);
} else {
setAvatar(GlideApp.with(this), recipient, false);
}
}
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
if (recipient != null) {
RecipientContactPhoto photo = new RecipientContactPhoto(recipient);
@@ -111,8 +127,8 @@ public final class AvatarImageView extends AppCompatImageView {
recipientContactPhoto = photo;
Drawable fallbackContactPhotoDrawable = size == SIZE_SMALL
? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted)
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted);
? photo.recipient.getSmallFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider)
: photo.recipient.getFallbackContactPhotoDrawable(getContext(), inverted, fallbackPhotoProvider);
if (photo.contactPhoto != null) {
requestManager.load(photo.contactPhoto)
@@ -130,23 +146,24 @@ public final class AvatarImageView extends AppCompatImageView {
} else {
recipientContactPhoto = null;
requestManager.clear(this);
setImageDrawable(unknownRecipientDrawable);
if (fallbackPhotoProvider != null) {
setImageDrawable(fallbackPhotoProvider.getPhotoForRecipientWithoutName()
.asDrawable(getContext(), MaterialColor.STEEL.toAvatarColor(getContext()), inverted));
} else {
setImageDrawable(unknownRecipientDrawable);
}
super.setOnClickListener(listener);
}
}
private void setAvatarClickHandler(final Recipient recipient, boolean quickContactEnabled) {
super.setOnClickListener(v -> {
if (!recipient.isGroup() && quickContactEnabled) {
if (recipient.getContactUri() != null) {
ContactsContract.QuickContact.showQuickContact(getContext(), AvatarImageView.this, recipient.getContactUri(), ContactsContract.QuickContact.MODE_LARGE, null);
} else {
getContext().startActivity(RecipientExporter.export(recipient).asAddContactIntent());
}
} else if (listener != null) {
listener.onClick(v);
}
});
if (quickContactEnabled) {
super.setOnClickListener(v -> getContext().startActivity(RecipientPreferenceActivity.getLaunchIntent(getContext(), recipient.getId())));
} else {
super.setOnClickListener(listener);
setClickable(listener != null);
}
}
private static class RecipientContactPhoto {

View File

@@ -1,20 +1,16 @@
package org.thoughtcrime.securesms.components;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.DimenRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
@@ -24,7 +20,16 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.DimenRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.view.ViewCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
@@ -57,7 +62,8 @@ public class InputPanel extends LinearLayout
private static final String TAG = InputPanel.class.getSimpleName();
private static final int FADE_TIME = 150;
private static final long QUOTE_REVEAL_DURATION_MILLIS = 150;
private static final int FADE_TIME = 150;
private RecyclerView stickerSuggestion;
private QuoteView quoteView;
@@ -73,6 +79,7 @@ public class InputPanel extends LinearLayout
private MicrophoneRecorderView microphoneRecorderView;
private SlideToCancel slideToCancel;
private RecordTime recordTime;
private ValueAnimator quoteAnimator;
private @Nullable Listener listener;
private boolean emojiVisible;
@@ -157,7 +164,20 @@ public class InputPanel extends LinearLayout
@NonNull SlideDeck attachments)
{
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
this.quoteView.setVisibility(View.VISIBLE);
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
: 0;
this.quoteView.setVisibility(VISIBLE);
this.quoteView.measure(0, 0);
if (quoteAnimator != null) {
quoteAnimator.cancel();
}
quoteAnimator = createHeightAnimator(quoteView, originalHeight, this.quoteView.getMeasuredHeight(), null);
quoteAnimator.start();
if (this.linkPreview.getVisibility() == View.VISIBLE) {
int cornerRadius = readDimen(R.dimen.message_corner_collapse_radius);
@@ -166,12 +186,44 @@ public class InputPanel extends LinearLayout
}
public void clearQuote() {
this.quoteView.dismiss();
if (this.linkPreview.getVisibility() == View.VISIBLE) {
int cornerRadius = readDimen(R.dimen.message_corner_radius);
this.linkPreview.setCorners(cornerRadius, cornerRadius);
if (quoteAnimator != null) {
quoteAnimator.cancel();
}
quoteAnimator = createHeightAnimator(quoteView, quoteView.getMeasuredHeight(), 0, new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
quoteView.dismiss();
if (linkPreview.getVisibility() == View.VISIBLE) {
int cornerRadius = readDimen(R.dimen.message_corner_radius);
linkPreview.setCorners(cornerRadius, cornerRadius);
}
}
});
quoteAnimator.start();
}
private static ValueAnimator createHeightAnimator(@NonNull View view,
int originalHeight,
int finalHeight,
@Nullable AnimationCompleteListener onAnimationComplete)
{
ValueAnimator animator = ValueAnimator.ofInt(originalHeight, finalHeight)
.setDuration(QUOTE_REVEAL_DURATION_MILLIS);
animator.addUpdateListener(animation -> {
ViewGroup.LayoutParams params = view.getLayoutParams();
params.height = (int) animation.getAnimatedValue();
view.setLayoutParams(params);
});
if (onAnimationComplete != null) {
animator.addListener(onAnimationComplete);
}
return animator;
}
public Optional<QuoteModel> getQuote() {

View File

@@ -0,0 +1,40 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.HorizontalScrollView;
import androidx.annotation.Nullable;
/**
* Unfortunately {@link HorizontalScrollView#setOnScrollChangeListener(OnScrollChangeListener)}
* wasn't added until API 23, so now we have to do this ourselves.
*/
public class ListenableHorizontalScrollView extends HorizontalScrollView {
private OnScrollListener listener;
public ListenableHorizontalScrollView(Context context) {
super(context);
}
public ListenableHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setOnScrollListener(@Nullable OnScrollListener listener) {
this.listener = listener;
}
@Override
protected void onScrollChanged(int newLeft, int newTop, int oldLeft, int oldTop) {
if (listener != null) {
listener.onScroll(newLeft, oldLeft);
}
super.onScrollChanged(newLeft, newTop, oldLeft, oldTop);
}
public interface OnScrollListener {
void onScroll(int newLeft, int oldLeft);
}
}

View File

@@ -21,6 +21,7 @@ public class MaskView extends View {
private ViewGroup activityContentView;
private Paint maskPaint;
private Rect drawingRect = new Rect();
private float targetParentTranslationY;
private final ViewTreeObserver.OnDrawListener onDrawListener = this::invalidate;
@@ -63,18 +64,26 @@ public class MaskView extends View {
invalidate();
}
public void setTargetParentTranslationY(float targetParentTranslationY) {
this.targetParentTranslationY = targetParentTranslationY;
}
@Override
protected void onDraw(@NonNull Canvas canvas) {
super.onDraw(canvas);
if (target == null) {
if (target == null || !target.isAttachedToWindow()) {
return;
}
target.getDrawingRect(drawingRect);
activityContentView.offsetDescendantRectToMyCoords(target, drawingRect);
Bitmap mask = Bitmap.createBitmap(target.getWidth(), target.getHeight(), Bitmap.Config.ARGB_8888);
drawingRect.bottom = Math.min(drawingRect.bottom, getBottom() - getPaddingBottom());
drawingRect.top += targetParentTranslationY;
drawingRect.bottom += targetParentTranslationY;
Bitmap mask = Bitmap.createBitmap(target.getWidth(), drawingRect.height(), Bitmap.Config.ARGB_8888);
Canvas maskCanvas = new Canvas(mask);
target.draw(maskCanvas);

View File

@@ -1,10 +1,14 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import android.graphics.Color;
import android.util.AttributeSet;
import org.thoughtcrime.securesms.R;
@@ -21,20 +25,30 @@ public class OutlinedThumbnailView extends ThumbnailView {
public OutlinedThumbnailView(Context context) {
super(context);
init();
init(null);
}
public OutlinedThumbnailView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
init(attrs);
}
private void init() {
private void init(@Nullable AttributeSet attrs) {
cornerMask = new CornerMask(this);
outliner = new Outliner();
outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color));
setRadius(0);
int radius = 0;
if (attrs != null) {
TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.OutlinedThumbnailView, 0, 0);
radius = typedArray.getDimensionPixelOffset(R.styleable.OutlinedThumbnailView_otv_cornerRadius, 0);
}
setRadius(radius);
setCorners(radius, radius, radius, radius);
setWillNotDraw(false);
}

View File

@@ -6,9 +6,6 @@ import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
@@ -17,6 +14,10 @@ import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import com.annimon.stream.Stream;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
@@ -248,6 +249,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
}
glideRequests.load(new DecryptableUri(imageVideoSlides.get(0).getThumbnailUri()))
.centerCrop()
.override(getContext().getResources().getDimensionPixelSize(R.dimen.quote_thumb_size))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(thumbnailView);
} else if (!documentSlides.isEmpty()){

View File

@@ -90,9 +90,9 @@ public final class RecyclerViewFastScroller extends LinearLayout {
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
if (event.getX() < ViewUtil.getX(handle) - handle.getPaddingLeft() ||
event.getY() < ViewUtil.getY(handle) - handle.getPaddingTop() ||
event.getY() > ViewUtil.getY(handle) + handle.getHeight() + handle.getPaddingBottom())
if (event.getX() < handle.getX() - handle.getPaddingLeft() ||
event.getY() < handle.getY() - handle.getPaddingTop() ||
event.getY() > handle.getY() + handle.getHeight() + handle.getPaddingBottom())
{
return false;
}
@@ -150,9 +150,9 @@ public final class RecyclerViewFastScroller extends LinearLayout {
if (recyclerView != null) {
final int itemCount = recyclerView.getAdapter().getItemCount();
float proportion;
if (ViewUtil.getY(handle) == 0) {
if (handle.getY() == 0) {
proportion = 0f;
} else if (ViewUtil.getY(handle) + handle.getHeight() >= height - TRACK_SNAP_RANGE) {
} else if (handle.getY() + handle.getHeight() >= height - TRACK_SNAP_RANGE) {
proportion = 1f;
} else {
proportion = y / (float)height;
@@ -169,10 +169,10 @@ public final class RecyclerViewFastScroller extends LinearLayout {
final int handleHeight = handle.getHeight();
final int bubbleHeight = bubble.getHeight();
final int handleY = Util.clamp((int)((height - handleHeight) * y), 0, height - handleHeight);
ViewUtil.setY(handle, handleY);
ViewUtil.setY(bubble, Util.clamp(handleY - bubbleHeight - bubble.getPaddingBottom() + handleHeight,
0,
height - bubbleHeight));
handle.setY(handleY);
bubble.setY(Util.clamp(handleY - bubbleHeight - bubble.getPaddingBottom() + handleHeight,
0,
height - bubbleHeight));
}
private void showBubble() {

View File

@@ -344,6 +344,10 @@ public class ThumbnailView extends FrameLayout {
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
return setImageResource(glideRequests, uri, 0, 0);
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height) {
SettableFuture<Boolean> future = new SettableFuture<>();
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
@@ -352,6 +356,10 @@ public class ThumbnailView extends FrameLayout {
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(withCrossFade());
if (width > 0 && height > 0) {
request = request.override(width, height);
}
if (radius > 0) {
request = request.transforms(new CenterCrop(), new RoundedCorners(radius));
} else {

View File

@@ -11,7 +11,6 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;

View File

@@ -133,7 +133,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener);
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, true);
page.setModel(pages.get(position));
container.addView(page);
return page;

View File

@@ -26,7 +26,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
public EmojiPageView(@NonNull Context context,
@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener)
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations)
{
super(context);
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
@@ -40,7 +41,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
popup,
emojiSelectionListener,
this);
this,
allowVariations);
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
@@ -83,6 +85,10 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
}
}
public void setRecyclerNestedScrollingEnabled(boolean enabled) {
recyclerView.setNestedScrollingEnabled(enabled);
}
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {

View File

@@ -22,17 +22,20 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
private final EmojiVariationSelectorPopup popup;
private final VariationSelectorListener variationSelectorListener;
private final EmojiEventListener emojiEventListener;
private final boolean allowVariations;
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
@NonNull EmojiVariationSelectorPopup popup,
@NonNull EmojiEventListener emojiEventListener,
@NonNull VariationSelectorListener variationSelectorListener)
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations)
{
this.emojiList = new ArrayList<>();
this.emojiProvider = emojiProvider;
this.popup = popup;
this.emojiEventListener = emojiEventListener;
this.variationSelectorListener = variationSelectorListener;
this.allowVariations = allowVariations;
popup.setOnDismissListener(this);
}
@@ -65,7 +68,7 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageView
emojiEventListener.onEmojiSelected(emoji.getValue());
});
if (emoji.getVariations().size() > 1) {
if (allowVariations && emoji.getVariations().size() > 1) {
viewHolder.itemView.setOnLongClickListener(v -> {
popup.dismiss();
popup.setVariations(emoji.getVariations());

File diff suppressed because one or more lines are too long

View File

@@ -41,7 +41,7 @@ class EmojiProvider {
private static final int EMOJI_RAW_HEIGHT = 64;
private static final int EMOJI_RAW_WIDTH = 64;
private static final int EMOJI_VERT_PAD = 0;
private static final int EMOJI_PER_ROW = 32;
private static final int EMOJI_PER_ROW = 16;
private final float decodeScale;
private final float verticalPad;

View File

@@ -23,6 +23,7 @@ import org.whispersystems.libsignal.util.guava.Optional;
public class EmojiTextView extends AppCompatTextView {
private final boolean scaleEmojis;
private final boolean forceCustom;
private static final char ELLIPSIS = '…';
@@ -49,6 +50,7 @@ public class EmojiTextView extends AppCompatTextView {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
a.recycle();
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
@@ -166,7 +168,7 @@ public class EmojiTextView extends AppCompatTextView {
}
private boolean useSystemEmoji() {
return TextSecurePreferences.isSystemEmojiPreferred(getContext());
return !forceCustom && TextSecurePreferences.isSystemEmojiPreferred(getContext());
}
@Override

View File

@@ -0,0 +1,75 @@
package org.thoughtcrime.securesms.components.emoji;
import androidx.annotation.NonNull;
import org.whispersystems.libsignal.util.Pair;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class EmojiUtil {
private static final Map<String, String> VARIATION_MAP = new HashMap<>();
static {
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
for (Emoji emoji : page.getDisplayEmoji()) {
for (String variation : emoji.getVariations()) {
VARIATION_MAP.put(variation, emoji.getValue());
}
}
}
}
public static final int MAX_EMOJI_LENGTH;
static {
int max = 0;
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
for (String emoji : page.getEmoji()) {
max = Math.max(max, emoji.length());
}
}
MAX_EMOJI_LENGTH = max;
}
private EmojiUtil() {}
public static List<EmojiPageModel> getDisplayPages() {
return EmojiPages.DISPLAY_PAGES;
}
/**
* This will return all ways we know of expressing a singular emoji. This is to aid in search,
* where some platforms may send an emoji we've locally marked as 'obsolete'.
*/
public static @NonNull Set<String> getAllRepresentations(@NonNull String emoji) {
Set<String> out = new HashSet<>();
out.add(emoji);
for (Pair<String, String> pair : EmojiPages.OBSOLETE) {
if (pair.first().equals(emoji)) {
out.add(pair.second());
} else if (pair.second().equals(emoji)) {
out.add(pair.first());
}
}
return out;
}
/**
* When provided an emoji that is a skin variation of another, this will return the default yellow
* version. This is to aid in search, so using a variation will still find all emojis tagged with
* the default version.
*
* If the emoji has no skin variations, this function will return the original emoji.
*/
public static @NonNull String getCanonicalRepresentation(@NonNull String emoji) {
String canonical = VARIATION_MAP.get(emoji);
return canonical != null ? canonical : emoji;
}
}

View File

@@ -25,7 +25,7 @@ public class EmojiVariationSelectorPopup extends PopupWindow {
ViewGroup.LayoutParams.WRAP_CONTENT);
this.context = context;
this.listener = listener;
this.list = (ViewGroup) getContentView();
this.list = (ViewGroup) getContentView().findViewById(R.id.emoji_variation_container);
setBackgroundDrawable(null);
setOutsideTouchable(true);

View File

@@ -0,0 +1,59 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import java.util.List;
final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.AudioOutputViewHolder> {
private final Consumer<WebRtcAudioOutput> consumer;
private final List<WebRtcAudioOutput> audioOutputs;
AudioOutputAdapter(@NonNull Consumer<WebRtcAudioOutput> consumer, @NonNull List<WebRtcAudioOutput> audioOutputs) {
this.audioOutputs = audioOutputs;
this.consumer = consumer;
}
@Override
public @NonNull AudioOutputViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new AudioOutputViewHolder((TextView) LayoutInflater.from(parent.getContext()).inflate(R.layout.audio_output_adapter_item, parent, false), consumer);
}
@Override
public void onBindViewHolder(@NonNull AudioOutputViewHolder holder, int position) {
WebRtcAudioOutput audioOutput = audioOutputs.get(position);
holder.view.setText(audioOutput.getLabelRes());
holder.view.setCompoundDrawablesRelativeWithIntrinsicBounds(audioOutput.getIconRes(), 0, 0, 0);
}
@Override
public int getItemCount() {
return audioOutputs.size();
}
final static class AudioOutputViewHolder extends RecyclerView.ViewHolder {
private final TextView view;
AudioOutputViewHolder(@NonNull TextView itemView, @NonNull Consumer<WebRtcAudioOutput> consumer) {
super(itemView);
view = itemView;
itemView.setOnClickListener(v -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
consumer.accept(WebRtcAudioOutput.values()[getAdapterPosition()]);
}
});
}
}
}

View File

@@ -0,0 +1,284 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.animation.Animator;
import android.annotation.SuppressLint;
import android.graphics.Point;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import androidx.annotation.NonNull;
import androidx.core.view.GestureDetectorCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
import java.util.Arrays;
public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestureListener {
private static final float DECELERATION_RATE = 0.99f;
private final ViewGroup parent;
private final View child;
private final int framePadding;
private final int pipWidth;
private final int pipHeight;
private int activePointerId = MotionEvent.INVALID_POINTER_ID;
private float lastTouchX;
private float lastTouchY;
private boolean isDragging;
private boolean isAnimating;
private int extraPaddingTop;
private int extraPaddingBottom;
private double projectionX;
private double projectionY;
private VelocityTracker velocityTracker;
private int maximumFlingVelocity;
@SuppressLint("ClickableViewAccessibility")
public static PictureInPictureGestureHelper applyTo(@NonNull View child) {
TouchInterceptingFrameLayout parent = (TouchInterceptingFrameLayout) child.getParent();
PictureInPictureGestureHelper helper = new PictureInPictureGestureHelper(parent, child);
GestureDetectorCompat gestureDetector = new GestureDetectorCompat(child.getContext(), helper);
parent.setOnInterceptTouchEventListener((event) -> {
if (helper.velocityTracker == null) {
helper.velocityTracker = VelocityTracker.obtain();
}
helper.velocityTracker.addMovement(event);
return false;
});
parent.setOnTouchListener((v, event) -> {
if (helper.velocityTracker != null) {
helper.velocityTracker.recycle();
helper.velocityTracker = null;
}
return false;
});
child.setOnTouchListener((v, event) -> {
boolean handled = gestureDetector.onTouchEvent(event);
if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
if (!handled) {
handled = helper.onGestureFinished(event);
}
if (helper.velocityTracker != null) {
helper.velocityTracker.recycle();
helper.velocityTracker = null;
}
}
return handled;
});
return helper;
}
private PictureInPictureGestureHelper(@NonNull ViewGroup parent, @NonNull View child) {
this.parent = parent;
this.child = child;
this.framePadding = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_frame_padding);
this.pipWidth = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_width);
this.pipHeight = child.getResources().getDimensionPixelSize(R.dimen.picture_in_picture_gesture_helper_pip_height);
this.maximumFlingVelocity = ViewConfiguration.get(child.getContext()).getScaledMaximumFlingVelocity();
}
public void clearVerticalBoundaries() {
setVerticalBoundaries(0, parent.getMeasuredHeight());
}
public void setVerticalBoundaries(int topBoundary, int bottomBoundary) {
extraPaddingTop = topBoundary;
extraPaddingBottom = parent.getMeasuredHeight() - bottomBoundary;
if (isAnimating) {
fling();
} else if (!isDragging) {
onFling(null, null, 0, 0);
}
}
private boolean onGestureFinished(MotionEvent e) {
final int pointerIndex = e.findPointerIndex(activePointerId);
if (e.getActionIndex() == pointerIndex) {
onFling(e, e, 0, 0);
return true;
}
return false;
}
@Override
public boolean onDown(MotionEvent e) {
activePointerId = e.getPointerId(0);
lastTouchX = e.getX(activePointerId) + child.getX();
lastTouchY = e.getY(activePointerId) + child.getY();
isDragging = true;
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
int pointerIndex = e2.findPointerIndex(activePointerId);
float x = e2.getX(pointerIndex) + child.getX();
float y = e2.getY(pointerIndex) + child.getY();
float dx = x - lastTouchX;
float dy = y - lastTouchY;
child.setTranslationX(child.getTranslationX() + dx);
child.setTranslationY(child.getTranslationY() + dy);
lastTouchX = x;
lastTouchY = y;
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
if (velocityTracker != null) {
velocityTracker.computeCurrentVelocity(1000, maximumFlingVelocity);
projectionX = child.getX() + project(velocityTracker.getXVelocity());
projectionY = child.getY() + project(velocityTracker.getYVelocity());
} else {
projectionX = child.getX();
projectionY = child.getY();
}
fling();
return true;
}
private void fling() {
Point projection = new Point((int) projectionX, (int) projectionY);
Point nearestCornerPosition = findNearestCornerPosition(projection);
isAnimating = true;
isDragging = false;
child.animate()
.translationX(getTranslationXForPoint(nearestCornerPosition))
.translationY(getTranslationYForPoint(nearestCornerPosition))
.setDuration(250)
.setInterpolator(new ViscousFluidInterpolator())
.setListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
isAnimating = false;
}
})
.start();
}
private Point findNearestCornerPosition(Point projection) {
Point maxPoint = null;
double maxDistance = Double.MAX_VALUE;
for (Point point : Arrays.asList(calculateTopLeftCoordinates(),
calculateTopRightCoordinates(parent),
calculateBottomLeftCoordinates(parent),
calculateBottomRightCoordinates(parent)))
{
double distance = distance(point, projection);
if (distance < maxDistance) {
maxDistance = distance;
maxPoint = point;
}
}
return maxPoint;
}
private float getTranslationXForPoint(Point destination) {
return destination.x - child.getLeft();
}
private float getTranslationYForPoint(Point destination) {
return destination.y - child.getTop();
}
private Point calculateTopLeftCoordinates() {
return new Point(framePadding,
framePadding + extraPaddingTop);
}
private Point calculateTopRightCoordinates(@NonNull ViewGroup parent) {
return new Point(parent.getMeasuredWidth() - pipWidth - framePadding,
framePadding + extraPaddingTop);
}
private Point calculateBottomLeftCoordinates(@NonNull ViewGroup parent) {
return new Point(framePadding,
parent.getMeasuredHeight() - pipHeight - framePadding - extraPaddingBottom);
}
private Point calculateBottomRightCoordinates(@NonNull ViewGroup parent) {
return new Point(parent.getMeasuredWidth() - pipWidth - framePadding,
parent.getMeasuredHeight() - pipHeight - framePadding - extraPaddingBottom);
}
private static float project(float initialVelocity) {
return (initialVelocity / 1000f) * DECELERATION_RATE / (1f - DECELERATION_RATE);
}
private static double distance(Point a, Point b) {
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2));
}
/** Borrowed from ScrollView */
private static class ViscousFluidInterpolator implements Interpolator {
/** Controls the viscous fluid effect (how much of it). */
private static final float VISCOUS_FLUID_SCALE = 8.0f;
private static final float VISCOUS_FLUID_NORMALIZE;
private static final float VISCOUS_FLUID_OFFSET;
static {
// must be set to 1.0 (used in viscousFluid())
VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
// account for very small floating-point error
VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
}
private static float viscousFluid(float x) {
x *= VISCOUS_FLUID_SCALE;
if (x < 1.0f) {
x -= (1.0f - (float)Math.exp(-x));
} else {
float start = 0.36787944117f; // 1/e == exp(-1)
x = 1.0f - (float)Math.exp(1.0f - x);
x = start + x * (1.0f - start);
}
return x;
}
@Override
public float getInterpolation(float input) {
final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
if (interpolated > 0) {
return interpolated + VISCOUS_FLUID_OFFSET;
}
return interpolated;
}
}
}

View File

@@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
public enum WebRtcAudioOutput {
HANDSET(R.string.WebRtcAudioOutputToggle__phone, R.drawable.ic_phone_right_black_28),
SPEAKER(R.string.WebRtcAudioOutputToggle__speaker, R.drawable.ic_speaker_solid_black_28),
HEADSET(R.string.WebRtcAudioOutputToggle__bluetooth, R.drawable.ic_speaker_bt_solid_black_28);
private final @StringRes int labelRes;
private final @DrawableRes int iconRes;
WebRtcAudioOutput(@StringRes int labelRes, @DrawableRes int iconRes) {
this.labelRes = labelRes;
this.iconRes = iconRes;
}
public int getIconRes() {
return iconRes;
}
public int getLabelRes() {
return labelRes;
}
}

View File

@@ -0,0 +1,163 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import java.util.Arrays;
import java.util.List;
public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
private static final String STATE_OUTPUT_INDEX = "audio.output.toggle.state.output.index";
private static final String STATE_HEADSET_ENABLED = "audio.output.toggle.state.headset.enabled";
private static final String STATE_PARENT = "audio.output.toggle.state.parent";
private static final int[] OUTPUT_HANDSET = { R.attr.state_handset };
private static final int[] OUTPUT_SPEAKER = { R.attr.state_speaker };
private static final int[] OUTPUT_HEADSET = { R.attr.state_headset };
private static final int[][] OUTPUT_ENUM = { OUTPUT_HANDSET, OUTPUT_SPEAKER, OUTPUT_HEADSET };
private static final List<WebRtcAudioOutput> OUTPUT_MODES = Arrays.asList(WebRtcAudioOutput.HANDSET, WebRtcAudioOutput.SPEAKER, WebRtcAudioOutput.HEADSET);
private static final WebRtcAudioOutput OUTPUT_FALLBACK = WebRtcAudioOutput.HANDSET;
private boolean isHeadsetAvailable;
private int outputIndex;
private OnAudioOutputChangedListener audioOutputChangedListener;
private AlertDialog picker;
public WebRtcAudioOutputToggleButton(Context context) {
this(context, null);
}
public WebRtcAudioOutputToggleButton(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public WebRtcAudioOutputToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
super.setOnClickListener((v) -> {
if (isHeadsetAvailable) showPicker();
else setAudioOutput(OUTPUT_MODES.get((outputIndex + 1) % OUTPUT_ENUM.length));
});
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
final int[] extra = OUTPUT_ENUM[outputIndex];
final int[] drawableState = super.onCreateDrawableState(extraSpace + extra.length);
mergeDrawableStates(drawableState, extra);
return drawableState;
}
@Override
public void setOnClickListener(@Nullable OnClickListener l) {
throw new UnsupportedOperationException("This View does not support custom click listeners.");
}
public void setIsHeadsetAvailable(boolean isHeadsetAvailable) {
this.isHeadsetAvailable = isHeadsetAvailable;
setAudioOutput(OUTPUT_MODES.get(outputIndex));
}
public void setAudioOutput(@NonNull WebRtcAudioOutput audioOutput) {
int oldIndex = outputIndex;
outputIndex = resolveAudioOutputIndex(OUTPUT_MODES.indexOf(audioOutput), isHeadsetAvailable);
if (oldIndex != outputIndex) {
refreshDrawableState();
notifyListener();
}
}
public void setOnAudioOutputChangedListener(@Nullable OnAudioOutputChangedListener listener) {
this.audioOutputChangedListener = listener;
}
private void showPicker() {
RecyclerView rv = new RecyclerView(getContext());
rv.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
rv.setAdapter(new AudioOutputAdapter(this::setAudioOutputViaDialog, OUTPUT_MODES));
picker = new AlertDialog.Builder(getContext())
.setView(rv)
.show();
}
private void hidePicker() {
if (picker != null) {
picker.dismiss();
picker = null;
}
}
@Override
protected Parcelable onSaveInstanceState() {
Parcelable parentState = super.onSaveInstanceState();
Bundle bundle = new Bundle();
bundle.putParcelable(STATE_PARENT, parentState);
bundle.putInt(STATE_OUTPUT_INDEX, outputIndex);
bundle.putBoolean(STATE_HEADSET_ENABLED, isHeadsetAvailable);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle savedState = (Bundle) state;
isHeadsetAvailable = savedState.getBoolean(STATE_HEADSET_ENABLED);
setAudioOutput(OUTPUT_MODES.get(
resolveAudioOutputIndex(savedState.getInt(STATE_OUTPUT_INDEX), isHeadsetAvailable))
);
super.onRestoreInstanceState(savedState.getParcelable(STATE_PARENT));
} else {
super.onRestoreInstanceState(state);
}
}
private void notifyListener() {
if (audioOutputChangedListener == null) return;
audioOutputChangedListener.audioOutputChanged(OUTPUT_MODES.get(outputIndex));
}
private void setAudioOutputViaDialog(@NonNull WebRtcAudioOutput audioOutput) {
setAudioOutput(audioOutput);
hidePicker();
}
private static int resolveAudioOutputIndex(int desiredAudioOutputIndex, boolean isHeadsetAvailable) {
if (isIllegalAudioOutputIndex(desiredAudioOutputIndex)) {
throw new IllegalArgumentException("Unsupported index: " + desiredAudioOutputIndex);
}
if (isUnsupportedAudioOutput(desiredAudioOutputIndex, isHeadsetAvailable)) {
return OUTPUT_MODES.indexOf(OUTPUT_FALLBACK);
}
return desiredAudioOutputIndex;
}
private static boolean isIllegalAudioOutputIndex(int desiredFlashIndex) {
return desiredFlashIndex < 0 || desiredFlashIndex > OUTPUT_ENUM.length;
}
private static boolean isUnsupportedAudioOutput(int desiredAudioOutputIndex, boolean isHeadsetAvailable) {
return OUTPUT_MODES.get(desiredAudioOutputIndex) == WebRtcAudioOutput.HEADSET && !isHeadsetAvailable;
}
public interface OnAudioOutputChangedListener {
void audioOutputChanged(WebRtcAudioOutput audioOutput);
}
}

View File

@@ -1,217 +0,0 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioManager;
import android.os.Build;
import androidx.annotation.NonNull;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import com.tomergoldst.tooltips.ToolTip;
import com.tomergoldst.tooltips.ToolTipsManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
public class WebRtcCallControls extends LinearLayout {
private static final String TAG = WebRtcCallControls.class.getSimpleName();
private AccessibleToggleButton audioMuteButton;
private AccessibleToggleButton videoMuteButton;
private AccessibleToggleButton speakerButton;
private AccessibleToggleButton bluetoothButton;
private AccessibleToggleButton cameraFlipButton;
private boolean cameraFlipAvailable;
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize();
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public WebRtcCallControls(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
public WebRtcCallControls(Context context, AttributeSet attrs) {
super(context, attrs);
initialize();
}
public WebRtcCallControls(Context context) {
super(context);
initialize();
}
private void initialize() {
LayoutInflater inflater = (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
inflater.inflate(R.layout.webrtc_call_controls, this, true);
this.speakerButton = ViewUtil.findById(this, R.id.speakerButton);
this.bluetoothButton = ViewUtil.findById(this, R.id.bluetoothButton);
this.audioMuteButton = ViewUtil.findById(this, R.id.muteButton);
this.videoMuteButton = ViewUtil.findById(this, R.id.video_mute_button);
this.cameraFlipButton = ViewUtil.findById(this, R.id.camera_flip_button);
}
public void setAudioMuteButtonListener(final MuteButtonListener listener) {
audioMuteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
listener.onToggle(b);
}
});
}
public void setVideoMuteButtonListener(final MuteButtonListener listener) {
videoMuteButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
boolean videoMuted = !isChecked;
listener.onToggle(videoMuted);
cameraFlipButton.setVisibility(!videoMuted && cameraFlipAvailable ? View.VISIBLE : View.GONE);
}
});
}
public void setCameraFlipButtonListener(final CameraFlipButtonListener listener) {
cameraFlipButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onToggle();
cameraFlipButton.setEnabled(false);
}
});
}
public void setSpeakerButtonListener(final SpeakerButtonListener listener) {
speakerButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onSpeakerChange(isChecked);
}
});
}
public void setBluetoothButtonListener(final BluetoothButtonListener listener) {
bluetoothButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
listener.onBluetoothChange(isChecked);
}
});
}
public void updateAudioState(boolean isBluetoothAvailable) {
AudioManager audioManager = ServiceUtil.getAudioManager(getContext());
if (!isBluetoothAvailable) {
bluetoothButton.setVisibility(View.GONE);
} else {
bluetoothButton.setVisibility(View.VISIBLE);
}
if (audioManager.isBluetoothScoOn()) {
bluetoothButton.setChecked(true, false);
speakerButton.setChecked(false, false);
} else if (audioManager.isSpeakerphoneOn()) {
speakerButton.setChecked(true, false);
bluetoothButton.setChecked(false, false);
} else {
speakerButton.setChecked(false, false);
bluetoothButton.setChecked(false, false);
}
}
public boolean isVideoEnabled() {
return videoMuteButton.isChecked();
}
public void setVideoEnabled(boolean enabled) {
videoMuteButton.setChecked(enabled, false);
}
public void setVideoAvailable(boolean available) {
videoMuteButton.setVisibility(available ? VISIBLE : GONE);
}
public void setCameraFlipButtonEnabled(boolean enabled) {
cameraFlipButton.setChecked(enabled, false);
}
public void setCameraFlipAvailable(boolean available) {
cameraFlipAvailable = available;
cameraFlipButton.setVisibility(cameraFlipAvailable && isVideoEnabled() ? View.VISIBLE : View.GONE);
}
public void setCameraFlipClickable(boolean clickable) {
setControlEnabled(cameraFlipButton, clickable);
}
public void setMicrophoneEnabled(boolean enabled) {
audioMuteButton.setChecked(!enabled, false);
}
public void setControlsEnabled(boolean enabled) {
setControlEnabled(speakerButton, enabled);
setControlEnabled(bluetoothButton, enabled);
setControlEnabled(videoMuteButton, enabled);
setControlEnabled(cameraFlipButton, enabled);
setControlEnabled(audioMuteButton, enabled);
}
private void setControlEnabled(@NonNull View view, boolean enabled) {
if (enabled) {
view.setAlpha(1.0f);
view.setEnabled(true);
} else {
view.setAlpha(0.3f);
view.setEnabled(false);
}
}
public void displayVideoTooltip(ViewGroup viewGroup) {
if (videoMuteButton.getVisibility() == VISIBLE) {
final ToolTipsManager toolTipsManager = new ToolTipsManager();
ToolTip toolTip = new ToolTip.Builder(getContext(), videoMuteButton, viewGroup,
getContext().getString(R.string.WebRtcCallControls_tap_to_enable_your_video),
ToolTip.POSITION_BELOW).build();
toolTipsManager.show(toolTip);
videoMuteButton.postDelayed(() -> toolTipsManager.findAndDismiss(videoMuteButton), 4000);
}
}
public static interface MuteButtonListener {
public void onToggle(boolean isMuted);
}
public static interface CameraFlipButtonListener {
public void onToggle();
}
public static interface SpeakerButtonListener {
public void onSpeakerChange(boolean isSpeaker);
}
public static interface BluetoothButtonListener {
public void onBluetoothChange(boolean isBluetooth);
}
}

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.media.AudioManager;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.ServiceUtil;
class WebRtcCallRepository {
private final AudioManager audioManager;
WebRtcCallRepository() {
this.audioManager = ServiceUtil.getAudioManager(ApplicationDependencies.getApplication());
}
WebRtcAudioOutput getAudioOutput() {
if (audioManager.isBluetoothScoOn()) {
return WebRtcAudioOutput.HEADSET;
} else if (audioManager.isSpeakerphoneOn()) {
return WebRtcAudioOutput.SPEAKER;
} else {
return WebRtcAudioOutput.HANDSET;
}
}
}

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