Compare commits

...

221 Commits

Author SHA1 Message Date
Greyson Parrelli
4f4aea22ce Bump version to 5.2.2 2021-01-16 21:27:38 -05:00
Greyson Parrelli
e0ea2bdde4 Updated language translations. 2021-01-16 21:27:14 -05:00
Greyson Parrelli
d40dc1d90b Bump signal-client-java version to 0.1.5 2021-01-16 21:11:42 -05:00
Greyson Parrelli
4571151e3c Revert "Remove reset session button."
This reverts commit f24020e7b7.
2021-01-16 21:11:42 -05:00
Greyson Parrelli
3e43963f67 Put receipts in the recipient's queue. 2021-01-16 21:11:42 -05:00
Greyson Parrelli
fe71d6ac41 Make outage banner color less aggressive. 2021-01-16 21:11:42 -05:00
Greyson Parrelli
0514950333 Feature flag OkHttp automatic network retry. 2021-01-16 21:11:42 -05:00
Greyson Parrelli
a2dc781840 Add an automatic session reset interval. 2021-01-16 21:11:42 -05:00
Greyson Parrelli
2c1c6fab35 Bump version to 5.2.1 2021-01-16 03:41:29 -05:00
Greyson Parrelli
3c2e428c54 Updated language translations. 2021-01-16 03:41:29 -05:00
Greyson Parrelli
8f7fe5c3ee Add jitter to job exponential backoff. 2021-01-16 03:41:29 -05:00
Greyson Parrelli
93e9dd6425 Feature flag the default max backoff interval. 2021-01-16 03:06:54 -05:00
Greyson Parrelli
c95f0fce6e Handle ServerRejectedException.
Handle an exception that indicates we should halt retries.
2021-01-16 02:32:09 -05:00
Greyson Parrelli
a3c7e7e552 Feature flag automatic session reset. 2021-01-16 02:05:43 -05:00
Greyson Parrelli
1e2590af49 Lock the threadId during message send.
Fixes #10659
2021-01-15 12:15:07 -05:00
Greyson Parrelli
562e608e1f Fix issue with previously-enqueued bad encrypted messages. 2021-01-15 11:50:50 -05:00
Greyson Parrelli
417d5a2804 Be extra safe when posting a notification during a migration. 2021-01-15 11:22:15 -05:00
Ewout ter Hoeven
c0c8d2caa7 Update issue template.
Fixes #10626

Co-authored-by: Greyson Parrelli <greyson@signal.org>
2021-01-15 11:17:38 -05:00
Greyson Parrelli
727175e4f4 Add 'constraints' and 'key preferences' sections to logs. 2021-01-15 11:17:38 -05:00
Ewout ter Hoeven
577d2b13ca CI: Update to checkout v2, remove install NDK
- Updates to action/checkout v2, which is faster
 - Remove install NDK step, since it's installed by default and speeds up the build
2021-01-14 12:44:08 -04:00
Greyson Parrelli
6ac2f922e2 Fix capitalization in some strings. 2021-01-14 10:47:42 -05:00
Greyson Parrelli
98297e55c1 Don't show menu actions for chat refresh messages. 2021-01-14 10:46:09 -05:00
Alan Evans
aa2094a2cc Fix group recipient showing in verify safety number change "learn more". 2021-01-14 10:19:50 -04:00
Alex Hart
f8c053cc96 Add 'on another device' to participants description 2021-01-14 07:03:19 -04:00
Alex Hart
790f8426ac Fix issue when single user leaves ParticipantCollection. 2021-01-14 06:53:18 -04:00
Greyson Parrelli
ff11609a82 Bump version to 5.2.0 2021-01-13 19:57:58 -05:00
Greyson Parrelli
94346033a8 Updated language translations. 2021-01-13 19:57:35 -05:00
Alan Evans
cb1401f556 Prompt to confirm number before SMS or call. 2021-01-13 19:43:35 -05:00
Alan Evans
ae676d7486 Fast job sorting. 2021-01-13 19:43:35 -05:00
Alan Evans
2d39e43677 Restrict group names to 32 graphemes.
Uses some code from #10132 hence co-author:

Co-authored-by: Fumiaki Yoshimatsu <fumiakiy@gmail.com>
2021-01-13 19:43:35 -05:00
Alex Hart
0ccc7e3c06 Distinguish between primary and secondary devices in participants list. 2021-01-13 19:43:23 -05:00
Alex Hart
2d20ceea01 Show contact profile photo instead of system contact. 2021-01-13 19:43:23 -05:00
Alex Hart
cee2702fdf Add expandable video pip to 1:1 conversations. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
6c94be70dc Update safety number UI. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
f24020e7b7 Remove reset session button. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
728f1707b6 Automatically recover from bad encrypted messages. 2021-01-13 19:43:23 -05:00
Alan Evans
adea15df10 Recover from CDN 416 Range error on attachment download. 2021-01-13 19:43:23 -05:00
Alex Hart
be91f2396c Add toggle to control call bandwidth. 2021-01-13 19:43:23 -05:00
Alex Hart
8724d904b7 Add NotInCallConstraint, restrict auto-download of media and documents when on an active voice or video call. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
ef95479157 Increase versionCode postFixSize from 10 to 100. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
710cd23537 Fix typo in log. 2021-01-13 19:43:23 -05:00
Alex Hart
0af313a81f Add correct margin to in-call menu item. 2021-01-13 19:43:23 -05:00
Alex Hart
71be388989 Order grid by latest speakers and prevent any unnecessary shifts. 2021-01-13 19:43:23 -05:00
Alex Hart
db3098f633 Add immersive mode for calling. 2021-01-13 19:43:23 -05:00
Greyson Parrelli
ac197f42f2 Bump version to 5.1.9 2021-01-13 17:39:05 -05:00
Greyson Parrelli
d82882ba28 Updated language translations. 2021-01-13 17:38:35 -05:00
Greyson Parrelli
957a12875d Fix situations where we might not have detected first-ever-launch. 2021-01-13 17:33:14 -05:00
Greyson Parrelli
796eb5043c Bump version to 5.1.8 2021-01-12 12:47:57 -05:00
Greyson Parrelli
4f8d86828f Updated language translations. 2021-01-12 12:47:57 -05:00
Greyson Parrelli
5370605815 Control CDS refresh interval with a feature flag. 2021-01-12 12:47:57 -05:00
Greyson Parrelli
d5fb71b63f Prevent creating threads for remapped users.
Fixes #10538
2021-01-12 11:41:13 -05:00
Greyson Parrelli
2455c291d8 Bump version to 5.1.7 2021-01-12 02:06:59 -05:00
Greyson Parrelli
80ad28e9cc Updated language translations. 2021-01-12 02:06:00 -05:00
Greyson Parrelli
74552ba545 Fix possible crash with ProcessLifecycleOwner. 2021-01-12 02:06:00 -05:00
Greyson Parrelli
141cab1105 Perfom a migration to notify users of new contacts. 2021-01-11 23:22:01 -05:00
Greyson Parrelli
f012a41345 Fix issue with Signal join notifications. 2021-01-11 23:21:54 -05:00
Alan Evans
1f95df60d4 Fix style of approve new member switch in light bottom sheet. 2021-01-11 19:07:27 -04:00
Alan Evans
560c8c8cac Bump version to 5.1.6 2021-01-11 17:27:32 -04:00
Alan Evans
7cd79f8a94 Updated language translations. 2021-01-11 17:27:32 -04:00
Greyson Parrelli
667304c81e Cause LiveRecipient.refresh() to force a LiveData change. 2021-01-11 17:18:46 -04:00
Greyson Parrelli
2dd95c6ef6 Increase profile timeouts. 2021-01-11 17:18:46 -04:00
Greyson Parrelli
29e66e1d47 Fix the invite share button. 2021-01-11 17:18:46 -04:00
Alan Evans
5eb5af2f87 Bump version to 5.1.5 2021-01-11 14:13:02 -04:00
Alan Evans
e47b62805b Updated language translations. 2021-01-11 14:13:02 -04:00
Alan Evans
57adc73e95 Revert "Fast job sorting."
This reverts commit 373972f5dc.
2021-01-11 13:59:01 -04:00
Greyson Parrelli
8f4d64d37a Update link preview user agent. 2021-01-11 13:46:35 -04:00
Alan Evans
9ce3813044 Add "Enter your phone number" string for translation. 2021-01-11 13:46:35 -04:00
Alan Evans
6436e2836d No cell service hint during registration. 2021-01-11 13:46:35 -04:00
Alan Evans
77c83019d0 Smaller titles on small screen registration. 2021-01-11 13:46:35 -04:00
Greyson Parrelli
e6dfe96569 Add a gradient and background to the onboarding megaphone. 2021-01-11 13:46:35 -04:00
Alan Evans
5d515198e6 Fix initial state for update button. 2021-01-10 11:47:59 -04:00
Greyson Parrelli
1d912c0db2 Fix issue where conversation hero avatars didn't show up. 2021-01-10 10:01:31 -05:00
Greyson Parrelli
bac04dea8d Bump version to 5.1.4 2021-01-09 23:45:05 -05:00
Greyson Parrelli
3b39d13412 Fix possible crash with ProcessLifecycleObserver. 2021-01-09 23:41:31 -05:00
Greyson Parrelli
9838b2cf0a Fix crash in ContactSelectionListFragment. 2021-01-09 23:36:57 -05:00
Greyson Parrelli
0ac56ca571 Fix crash with ExpiringMessageManager. 2021-01-09 23:36:09 -05:00
Greyson Parrelli
12321bc2f0 Bump version to 5.1.3 2021-01-09 23:22:10 -05:00
Greyson Parrelli
3a55dfa32f Updated language translations. 2021-01-09 23:21:50 -05:00
Alan Evans
373972f5dc Fast job sorting. 2021-01-09 23:16:46 -05:00
Alan Evans
60a701f84f Fix missing dialog message on single user add confirm. 2021-01-09 20:12:10 -04:00
Greyson Parrelli
14f7c01fcb Only notify for actual recipient changes. 2021-01-09 18:45:22 -05:00
Greyson Parrelli
caf4f1a7ba Bump version to 5.1.2 2021-01-08 23:08:31 -05:00
Greyson Parrelli
eb55ac9a97 Updated language translations. 2021-01-08 23:07:17 -05:00
Greyson Parrelli
b9d8868aab Added a new onboarding megaphone. 2021-01-08 23:00:41 -05:00
Alex Hart
bec03534ef Animated skip button. 2021-01-08 21:10:40 -04:00
Alan Evans
565eab9dc1 Fix jumping "0 members". 2021-01-08 21:10:40 -04:00
Alan Evans
4d229862b6 Invite Friends bottom sheet. 2021-01-08 21:10:40 -04:00
Greyson Parrelli
3739eb7731 Add extra conditions for the SMS banner. 2021-01-08 21:01:13 -04:00
Alex Hart
ae5f9fb8ac Add empty state for members list in AddGroupDetailsFragment. 2021-01-08 21:01:13 -04:00
Alex Hart
4320a81846 Add invite friends action button and text. 2021-01-08 21:01:13 -04:00
Alan Evans
9fcf40fdc4 Allow empty group creation. 2021-01-08 12:53:23 -04:00
Greyson Parrelli
79d6ac100c Fix issue where megaphone display may be delayed. 2021-01-08 11:31:35 -05:00
Greyson Parrelli
a3e3153ee3 Add the Honor Play to the CameraX blacklist. 2021-01-08 10:13:50 -05:00
Alan Evans
0f525d2b07 Bump version to 5.1.1 2021-01-07 16:08:02 -04:00
Alan Evans
8de3f5045b Updated language translations. 2021-01-07 16:07:21 -04:00
Greyson Parrelli
fba4ae91e3 Fix issue where recipient observing could show stale data. 2021-01-07 16:07:21 -04:00
Alan Evans
dda68d6c95 Revert "Bump libsignal-client to 0.2.0"
This reverts commit e845fba8b3.
2021-01-07 16:07:04 -04:00
Greyson Parrelli
25af25cd19 Fix issue where button to go to archive was missing. 2021-01-07 16:07:04 -04:00
Alan Evans
dfd5b2c225 Ensure consistency and completeness of feature flag remote capable designation.
Make CustomVideoMuxer flag remote capable.
2021-01-07 16:07:04 -04:00
Greyson Parrelli
e850d8e917 Fix badge overlap in archive screen. 2021-01-07 09:54:02 -05:00
Alex Hart
677cf725a1 Fix bad screen lock behaviour. 2021-01-07 10:37:55 -04:00
Alan Evans
e95bb9cb0f Bump version to 5.1.0 2021-01-06 17:05:30 -04:00
Alan Evans
2c223a5826 Updated language translations. 2021-01-06 17:03:38 -04:00
Greyson Parrelli
bbc346bd7a Create a system for scheduling work post-initial-render. 2021-01-06 17:03:38 -04:00
Cody Henthorne
cf32b93269 Better error handling for group calls. 2021-01-06 17:03:38 -04:00
Cody Henthorne
84f1da76ad Fix bug where missing media keys would not always be shown on time. 2021-01-06 17:03:21 -04:00
Jack Lloyd
e845fba8b3 Bump libsignal-client to 0.2.0 2021-01-06 17:03:21 -04:00
Greyson Parrelli
01152ead61 Move the JobDatabase to a separate physical database.
Also removes maxInstancesPerFactory from DB, which was only used during job submission and had no need to be persisted.
2021-01-06 17:03:21 -04:00
Alex Hart
198281aa47 Show 'return to call' if local user is in the call group. 2021-01-06 17:03:21 -04:00
Jim Gustafson
8e8d86606b Update to RingRTC v2.8.9 2021-01-06 17:03:21 -04:00
Alan Evans
b4c2e21415 Custom streaming video muxer. 2021-01-06 17:03:21 -04:00
Alan Evans
6080e1f338 Ensure ProfileKeyCredentials match ProfileKey.
Fixes #10344
2021-01-06 17:03:20 -04:00
Alan Evans
6dd3fdaa55 Remove usages of deprecated Handler constructor. 2021-01-06 17:03:20 -04:00
Alan Evans
64312f9c7f Fix non-rendered previews when differ by trailing slash. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
86542febf9 Move the MegaphoneDatabase to a separate physical database. 2021-01-06 17:03:20 -04:00
Alex Hart
9da49f9f8a Load correct recipient from thread record. 2021-01-06 17:03:20 -04:00
Alex Hart
ce3872ce1a Fix ACTION_OPEN_DOCUMENT_TREE crash when no file picker available.
Fixes #10131
2021-01-06 17:03:20 -04:00
Greyson Parrelli
c466dba8c4 Move the KeyValueDatabase to a separate physical database. 2021-01-06 17:03:20 -04:00
Alex Hart
46d412a6c3 UX update and slight stability fix. 2021-01-06 17:03:20 -04:00
Alex Hart
e2872d9af8 Add emdash instead of 0 if no callers are present and we haven't connected / loaded the group state. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
3474b26f61 Don't include archived threads in recent conversation query. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
740e934e5d Speed up the recipient warm-up phase. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
61c5fc1057 Add shake-to-report for internal users. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
7ef77bf16c Remove unbounded conversation list query. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
aa3eb78956 Clean up and speed up conversation list item view. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
cdd7b2deb9 Improve and streamline Application#onCreate. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
c27300c19d Add a perf buildType for testing performance improvements. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
8927971a19 Replace non-essential conversation list views with stubs. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
1ced115b54 Only force a conversation list re-query for non-cold-starts. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
fcbd594def Add a system to easily trace jobs. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
4b8d02fdba Move Tracer to core-util. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
e10284bd13 Remove Trace annotation. 2021-01-06 17:03:20 -04:00
Greyson Parrelli
4b5f1d64e6 Switch the conversation list to our own paging library. 2021-01-06 17:03:20 -04:00
Alex Hart
b7477d287b Reopen properly when we select launcher icon.
* Reopen properly when we select launcher icon.

* Reduce noise
2021-01-06 17:03:20 -04:00
Greyson Parrelli
6bab6c2454 Increase prekey archive age to 30 days. 2021-01-06 17:03:20 -04:00
Alex Hart
586c45616c Utilize ACTION_GET_CONTENT for one-time-access to backup.
Fixes #10312
2021-01-06 17:03:20 -04:00
Greyson Parrelli
ccd405fdce Don't double-isolate-bidi on phone numbers.
Fixes #10257
2021-01-06 17:03:20 -04:00
henry
dbf78d1b69 Show correct fragment layout preview. 2020-12-18 10:41:14 -04:00
Alex Hart
5f947ea2d6 Remove a few more instances of AsyncTask. 2020-12-18 10:41:14 -04:00
Alex Hart
73afa82147 Remove ViewUtil deprecated methods. 2020-12-18 10:41:14 -04:00
Alex Hart
744b79419b Swap out AsyncTask usage in notification action receivers with bounded threadpool. 2020-12-18 10:41:14 -04:00
Alex Hart
ce20dd97ff Fix bad compose input height. 2020-12-18 10:41:14 -04:00
Greyson Parrelli
3983d5aca4 Log the threadId of a log. 2020-12-18 10:41:14 -04:00
Greyson Parrelli
7b0de2d2a9 Force a feature flag refresh after a version change. 2020-12-18 10:41:14 -04:00
Cody Henthorne
2b65482abd Fix KitKat OOM when rendering rounded material buttons. 2020-12-18 10:41:14 -04:00
Cody Henthorne
fe01e80af5 Fix bug with mute states not dynamically updating in participants list. 2020-12-18 10:41:14 -04:00
Greyson Parrelli
fc43a0d8e9 Put log tag in brackets. 2020-12-18 10:41:14 -04:00
Alex Hart
e709cdc9d5 Remember the last position of emoji and sticker picker as you swap between them. 2020-12-18 10:41:14 -04:00
Jack Lloyd
d2d698f64e Don't rely on the SessionState protobuf.
Instead use the convenient deserialization constructor
2020-12-18 10:41:14 -04:00
Alan Evans
7f1e33be32 Fix not deselecting item that is too large to send. 2020-12-18 10:41:14 -04:00
Greyson Parrelli
443f1a1554 Bump version to 5.0.8 2020-12-17 17:55:40 -05:00
Greyson Parrelli
ebb025c40a Updated language translations. 2020-12-17 17:55:40 -05:00
Greyson Parrelli
f3ce582fa5 Inline GV1 auto-migration flag. 2020-12-17 17:55:33 -05:00
Greyson Parrelli
372744178e Bump version to 5.0.7 2020-12-15 20:24:51 -05:00
Greyson Parrelli
fc3aa96b5a Updated language translations. 2020-12-15 20:24:22 -05:00
Greyson Parrelli
f4c723cc60 Refactor how we handle GV1->GV2 migration suggestions. 2020-12-15 20:18:47 -05:00
Alan Evans
7864c8ceb4 Fix translations in group call screen when using in-app language. 2020-12-15 12:34:34 -04:00
Alan Evans
4c80aac4d6 Drop sync messages with bad GV1 lengths. 2020-12-15 12:10:42 -04:00
Greyson Parrelli
e2b6e85431 Bump version to 5.0.6 2020-12-14 22:48:57 -05:00
Greyson Parrelli
8587153ddd Updated language translations. 2020-12-14 22:48:18 -05:00
Greyson Parrelli
21956e400f Use a new DatabaseObserver system. 2020-12-14 22:43:34 -05:00
Alex Hart
fa7346f79b Add group calling tooltip and megaphone. 2020-12-14 22:43:34 -05:00
Alan Evans
7227b43bbe Remove conversation list datasource throttler. 2020-12-14 12:47:26 -04:00
Greyson Parrelli
e8c75249f1 Bump version to 5.0.5 2020-12-14 01:13:41 -05:00
Greyson Parrelli
cc5628cbce Updated language translations. 2020-12-14 01:12:46 -05:00
Greyson Parrelli
441808b1df Fix issue where client deprecation sometimes wasn't cleared. 2020-12-13 14:44:19 -05:00
Greyson Parrelli
42b0fe7853 Bump version to 5.0.4 2020-12-10 12:36:38 -05:00
Greyson Parrelli
7877f5db2f Updated language translations. 2020-12-10 12:36:38 -05:00
Alex Hart
b972e05660 Auto focus national number field after valid country code in delete fragment. 2020-12-10 12:36:38 -05:00
Greyson Parrelli
23579a9b1d Do not unnecessarily refresh known-unregistered users during migration. 2020-12-10 12:36:38 -05:00
Greyson Parrelli
af99753d47 Trace Application and Activity creates. 2020-12-10 11:45:15 -05:00
Greyson Parrelli
4b2366e537 Bump version to 5.0.3 2020-12-09 17:42:44 -05:00
Greyson Parrelli
bea72c2ee3 Updated language translations. 2020-12-09 17:42:44 -05:00
Greyson Parrelli
32a50fcfad Disable group calling for API 19. 2020-12-09 17:42:44 -05:00
Greyson Parrelli
30fa741365 Make group calling flag hot-swappable. 2020-12-09 17:39:02 -05:00
Greyson Parrelli
bed2544ff4 Don't try to update contacts if you have no permission.
Fixes #10271
2020-12-09 17:07:54 -05:00
Cody Henthorne
5a773de3b1 Handle group call update sync messages. 2020-12-09 16:33:47 -05:00
Alan Evans
924405c8ba Increase uncompressed video attachment size to 500 Mb. 2020-12-09 16:30:42 -04:00
Alan Evans
93e9de3932 Increase stream copy buffer size to 64K. 2020-12-09 16:29:08 -04:00
Alan Evans
a8dd81eace Return optional for telephone number region name for the unknown case to be localized. 2020-12-09 15:47:44 -04:00
Greyson Parrelli
ec8793c6fe Fix rendering issue when deleting the last message in a conversation. 2020-12-09 14:39:22 -05:00
Alex Hart
ffc0a230be Fix country code width on account deletion screen. 2020-12-09 14:09:21 -04:00
Cody Henthorne
5d4922ed8d Show accurate current group call participants in lobby header. 2020-12-09 11:53:59 -05:00
Alan Evans
974c33fe37 Directly reference activity for remove avatar confirmation prompt. 2020-12-09 11:15:48 -04:00
Alex Hart
3f2b4d60fd Fix voice note saves on API 28 and lower. 2020-12-09 10:22:31 -04:00
Greyson Parrelli
ca633b13af Bump version to 5.0.2 2020-12-08 18:23:07 -05:00
Greyson Parrelli
a671e152bd Updated language translations. 2020-12-08 18:22:44 -05:00
Cody Henthorne
a564aae80a Do not show speaker hint in pip. 2020-12-08 18:10:04 -05:00
Greyson Parrelli
9f8e31db78 Change WebsocketDrainedConstraint to DecryptionsDrainedConstraint. 2020-12-08 18:10:04 -05:00
Cody Henthorne
84e9282f87 Attempt to reduce number of peek jobs run after being offline. 2020-12-08 18:10:04 -05:00
Alan Evans
3949f4fd45 Hide join group call for inactive groups. 2020-12-08 18:10:04 -05:00
Greyson Parrelli
944a180b68 Ensure GV1->GV2 migrations work via group links. 2020-12-08 18:10:04 -05:00
Greyson Parrelli
9cd1a12b6a Fix NPE in FastJobStorage#getJobCountForQueue(). 2020-12-08 18:10:04 -05:00
Greyson Parrelli
a4a2d2fc0d Log out exception when a backup fails. 2020-12-08 18:10:04 -05:00
Artem Varaksa
6df839612d Fix "Advanced PIN settings" pushing wrong fragment. 2020-12-08 18:10:04 -05:00
Greyson Parrelli
dd630abd0e Fix issue where scrolling could get stuck.
The number of off-screen pages was too small, resulting in the
possibility of you still being offscreen after the pages loaded,
which could lead to loading more data, which could lead to you still
being offscreen, ad infinitum.

Simply increasing the number of buffer
pages resolves it.

Tested by adding an artificial 1 second delay to
loading a page.
2020-12-08 18:10:04 -05:00
Greyson Parrelli
6826c0ded5 Fix another scenario where search position was off. 2020-12-08 18:10:04 -05:00
Alex Hart
f1d0d4f81b Fix account deletion UI bugs. 2020-12-08 18:10:04 -05:00
Alex Hart
bfa56f771d Do not show join banner in pip mode. 2020-12-08 09:20:51 -04:00
Greyson Parrelli
167b9c13e5 Bump version to 5.0.1 2020-12-07 22:52:26 -05:00
Greyson Parrelli
4b7d9a3b9d Updated language translations. 2020-12-07 22:52:10 -05:00
Greyson Parrelli
c7585c5594 Fix issues with jumpToMessage behavior. 2020-12-07 22:40:43 -05:00
Greyson Parrelli
c3d7b88cf6 Add support for setting max instances per job queue. 2020-12-07 17:30:05 -05:00
Cody Henthorne
dc4ce234b7 Ensure proper group call history in chat after being offline.
Co-authored-by: Alan Evans <alan@signal.org>
2020-12-07 17:27:35 -05:00
Cody Henthorne
12330b0aff Bump RingRTC to 2.8.7 2020-12-07 16:43:47 -05:00
Alex Hart
edb2a17bcb Add ability to delete your Signal account from within the app. 2020-12-07 17:39:16 -04:00
Alan Evans
00b6416583 Prevent surplus notification sound when entering group. 2020-12-07 17:36:21 -04:00
Alex Hart
62297f1f98 Stabilize bluetooth a bit. 2020-12-07 16:33:14 -05:00
Cody Henthorne
c00b0727e3 Show call full UI when group call is full. 2020-12-07 16:17:39 -05:00
Greyson Parrelli
13616b9820 Fix preview of link previews with no thumbnails. 2020-12-07 15:54:16 -05:00
Greyson Parrelli
6530e1d937 Update CameraX blacklist. 2020-12-07 14:34:52 -05:00
Alex Hart
aff00615cb Fix bad theming on audio device selection popup. 2020-12-07 15:32:43 -04:00
Alex Hart
be53bfa88f Hide members list when user enters pip. 2020-12-07 14:50:11 -04:00
Alex Hart
5de50f1a8b Fix overflow presentation when active speaker changes. 2020-12-07 14:11:35 -04:00
Alex Hart
61886ea10a Display speaker in PiP. 2020-12-07 13:16:02 -04:00
Sgn-32
ea94f6bc91 Pretty print your phone number in advanced settings. 2020-12-07 11:15:13 -05:00
Greyson Parrelli
6080c18c90 Fix RTL display of formatted phone numbers.
Fixes #10261

Thank you to @Sgn-32 for finding that it can be solved with
StringUtil#isolateBidi()
2020-12-07 11:02:46 -05:00
Cody Henthorne
595d5dddbe Add Group Call speaker view hint. 2020-12-07 10:46:36 -05:00
Bastian Köcher
9b81e7f71b Removes deprecated Samsung multi-window support
This removes the deprecated Samsung multi-window support. Actually this
breaks multi-window support on newer Samsung devices. Android supports
multi-window since Android 7.0 and AFAIK Samsung switched to this as
well. There isn't even any reference anymore that mentions these lines
of code as required.
2020-12-07 10:39:54 -05:00
Cody Henthorne
bdc6c8c65a Fix a few minor group call UI issues. 2020-12-07 10:05:35 -05:00
Cody Henthorne
2dcc7d284f Update group membership for a group call when it changes. 2020-12-05 20:55:52 -05:00
583 changed files with 26686 additions and 6011 deletions

View File

@@ -1,3 +1,12 @@
---
name: 🛠️ Bug report
about: Let us know that something isn't working as intended
title: ''
labels: ''
assignees: ''
---
<!-- This is a bug report template. By following the instructions below and filling out the sections with your information, you will help the developers get all the necessary data to fix your issue.
You can also preview your report before submitting it. You may remove sections that aren't relevant to your particular case.

20
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
blank_issues_enabled: false
contact_links:
- name: 📃Support Center
url: https://support.signal.org/
about: Find answers to many common questions.
- name: ✨ Feature request
url: https://community.signalusers.org/c/feature-requests/
about: Missing something in Signal? Let us know.
- name: 💬 Community support
url: https://community.signalusers.org/c/support/
about: Feel free to ask anything.
- name: 📖 Developer documentation
url: https://signal.org/docs/
about: Official Signal developer documentation.
- name: 📚 Translation feedback.
url: https://community.signalusers.org/c/translation-feedback/
about: Share feedback on translations.
- name: ❓ Other issue?
url: https://community.signalusers.org/
about: Search on the community forums.

View File

@@ -14,16 +14,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v2
- 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;21.0.6113669" --sdk_root=${ANDROID_SDK_ROOT}
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1

View File

@@ -61,10 +61,10 @@ protobuf {
}
}
def canonicalVersionCode = 751
def canonicalVersionName = "5.0.0"
def canonicalVersionCode = 771
def canonicalVersionName = "5.2.2"
def postFixSize = 10
def postFixSize = 100
def abiPostFix = ['universal' : 0,
'armeabi-v7a' : 1,
'arm64-v8a' : 2,
@@ -128,7 +128,6 @@ android {
buildConfigField "String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X0=\""
buildConfigField "String[]", "LANGUAGES", "new String[]{\"" + autoResConfig().collect { s -> s.replace('-r', '_') }.join('", "') + '"}'
buildConfigField "int", "CANONICAL_VERSION_CODE", "$canonicalVersionCode"
buildConfigField "int", "TRACE_EVENT_MAX", "3500"
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
@@ -206,6 +205,12 @@ android {
minifyEnabled true
proguardFiles = buildTypes.debug.proguardFiles
}
perf {
initWith debug
isDefault false
debuggable false
matchingFallbacks = ['debug']
}
}
productFlavors {
@@ -229,7 +234,6 @@ android {
ext.websiteUpdateUrl = "null"
buildConfigField "boolean", "PLAY_STORE_DISABLED", "false"
buildConfigField "String", "NOPLAY_UPDATE_URL", "$ext.websiteUpdateUrl"
buildConfigField "int", "TRACE_EVENT_MAX", "30_000"
}
prod {
@@ -311,8 +315,6 @@ dependencies {
implementation "androidx.camera:camera-view:1.0.0-alpha18"
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-ml-vision:24.0.3'
implementation 'com.google.firebase:firebase-ml-vision-face-model:20.0.1'
@@ -335,12 +337,14 @@ dependencies {
implementation project(':libsignal-service')
implementation project(':paging')
implementation project(':core-util')
implementation project(':video')
implementation 'org.signal:zkgroup-android:0.7.0'
implementation 'org.whispersystems:signal-client-android:0.1.5'
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.8.5'
implementation 'org.signal:ringrtc-android:2.8.9'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
@@ -24,6 +25,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver}
@@ -42,8 +44,16 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
try {
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
databaseHelperField.setAccessible(true);
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
SignalDatabase mainOpenHelper = Objects.requireNonNull((SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext())));
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
SignalDatabase megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext());
SignalDatabase jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext());
return Arrays.asList(new Descriptor(mainOpenHelper),
new Descriptor(keyValueOpenHelper),
new Descriptor(megaphoneOpenHelper),
new Descriptor(jobManagerOpenHelper));
} catch (Exception e) {
Log.i(TAG, "Unable to use reflection to access raw database.", e);
}
@@ -235,9 +245,9 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
}
static class Descriptor implements DatabaseDescriptor {
private final SQLCipherOpenHelper sqlCipherOpenHelper;
private final SignalDatabase sqlCipherOpenHelper;
Descriptor(@NonNull SQLCipherOpenHelper sqlCipherOpenHelper) {
Descriptor(@NonNull SignalDatabase sqlCipherOpenHelper) {
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
}
@@ -247,11 +257,11 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
}
public @NonNull SQLiteDatabase getReadable() {
return sqlCipherOpenHelper.getReadableDatabase().getSqlCipherDatabase();
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
public @NonNull SQLiteDatabase getWritable() {
return sqlCipherOpenHelper.getWritableDatabase().getSqlCipherDatabase();
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
}
}

View File

@@ -225,6 +225,17 @@
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER" />
</intent-filter>
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher" />
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher" />
</activity-alias>
<activity android:name=".deeplinks.DeepLinkEntryActivity"
android:noHistory="true"
android:theme="@style/Signal.Transparent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -241,13 +252,7 @@
<data android:scheme="https"
android:host="signal.group"/>
</intent-filter>
<meta-data android:name="com.sec.minimode.icon.portrait.normal"
android:resource="@mipmap/ic_launcher" />
<meta-data android:name="com.sec.minimode.icon.landscape.normal"
android:resource="@mipmap/ic_launcher" />
</activity-alias>
</activity>
<activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
@@ -465,17 +470,11 @@
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">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<activity android:name=".ClearAvatarPromptActivity"
android:theme="@style/Theme.AppCompat.Dialog.Alert"
android:icon="@drawable/clear_profile_avatar"
android:label="@string/AndroidManifest_remove_photo"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".contacts.TurnOffContactJoinedNotificationsActivity"
android:theme="@style/Theme.AppCompat.Dialog.Alert" />
@@ -510,7 +509,6 @@
<activity android:name=".MainActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<activity android:name=".pin.PinRestoreActivity"
@@ -802,12 +800,5 @@
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
<uses-library android:name="com.sec.android.app.multiwindow" android:required="false"/>
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W" android:value="632.0dip" />
<meta-data android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H" android:value="598.0dip" />
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_W" android:value="632.0dip" />
<meta-data android:name="com.sec.android.multiwindow.MINIMUM_SIZE_H" android:value="598.0dip" />
</application>
</manifest>

View File

@@ -8,14 +8,15 @@ public final class AppCapabilities {
private AppCapabilities() {
}
private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
private static final boolean UUID_CAPABLE = false;
private static final boolean GV2_CAPABLE = true;
private static final boolean GV1_MIGRATION = true;
/**
* @param storageCapable Whether or not the user can use storage service. This is another way of
* asking if the user has set a Signal PIN or not.
*/
public static AccountAttributes.Capabilities getCapabilities(boolean storageCapable) {
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, FeatureFlags.groupsV1AutoMigration());
return new AccountAttributes.Capabilities(UUID_CAPABLE, GV2_CAPABLE, storageCapable, GV1_MIGRATION);
}
}

View File

@@ -50,6 +50,28 @@ public final class AppInitialization {
public static void onPostBackupRestore(@NonNull Context context) {
Log.i(TAG, "onPostBackupRestore()");
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
SignalStore.onboarding().clearAll();
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()));
}
/**
* Temporary migration method that does the safest bits of {@link #onFirstEverAppLaunch(Context)}
*/
public static void onRepairFirstEverAppLaunch(@NonNull Context context) {
Log.w(TAG, "onRepairFirstEverAppLaunch()");
InsightsOptOut.userRequestedOptOut(context);
TextSecurePreferences.setAppMigrationVersion(context, ApplicationMigrations.CURRENT_VERSION);
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
TextSecurePreferences.setPasswordDisabled(context, true);
TextSecurePreferences.setLastExperienceVersionCode(context, Util.getCanonicalVersionCode());
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));

View File

@@ -16,13 +16,13 @@
*/
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;
import android.hardware.SensorManager;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
@@ -33,10 +33,12 @@ import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.Conscrypt;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.core.util.ShakeDetector;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.logging.PersistentLogger;
import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -44,6 +46,7 @@ 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.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
@@ -68,13 +71,15 @@ import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.shakereport.ShakeToReport;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.VersionTracker;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils;
@@ -93,7 +98,6 @@ import java.util.concurrent.TimeUnit;
*
* @author Moxie Marlinspike
*/
@Trace
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
private static final String TAG = ApplicationContext.class.getSimpleName();
@@ -110,59 +114,81 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
@Override
public void onCreate() {
Tracer.getInstance().start("Application#onCreate()");
AppStartup.getInstance().onApplicationCreate();
long startTime = System.currentTimeMillis();
super.onCreate();
initializeSecurityProvider();
initializeLogging();
Log.i(TAG, "onCreate()");
initializeCrashHandling();
initializeAppDependencies();
initializeFirstEverAppLaunch();
initializeApplicationMigrations();
initializeMessageRetrieval();
initializeExpiringMessageManager();
initializeRevealableMessageManager();
initializeGcmCheck();
initializeSignedPreKeyCheck();
initializePeriodicTasks();
initializeCircumvention();
initializeRingRtc();
initializePendingMessages();
initializeBlobProvider();
initializeCleanup();
initializeGlideCodecs();
FeatureFlags.init();
NotificationChannels.create(this);
RefreshPreKeysJob.scheduleIfNecessary();
StorageSyncHelper.scheduleRoutineSync();
RegistrationUtil.maybeMarkRegistrationComplete(this);
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
if (Build.VERSION.SDK_INT < 21) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
if (FeatureFlags.internalUser()) {
Tracer.getInstance().setMaxBufferSize(35_000);
}
ApplicationDependencies.getJobManager().beginJobLoop();
super.onCreate();
DynamicTheme.setDefaultDayNightMode(this);
AppStartup.getInstance().addBlocking("security-provider", this::initializeSecurityProvider)
.addBlocking("logging", () -> {
initializeLogging();
Log.i(TAG, "onCreate()");
})
.addBlocking("crash-handling", this::initializeCrashHandling)
.addBlocking("eat-db", () -> DatabaseFactory.getInstance(this))
.addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
.addBlocking("app-migrations", this::initializeApplicationMigrations)
.addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("mark-registration", () -> RegistrationUtil.maybeMarkRegistrationComplete(this))
.addBlocking("lifecycle-observer", () -> ProcessLifecycleOwner.get().getLifecycle().addObserver(this))
.addBlocking("message-retriever", this::initializeMessageRetrieval)
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
.addBlocking("vector-compat", () -> {
if (Build.VERSION.SDK_INT < 21) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
})
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializeGcmCheck)
.addNonBlocking(this::initializeSignedPreKeyCheck)
.addNonBlocking(this::initializePeriodicTasks)
.addNonBlocking(this::initializeCircumvention)
.addNonBlocking(this::initializePendingMessages)
.addNonBlocking(this::initializeCleanup)
.addNonBlocking(this::initializeGlideCodecs)
.addNonBlocking(FeatureFlags::init)
.addNonBlocking(RefreshPreKeysJob::scheduleIfNecessary)
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(this::initializeBlobProvider)
.addPostRender(() -> NotificationChannels.create(this))
.execute();
ProcessLifecycleOwner.get().getLifecycle().addObserver(this);
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
Tracer.getInstance().end("Application#onCreate()");
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
long startTime = System.currentTimeMillis();
isAppVisible = true;
Log.i(TAG, "App is now visible.");
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getFrameRateTracker().begin();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
checkBuildExpiration();
SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary();
ApplicationDependencies.getRecipientCache().warmUp();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary(this);
GroupV1MigrationJob.enqueueRoutineMigrationsIfNecessary(this);
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getShakeToReport().enable();
checkBuildExpiration();
});
Log.d(TAG, "onStart() took " + (System.currentTimeMillis() - startTime) + " ms");
}
@Override
@@ -172,9 +198,13 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
KeyCachingService.onAppBackgrounded(this);
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
ApplicationDependencies.getFrameRateTracker().end();
ApplicationDependencies.getShakeToReport().disable();
}
public ExpiringMessageManager getExpiringMessageManager() {
if (expiringMessageManager == null) {
initializeExpiringMessageManager();
}
return expiringMessageManager;
}
@@ -247,13 +277,16 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
private void initializeFirstEverAppLaunch() {
if (TextSecurePreferences.getFirstInstallVersion(this) == -1) {
if (!SQLCipherOpenHelper.databaseFileExists(this)) {
if (!SQLCipherOpenHelper.databaseFileExists(this) || VersionTracker.getDaysSinceFirstInstalled(this) < 365) {
Log.i(TAG, "First ever app launch!");
AppInitialization.onFirstEverAppLaunch(this);
}
Log.i(TAG, "Setting first install version to " + BuildConfig.CANONICAL_VERSION_CODE);
TextSecurePreferences.setFirstInstallVersion(this, BuildConfig.CANONICAL_VERSION_CODE);
} else if (!TextSecurePreferences.isPasswordDisabled(this) && VersionTracker.getDaysSinceFirstInstalled(this) < 90) {
Log.i(TAG, "Detected a new install that doesn't have passphrases disabled -- assuming bad initialization.");
AppInitialization.onRepairFirstEverAppLaunch(this);
}
}
@@ -328,23 +361,15 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
}
}
@SuppressLint("StaticFieldLeak")
@WorkerThread
private void initializeCircumvention() {
AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
try {
ProviderInstaller.installIfNeeded(ApplicationContext.this);
} catch (Throwable t) {
Log.w(TAG, t);
}
}
return null;
if (new SignalServiceNetworkAccess(ApplicationContext.this).isCensored(ApplicationContext.this)) {
try {
ProviderInstaller.installIfNeeded(ApplicationContext.this);
} catch (Throwable t) {
Log.w(TAG, t);
}
};
task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
}
}
private void executePendingContactSync() {
@@ -365,17 +390,15 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
}
}
@WorkerThread
private void initializeBlobProvider() {
SignalExecutors.BOUNDED.execute(() -> {
BlobProvider.getInstance().onSessionStart(this);
});
BlobProvider.getInstance().onSessionStart(this);
}
@WorkerThread
private void initializeCleanup() {
SignalExecutors.BOUNDED.execute(() -> {
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
});
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
}
private void initializeGlideCodecs() {

View File

@@ -39,9 +39,9 @@ import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.DataAndStoragePreferenceFragment;
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
@@ -65,6 +65,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
implements SharedPreferences.OnSharedPreferenceChangeListener
{
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment";
@SuppressWarnings("unused")
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
@@ -104,6 +105,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
initFragment(android.R.id.content, new NotificationsPreferenceFragment());
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_BACKUPS_FRAGMENT, false)) {
initFragment(android.R.id.content, new BackupsPreferenceFragment());
} else if (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_HELP_FRAGMENT, false)) {
initFragment(android.R.id.content, new HelpFragment());
} else if (icicle == null) {
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
} else {
@@ -309,7 +312,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
fragment = new ChatsPreferenceFragment();
break;
case PREFERENCE_CATEGORY_STORAGE:
fragment = new StoragePreferenceFragment();
fragment = new DataAndStoragePreferenceFragment();
break;
case PREFERENCE_CATEGORY_DEVICES:
Intent intent = new Intent(getActivity(), DeviceActivity.class);

View File

@@ -15,6 +15,8 @@ import androidx.core.app.ActivityCompat;
import androidx.core.app.ActivityOptionsCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.ConfigurationUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
@@ -31,8 +33,10 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
AppStartup.getInstance().onCriticalRenderEventStart();
logEvent("onCreate()");
super.onCreate(savedInstanceState);
AppStartup.getInstance().onCriticalRenderEventEnd();
}
@Override
@@ -44,6 +48,7 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onStart() {
logEvent("onStart()");
ApplicationDependencies.getShakeToReport().registerActivity(this);
super.onStart();
}

View File

@@ -61,7 +61,10 @@ public interface BindableConversationItem extends Unbindable {
void onVoiceNotePlay(@NonNull Uri uri, long messageId, double position);
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
void onDecryptionFailedLearnMoreClicked();
void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient);
void onJoinGroupCallClicked();
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);

View File

@@ -7,18 +7,21 @@ import android.view.ContextThemeWrapper;
import androidx.appcompat.app.AlertDialog;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class ClearProfileAvatarActivity extends Activity {
public final class ClearAvatarPromptActivity extends Activity {
private static final String ARG_TITLE = "arg_title";
public static Intent createForUserProfilePhoto() {
return new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
Intent intent = new Intent(ApplicationDependencies.getApplication(), ClearAvatarPromptActivity.class);
intent.putExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo);
return intent;
}
public static Intent createForGroupProfilePhoto() {
Intent intent = new Intent("org.thoughtcrime.securesms.action.CLEAR_PROFILE_PHOTO");
Intent intent = new Intent(ApplicationDependencies.getApplication(), ClearAvatarPromptActivity.class);
intent.putExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_group_photo);
return intent;
}
@@ -27,10 +30,10 @@ public class ClearProfileAvatarActivity extends Activity {
public void onResume() {
super.onResume();
int titleId = getIntent().getIntExtra(ARG_TITLE, R.string.ClearProfileActivity_remove_profile_photo);
int message = getIntent().getIntExtra(ARG_TITLE, 0);
new AlertDialog.Builder(new ContextThemeWrapper(this, DynamicTheme.isDarkTheme(this) ? R.style.TextSecure_DarkTheme : R.style.TextSecure_LightTheme))
.setMessage(titleId)
.setMessage(message)
.setNegativeButton(android.R.string.cancel, (dialog, which) -> finish())
.setPositiveButton(R.string.ClearProfileActivity_remove, (dialog, which) -> {
Intent result = new Intent();

View File

@@ -448,8 +448,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
swipeRefresh.setVisibility(View.VISIBLE);
reset();
} else {
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
initializeNoContactsPermission();
Context context = getContext();
if (context != null) {
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
initializeNoContactsPermission();
}
}
}
}.execute();

View File

@@ -9,6 +9,7 @@ import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Parcelable;
import android.view.View;
@@ -150,7 +151,7 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
} else {
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
startActivity(MainActivity.clearTop(this));
}
}
@@ -158,6 +159,11 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActivity {
}
private class ImportStateHandler extends Handler {
public ImportStateHandler() {
super(Looper.getMainLooper());
}
@Override
public void handleMessage(Message message) {
switch (message.what) {

View File

@@ -32,9 +32,9 @@ public class DeviceAddFragment extends LoggingFragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.device_add_fragment);
this.overlay = ViewUtil.findById(this.container, R.id.overlay);
this.scannerView = ViewUtil.findById(this.container, R.id.scanner);
this.devicesImage = ViewUtil.findById(this.container, R.id.devices);
this.overlay = this.container.findViewById(R.id.overlay);
this.scannerView = this.container.findViewById(R.id.scanner);
this.devicesImage = this.container.findViewById(R.id.devices);
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
this.overlay.setOrientation(LinearLayout.HORIZONTAL);

View File

@@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicelist.Device;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.whispersystems.signalservice.api.SignalServiceAccountManager;
@@ -68,7 +67,7 @@ public class DeviceListFragment extends ListFragment
this.empty = view.findViewById(R.id.empty);
this.progressContainer = view.findViewById(R.id.progress_container);
this.addDeviceButton = ViewUtil.findById(view, R.id.add_device);
this.addDeviceButton = view.findViewById(R.id.add_device);
this.addDeviceButton.setOnClickListener(this);
return view;

View File

@@ -35,6 +35,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
@@ -93,26 +94,33 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
slideInAnimation = loadAnimation(R.anim.slide_from_bottom);
slideOutAnimation = loadAnimation(R.anim.slide_to_bottom);
View shareButton = ViewUtil.findById(this, R.id.share_button);
View smsButton = ViewUtil.findById(this, R.id.sms_button);
Button smsCancelButton = ViewUtil.findById(this, R.id.cancel_sms_button);
ContactFilterToolbar contactFilter = ViewUtil.findById(this, R.id.contact_filter);
View shareButton = findViewById(R.id.share_button);
Button smsButton = findViewById(R.id.sms_button);
Button smsCancelButton = findViewById(R.id.cancel_sms_button);
ContactFilterToolbar contactFilter = findViewById(R.id.contact_filter);
inviteText = ViewUtil.findById(this, R.id.invite_text);
smsSendFrame = ViewUtil.findById(this, R.id.sms_send_frame);
smsSendButton = ViewUtil.findById(this, R.id.send_sms_button);
inviteText = findViewById(R.id.invite_text);
smsSendFrame = findViewById(R.id.sms_send_frame);
smsSendButton = findViewById(R.id.send_sms_button);
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
contactsFragment.setOnContactSelectedListener(this);
shareButton.setOnClickListener(new ShareClickListener());
smsButton.setOnClickListener(new SmsClickListener());
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
smsSendButton.setOnClickListener(new SmsSendClickListener());
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
contactFilter.setNavigationIcon(R.drawable.ic_search_conversation_24);
if (Util.isDefaultSmsProvider(this)) {
shareButton.setOnClickListener(new ShareClickListener());
smsButton.setOnClickListener(new SmsClickListener());
} else {
shareButton.setVisibility(View.GONE);
smsButton.setOnClickListener(new ShareClickListener());
smsButton.setText(R.string.InviteActivity_share);
}
}
private Animation loadAnimation(@AnimRes int animResId) {

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
@@ -8,13 +9,12 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
@Trace
public class MainActivity extends PassphraseRequiredActivity {
public static final int RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901;
@@ -22,8 +22,19 @@ public class MainActivity extends PassphraseRequiredActivity {
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final MainNavigator navigator = new MainNavigator(this);
public static @NonNull Intent clearTop(@NonNull Context context) {
Intent intent = new Intent(context, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_SINGLE_TOP);
return intent;
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
AppStartup.getInstance().onCriticalRenderEventStart();
super.onCreate(savedInstanceState, ready);
setContentView(R.layout.main_activity);
@@ -34,6 +45,13 @@ public class MainActivity extends PassphraseRequiredActivity {
CachedInflater.from(this).clear();
}
@Override
public Intent getIntent() {
return super.getIntent().setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP |
Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);

View File

@@ -12,6 +12,7 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.PushNotificationReceiveJob;
@@ -25,6 +26,7 @@ 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.AppStartup;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
@@ -49,6 +51,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
@Override
protected final void onCreate(Bundle savedInstanceState) {
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
AppStartup.getInstance().onCriticalRenderEventStart();
this.networkAccess = new SignalServiceNetworkAccess(this);
onPreCreate();
@@ -61,6 +65,9 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
initializeClearKeyReceiver();
onCreate(savedInstanceState, true);
}
AppStartup.getInstance().onCriticalRenderEventEnd();
Tracer.getInstance().end(Log.tag(getClass()) + "#onCreate()");
}
protected void onPreCreate() {}
@@ -218,15 +225,17 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private Intent getConversationListIntent() {
// TODO [greyson] Navigation
return new Intent(this, MainActivity.class);
return MainActivity.clearTop(this);
}
private void initializeClearKeyReceiver() {
this.clearKeyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive() for clear key event");
onMasterSecretCleared();
Log.i(TAG, "onReceive() for clear key event. PasswordDisabled: " + TextSecurePreferences.isPasswordDisabled(context) + ", ScreenLock: " + TextSecurePreferences.isScreenLockEnabled(context));
if (TextSecurePreferences.isScreenLockEnabled(context) || !TextSecurePreferences.isPasswordDisabled(context)) {
onMasterSecretCleared();
}
}
};

View File

@@ -35,7 +35,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
if (rawId == null) {
Toast.makeText(this, R.string.ShortcutLauncherActivity_invalid_shortcut, Toast.LENGTH_SHORT).show();
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
startActivity(MainActivity.clearTop(this));
finish();
return;
}
@@ -43,7 +43,7 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
Recipient recipient = Recipient.live(RecipientId.from(rawId)).get();
// TODO [greyson] Navigation
TaskStackBuilder backStack = TaskStackBuilder.create(this)
.addNextIntent(new Intent(this, MainActivity.class));
.addNextIntent(MainActivity.clearTop(this));
CommunicationActions.startConversation(this, recipient, null, backStack);
finish();

View File

@@ -11,8 +11,6 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
public class TransportOptionsAdapter extends BaseAdapter {
@@ -55,9 +53,9 @@ public class TransportOptionsAdapter extends BaseAdapter {
}
TransportOption transport = (TransportOption) getItem(position);
ImageView imageView = ViewUtil.findById(convertView, R.id.icon);
TextView textView = ViewUtil.findById(convertView, R.id.text);
TextView subtextView = ViewUtil.findById(convertView, R.id.subtext);
ImageView imageView = convertView.findViewById(R.id.icon);
TextView textView = convertView.findViewById(R.id.text);
TextView subtextView = convertView.findViewById(R.id.subtext);
imageView.getBackground().setColorFilter(transport.getBackgroundColor(), Mode.MULTIPLY);
imageView.setImageResource(transport.getDrawable());

View File

@@ -258,24 +258,24 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_display_fragment);
this.numbersContainer = ViewUtil.findById(container, R.id.number_table);
this.qrCode = ViewUtil.findById(container, R.id.qr_code);
this.verified = ViewUtil.findById(container, R.id.verified_switch);
this.qrVerified = ViewUtil.findById(container, R.id.qr_verified);
this.description = ViewUtil.findById(container, R.id.description);
this.tapLabel = ViewUtil.findById(container, R.id.tap_label);
this.codes[0] = ViewUtil.findById(container, R.id.code_first);
this.codes[1] = ViewUtil.findById(container, R.id.code_second);
this.codes[2] = ViewUtil.findById(container, R.id.code_third);
this.codes[3] = ViewUtil.findById(container, R.id.code_fourth);
this.codes[4] = ViewUtil.findById(container, R.id.code_fifth);
this.codes[5] = ViewUtil.findById(container, R.id.code_sixth);
this.codes[6] = ViewUtil.findById(container, R.id.code_seventh);
this.codes[7] = ViewUtil.findById(container, R.id.code_eighth);
this.codes[8] = ViewUtil.findById(container, R.id.code_ninth);
this.codes[9] = ViewUtil.findById(container, R.id.code_tenth);
this.codes[10] = ViewUtil.findById(container, R.id.code_eleventh);
this.codes[11] = ViewUtil.findById(container, R.id.code_twelth);
this.numbersContainer = container.findViewById(R.id.number_table);
this.qrCode = container.findViewById(R.id.qr_code);
this.verified = container.findViewById(R.id.verified_switch);
this.qrVerified = container.findViewById(R.id.qr_verified);
this.description = container.findViewById(R.id.description);
this.tapLabel = container.findViewById(R.id.tap_label);
this.codes[0] = container.findViewById(R.id.code_first);
this.codes[1] = container.findViewById(R.id.code_second);
this.codes[2] = container.findViewById(R.id.code_third);
this.codes[3] = container.findViewById(R.id.code_fourth);
this.codes[4] = container.findViewById(R.id.code_fifth);
this.codes[5] = container.findViewById(R.id.code_sixth);
this.codes[6] = container.findViewById(R.id.code_seventh);
this.codes[7] = container.findViewById(R.id.code_eighth);
this.codes[8] = container.findViewById(R.id.code_ninth);
this.codes[9] = container.findViewById(R.id.code_tenth);
this.codes[10] = container.findViewById(R.id.code_eleventh);
this.codes[11] = container.findViewById(R.id.code_twelth);
this.qrCode.setOnClickListener(clickListener);
this.registerForContextMenu(numbersContainer);
@@ -664,7 +664,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup viewGroup, Bundle bundle) {
this.container = ViewUtil.inflate(inflater, viewGroup, R.layout.verify_scan_fragment);
this.cameraView = ViewUtil.findById(container, R.id.scanner);
this.cameraView = container.findViewById(R.id.scanner);
return container;
}

View File

@@ -27,15 +27,18 @@ import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Rational;
import android.view.View;
import android.view.Window;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.ViewModelProviders;
import androidx.transition.Transition;
import androidx.transition.TransitionListenerAdapter;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
@@ -59,18 +62,19 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.whispersystems.libsignal.IdentityKey;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import org.whispersystems.signalservice.api.messages.calls.OfferMessage;
import java.util.List;
public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumberChangeDialog.Callback {
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
private static final String TAG = WebRtcCallActivity.class.getSimpleName();
private static final String TAG = Log.tag(WebRtcCallActivity.class);
private static final int STANDARD_DELAY_FINISH = 1000;
@@ -82,6 +86,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen;
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
@@ -102,8 +107,8 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
//noinspection ConstantConditions
getSupportActionBar().hide();
fullscreenHelper = new FullscreenHelper(this);
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
@@ -128,7 +133,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
@Override
public void onNewIntent(Intent intent){
public void onNewIntent(Intent intent) {
Log.i(TAG, "onNewIntent");
super.onNewIntent(intent);
processIntent(intent);
@@ -143,9 +148,9 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
EventBus.getDefault().unregister(this);
}
if (!viewModel.isCallingStarted()) {
if (!viewModel.isCallStarting()) {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
finish();
}
}
@@ -158,9 +163,9 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
EventBus.getDefault().unregister(this);
if (!viewModel.isCallingStarted()) {
if (!viewModel.isCallStarting()) {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
startService(intent);
@@ -188,6 +193,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
@Override
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
viewModel.setIsInPipMode(isInPictureInPictureMode);
participantUpdateWindow.setEnabled(!isInPictureInPictureMode);
}
private boolean enterPipModeIfPossible() {
@@ -196,6 +202,8 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
.setAspectRatio(new Rational(9, 16))
.build();
enterPictureInPictureMode(params);
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
return true;
}
return false;
@@ -241,6 +249,8 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall());
viewModel.shouldShowSpeakerHint().observe(this, this::updateSpeakerHint);
callScreen.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
@@ -468,7 +478,6 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
private void handleServerFailure() {
EventBus.getDefault().removeStickyEvent(WebRtcViewModel.class);
callScreen.setStatus(getString(R.string.RedPhone_network_failed));
delayedFinish();
}
private void handleNoSuchUser(final @NonNull WebRtcViewModel event) {
@@ -505,6 +514,18 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
}
private void updateGroupMembersForGroupCall() {
startService(new Intent(this, WebRtcCallService.class).setAction(WebRtcCallService.ACTION_GROUP_REQUEST_UPDATE_MEMBERS));
}
private void updateSpeakerHint(boolean showSpeakerHint) {
if (showSpeakerHint) {
callScreen.showSpeakerViewHint();
} else {
callScreen.hideSpeakerViewHint();
}
}
@Override
public void onSendAnywayAfterSafetyNumberChange(@NonNull List<RecipientId> changedRecipients) {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
@@ -514,7 +535,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
.putExtra(WebRtcCallService.EXTRA_RECIPIENT_IDS, RecipientId.toSerializedList(changedRecipients));
startService(intent);
} else {
startCall(state.getLocalParticipant().isVideoEnabled());
viewModel.startCall(state.getLocalParticipant().isVideoEnabled());
}
}
@@ -525,7 +546,7 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
public void onCanceled() {
CallParticipantsState state = viewModel.getCallParticipantsState().getValue();
if (state != null && state.getGroupCallState().isNotIdle()) {
if (state.getCallState() == WebRtcViewModel.State.CALL_PRE_JOIN) {
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_CANCEL_PRE_JOIN_CALL);
startService(intent);
@@ -622,6 +643,16 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
}
@Override
public void showSystemUI() {
fullscreenHelper.showSystemUI();
}
@Override
public void hideSystemUI() {
fullscreenHelper.hideSystemUI();
}
@Override
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
switch (audioOutput) {
@@ -687,5 +718,10 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
public void onPageChanged(@NonNull CallParticipantsState.SelectedPage page) {
viewModel.setIsViewingFocusedParticipant(page);
}
@Override
public void onLocalPictureInPictureClicked() {
viewModel.onLocalPictureInPictureClicked();
}
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.backup;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
@@ -126,7 +127,12 @@ public class BackupDialog {
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_READ_URI_PERMISSION);
fragment.startActivityForResult(intent, requestCode);
try {
fragment.startActivityForResult(intent, requestCode);
} catch (ActivityNotFoundException e) {
Toast.makeText(fragment.requireContext(), R.string.BackupDialog_no_file_picker_available, Toast.LENGTH_LONG)
.show();
}
dialog.dismiss();
}))

View File

@@ -74,11 +74,7 @@ public class FullBackupExporter extends FullBackupBase {
OneTimePreKeyDatabase.TABLE_NAME,
SessionDatabase.TABLE_NAME,
SearchDatabase.SMS_FTS_TABLE_NAME,
SearchDatabase.MMS_FTS_TABLE_NAME,
JobDatabase.JOBS_TABLE_NAME,
JobDatabase.CONSTRAINTS_TABLE_NAME,
JobDatabase.DEPENDENCIES_TABLE_NAME,
KeyValueDatabase.TABLE_NAME
SearchDatabase.MMS_FTS_TABLE_NAME
);
public static void export(@NonNull Context context,

View File

@@ -81,8 +81,8 @@ public class ComposeText extends EmojiEditText {
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (!TextUtils.isEmpty(hint)) {
if (!TextUtils.isEmpty(subHint)) {
@@ -92,6 +92,7 @@ public class ComposeText extends EmojiEditText {
} else {
setHint(ellipsizeToWidth(hint));
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}

View File

@@ -6,7 +6,6 @@ import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
@@ -20,6 +19,7 @@ import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.model.KeyPath;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
@@ -185,20 +185,16 @@ public class ConversationItemFooter extends LinearLayout {
ApplicationContext.getInstance(getContext()).getExpiringMessageManager().checkSchedule();
}
} else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(getContext()).getExpiringMessageManager();
long id = messageRecord.getId();
boolean mms = messageRecord.isMms();
SignalExecutors.BOUNDED.execute(() -> {
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(getContext()).getExpiringMessageManager();
long id = messageRecord.getId();
boolean mms = messageRecord.isMms();
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
if (mms) DatabaseFactory.getMmsDatabase(getContext()).markExpireStarted(id);
else DatabaseFactory.getSmsDatabase(getContext()).markExpireStarted(id);
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
return null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
});
}
} else {
this.timerView.setVisibility(View.GONE);

View File

@@ -27,8 +27,7 @@ public abstract class FullScreenDialogFragment extends DialogFragment {
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme_FullScreenDialog
: R.style.TextSecure_LightTheme_FullScreenDialog);
setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_FullScreen);
}
@Override

View File

@@ -8,6 +8,7 @@ import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.TextView;
@@ -91,4 +92,18 @@ public class LabeledEditText extends FrameLayout implements View.OnFocusChangeLi
super.setEnabled(enabled);
input.setEnabled(enabled);
}
public void focusAndMoveCursorToEndAndOpenKeyboard() {
input.requestFocus();
int numberLength = getText().length();
input.setSelection(numberLength, numberLength);
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
if (!imm.isAcceptingText()) {
imm.toggleSoftInput(InputMethodManager.SHOW_IMPLICIT, InputMethodManager.HIDE_IMPLICIT_ONLY);
}
}
}

View File

@@ -23,7 +23,6 @@ import androidx.core.view.ViewCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.ViewUtil;
public final class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {
@@ -55,7 +54,7 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
floatingRecordButton = new FloatingRecordButton(getContext(), findViewById(R.id.quick_audio_fab));
lockDropTarget = new LockDropTarget (getContext(), findViewById(R.id.lock_drop_target));
View recordButton = ViewUtil.findById(this, R.id.quick_audio_toggle);
View recordButton = findViewById(R.id.quick_audio_toggle);
recordButton.setOnTouchListener(this);
}

View File

@@ -32,7 +32,6 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.ViewUtil;
public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks<Cursor> {
@@ -52,7 +51,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
inflate(context, R.layout.recent_photo_view, this);
this.recyclerView = ViewUtil.findById(this, R.id.photo_list);
this.recyclerView = findViewById(R.id.photo_list);
this.recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
this.recyclerView.setItemAnimator(new DefaultItemAnimator());
}
@@ -158,7 +157,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
RecentPhotoViewHolder(View itemView) {
super(itemView);
this.imageView = ViewUtil.findById(itemView, R.id.thumbnail);
this.imageView = itemView.findViewById(R.id.thumbnail);
}
}
}

View File

@@ -36,7 +36,6 @@ import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
public final class RecyclerViewFastScroller extends LinearLayout {
private static final int BUBBLE_ANIMATION_DURATION = 100;
@@ -75,8 +74,8 @@ public final class RecyclerViewFastScroller extends LinearLayout {
setClipChildren(false);
setScrollContainer(true);
inflate(context, R.layout.recycler_view_fast_scroller, this);
bubble = ViewUtil.findById(this, R.id.fastscroller_bubble);
handle = ViewUtil.findById(this, R.id.fastscroller_handle);
bubble = findViewById(R.id.fastscroller_bubble);
handle = findViewById(R.id.fastscroller_handle);
}
@Override

View File

@@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
public class ThreadPhotoRailView extends FrameLayout {
@@ -41,7 +40,7 @@ public class ThreadPhotoRailView extends FrameLayout {
inflate(context, R.layout.recipient_preference_photo_rail, this);
this.recyclerView = ViewUtil.findById(this, R.id.photo_list);
this.recyclerView = findViewById(R.id.photo_list);
this.recyclerView.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false));
this.recyclerView.setItemAnimator(new DefaultItemAnimator());
this.recyclerView.setNestedScrollingEnabled(false);
@@ -112,7 +111,7 @@ public class ThreadPhotoRailView extends FrameLayout {
ThreadPhotoViewHolder(View itemView) {
super(itemView);
this.imageView = ViewUtil.findById(itemView, R.id.thumbnail);
this.imageView = itemView.findViewById(R.id.thumbnail);
}
}
}

View File

@@ -22,7 +22,6 @@ import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.events.PartProgressEvent;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Collections;
import java.util.HashMap;
@@ -61,16 +60,16 @@ public final class TransferControlView extends FrameLayout {
inflate(context, R.layout.transfer_controls_view, this);
setLongClickable(false);
ViewUtil.setBackground(this, ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
setBackground(ContextCompat.getDrawable(context, R.drawable.transfer_controls_background));
setVisibility(GONE);
setLayoutTransition(new LayoutTransition());
this.networkProgress = new HashMap<>();
this.compresssionProgress = new HashMap<>();
this.progressWheel = ViewUtil.findById(this, R.id.progress_wheel);
this.downloadDetails = ViewUtil.findById(this, R.id.download_details);
this.downloadDetailsText = ViewUtil.findById(this, R.id.download_details_text);
this.progressWheel = findViewById(R.id.progress_wheel);
this.downloadDetails = findViewById(R.id.download_details);
this.downloadDetailsText = findViewById(R.id.download_details_text);
}
@Override

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
@@ -21,11 +22,9 @@ public class TypingStatusSender {
private static final long REFRESH_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
private static final long PAUSE_TYPING_TIMEOUT = TimeUnit.SECONDS.toMillis(3);
private final Context context;
private final Map<Long, TimerPair> selfTypingTimers;
public TypingStatusSender(@NonNull Context context) {
this.context = context;
public TypingStatusSender() {
this.selfTypingTimers = new HashMap<>();
}

View File

@@ -3,14 +3,11 @@ package org.thoughtcrime.securesms.components;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.Target;
@@ -29,6 +26,8 @@ import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.io.IOException;
import java.io.InputStream;
@@ -83,32 +82,27 @@ public class ZoomingImageView extends FrameLayout {
Log.i(TAG, "Max texture size: " + maxTextureSize);
new AsyncTask<Void, Void, Pair<Integer, Integer>>() {
@Override
protected @Nullable Pair<Integer, Integer> doInBackground(Void... params) {
if (MediaUtil.isGif(contentType)) return null;
SimpleTask.run(ViewUtil.getActivityLifecycle(this), () -> {
if (MediaUtil.isGif(contentType)) return null;
try {
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
return BitmapUtil.getDimensions(inputStream);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
return null;
}
try {
InputStream inputStream = PartAuthority.getAttachmentStream(context, uri);
return BitmapUtil.getDimensions(inputStream);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
return null;
}
}, dimensions -> {
Log.i(TAG, "Dimensions: " + (dimensions == null ? "(null)" : dimensions.first + ", " + dimensions.second));
protected void onPostExecute(@Nullable Pair<Integer, Integer> dimensions) {
Log.i(TAG, "Dimensions: " + (dimensions == null ? "(null)" : dimensions.first + ", " + dimensions.second));
if (dimensions == null || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) {
Log.i(TAG, "Loading in standard image view...");
setImageViewUri(glideRequests, uri);
} else {
Log.i(TAG, "Loading in subsampling image view...");
setSubsamplingImageViewUri(uri);
}
if (dimensions == null || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) {
Log.i(TAG, "Loading in standard image view...");
setImageViewUri(glideRequests, uri);
} else {
Log.i(TAG, "Loading in subsampling image view...");
setSubsamplingImageViewUri(uri);
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
});
}
private void setImageViewUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {

View File

@@ -39,6 +39,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
private final EmojiEventListener emojiEventListener;
private Controller controller;
private int currentPosition;
public EmojiKeyboardProvider(@NonNull Context context, @Nullable EmojiEventListener emojiEventListener) {
this.context = context;
@@ -66,11 +67,18 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
models.add(recentModel);
models.addAll(EmojiPages.DISPLAY_PAGES);
currentPosition = recentModel.getEmoji().size() > 0 ? 0 : 1;
}
@Override
public void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider) {
presenter.present(this, emojiPagerAdapter, this, this, null, null, recentModel.getEmoji().size() > 0 ? 0 : 1);
presenter.present(this, emojiPagerAdapter, this, this, null, null, currentPosition);
}
@Override
public void setCurrentPosition(int currentPosition) {
this.currentPosition = currentPosition;
}
@Override

View File

@@ -212,6 +212,7 @@ public class MediaKeyboard extends FrameLayout implements InputView,
public void onPageSelected(int i) {
categoryTabAdapter.setActivePosition(i);
categoryTabs.smoothScrollToPosition(i);
providers[providerIndex].setCurrentPosition(i);
}
@Override

View File

@@ -14,6 +14,7 @@ public interface MediaKeyboardProvider {
/** @return True if the click was handled with provider-specific logic, otherwise false */
void requestPresentation(@NonNull Presenter presenter, boolean isSoloProvider);
void setController(@Nullable Controller controller);
void setCurrentPosition(int currentPosition);
interface BackspaceObserver {
void onBackspaceClicked();

View File

@@ -5,7 +5,6 @@ import android.content.Context;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
@@ -14,6 +13,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
import org.thoughtcrime.securesms.util.ListenableFutureTask;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.io.IOException;
import java.io.InputStream;
@@ -56,16 +56,11 @@ public class EmojiPageBitmap {
return null;
};
task = new ListenableFutureTask<>(callable);
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
task.run();
return null;
}
@Override protected void onPostExecute(Void aVoid) {
task = null;
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
SimpleTask.run(() -> {
task.run();
return null;
},
unused -> task = null);
}
return task;
}

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.identity;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
@@ -12,6 +11,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.util.List;
@@ -42,23 +42,15 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
public void onClick(DialogInterface dialog, int which) {
final IdentityDatabase identityDatabase = DatabaseFactory.getIdentityDatabase(getContext());
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
synchronized (SESSION_LOCK) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
}
SimpleTask.run(() -> {
synchronized (SESSION_LOCK) {
for (IdentityRecord identityRecord : untrustedRecords) {
identityDatabase.setApproval(identityRecord.getRecipientId(), true);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
resendListener.onResendMessage();
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
return null;
}, unused -> resendListener.onResendMessage());
}
public interface ResendListener {

View File

@@ -17,7 +17,6 @@ import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.List;
@@ -53,9 +52,9 @@ public class UnverifiedBannerView extends LinearLayout {
private void initialize() {
LayoutInflater.from(getContext()).inflate(R.layout.unverified_banner_view, this, true);
this.container = ViewUtil.findById(this, R.id.container);
this.text = ViewUtil.findById(this, R.id.unverified_text);
this.closeButton = ViewUtil.findById(this, R.id.cancel);
this.container = findViewById(R.id.container);
this.text = findViewById(R.id.unverified_text);
this.closeButton = findViewById(R.id.cancel);
}
public void display(@NonNull final String text,

View File

@@ -18,7 +18,6 @@ import com.google.android.gms.maps.OnMapReadyCallback;
import com.google.android.gms.maps.model.MarkerOptions;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
@@ -47,9 +46,9 @@ public class SignalMapView extends LinearLayout {
setOrientation(LinearLayout.VERTICAL);
LayoutInflater.from(context).inflate(R.layout.signal_map_view, this, true);
this.mapView = ViewUtil.findById(this, R.id.map_view);
this.imageView = ViewUtil.findById(this, R.id.image_view);
this.textView = ViewUtil.findById(this, R.id.address_view);
this.mapView = findViewById(R.id.map_view);
this.imageView = findViewById(R.id.image_view);
this.textView = findViewById(R.id.address_view);
}
public ListenableFuture<Bitmap> display(final SignalPlace place) {

View File

@@ -1,46 +0,0 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import android.view.View;
import android.view.View.OnClickListener;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.SmsUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
public class DefaultSmsReminder extends Reminder {
public DefaultSmsReminder(@NonNull Fragment fragment, short requestCode) {
super(fragment.getString(R.string.reminder_header_sms_default_title),
fragment.getString(R.string.reminder_header_sms_default_text));
final OnClickListener okListener = new OnClickListener() {
@Override
public void onClick(View v) {
TextSecurePreferences.setPromptedDefaultSmsProvider(fragment.requireContext(), true);
fragment.startActivityForResult(SmsUtil.getSmsRoleIntent(fragment.requireContext()), requestCode);
}
};
final OnClickListener dismissListener = new OnClickListener() {
@Override
public void onClick(View v) {
TextSecurePreferences.setPromptedDefaultSmsProvider(fragment.requireContext(), true);
}
};
setOkListener(okListener);
setDismissListener(dismissListener);
}
public static boolean isEligible(Context context) {
final boolean isDefault = Util.isDefaultSmsProvider(context);
if (isDefault) {
TextSecurePreferences.setPromptedDefaultSmsProvider(context, false);
}
return !isDefault && !TextSecurePreferences.hasPromptedDefaultSmsProvider(context);
}
}

View File

@@ -16,7 +16,7 @@ public class GroupsV1MigrationSuggestionsReminder extends Reminder {
public GroupsV1MigrationSuggestionsReminder(@NonNull Context context, @NonNull List<RecipientId> suggestions) {
super(null, context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_members_couldnt_be_added_to_the_new_group, suggestions.size(), suggestions.size()));
addAction(new Action(context.getResources().getQuantityString(R.plurals.GroupsV1MigrationSuggestionsReminder_add_members, suggestions.size()), R.id.reminder_action_gv1_suggestion_add_members));
addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationSuggestionsReminder_not_now), R.id.reminder_action_gv1_suggestion_not_now));
addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationSuggestionsReminder_no_thanks), R.id.reminder_action_gv1_suggestion_no_thanks));
}
@Override

View File

@@ -14,6 +14,7 @@ import android.widget.TextView;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
@@ -78,6 +79,7 @@ public final class ReminderView extends FrameLayout {
}
text.setText(reminder.getText());
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_button_primary_text));
switch (reminder.getImportance()) {
case NORMAL:
@@ -85,6 +87,7 @@ public final class ReminderView extends FrameLayout {
break;
case ERROR:
container.setBackgroundResource(R.drawable.reminder_background_error);
text.setTextColor(ContextCompat.getColor(getContext(), R.color.signal_text_primary));
break;
case TERMINAL:
container.setBackgroundResource(R.drawable.reminder_background_terminal);

View File

@@ -1,51 +0,0 @@
package org.thoughtcrime.securesms.components.reminder;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.view.View;
import android.view.View.OnClickListener;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.InviteActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class ShareReminder extends Reminder {
public ShareReminder(final @NonNull Context context) {
super(context.getString(R.string.reminder_header_share_title),
context.getString(R.string.reminder_header_share_text));
setDismissListener(new OnClickListener() {
@Override public void onClick(View v) {
TextSecurePreferences.setPromptedShare(context, true);
}
});
setOkListener(new OnClickListener() {
@Override public void onClick(View v) {
TextSecurePreferences.setPromptedShare(context, true);
context.startActivity(new Intent(context, InviteActivity.class));
}
});
}
public static boolean isEligible(final @NonNull Context context) {
if (!TextSecurePreferences.isPushRegistered(context) ||
TextSecurePreferences.hasPromptedShare(context))
{
return false;
}
Cursor cursor = null;
try {
cursor = DatabaseFactory.getThreadDatabase(context).getConversationList();
return cursor.getCount() >= 1;
} finally {
if (cursor != null) cursor.close();
}
}
}

View File

@@ -22,7 +22,7 @@ public class SystemSmsImportReminder extends Reminder {
context.startService(intent);
// TODO [greyson] Navigation
Intent nextIntent = new Intent(context, MainActivity.class);
Intent nextIntent = MainActivity.clearTop(context);
Intent activityIntent = new Intent(context, DatabaseMigrationActivity.class);
activityIntent.putExtra("next_intent", nextIntent);
context.startActivity(activityIntent);

View File

@@ -5,6 +5,7 @@ import android.media.AudioManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.support.v4.media.MediaBrowserCompat;
@@ -208,17 +209,20 @@ public class VoiceNoteMediaController implements DefaultLifecycleObserver {
private static class ProgressEventHandler extends Handler {
private final MediaControllerCompat mediaController;
private final MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState;
private final MediaControllerCompat mediaController;
private final MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState;
private ProgressEventHandler(@NonNull MediaControllerCompat mediaController,
@NonNull MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState) {
@NonNull MutableLiveData<VoiceNotePlaybackState> voiceNotePlaybackState)
{
super(Looper.getMainLooper());
this.mediaController = mediaController;
this.voiceNotePlaybackState = voiceNotePlaybackState;
}
@Override
public void handleMessage(Message msg) {
public void handleMessage(@NonNull Message msg) {
MediaMetadataCompat mediaMetadataCompat = mediaController.getMetadata();
if (isPlayerActive(mediaController.getPlaybackState()) &&
mediaMetadataCompat != null &&

View File

@@ -64,7 +64,6 @@ final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.V
static class ViewHolder extends RecyclerView.ViewHolder implements CompoundButton.OnCheckedChangeListener {
private final TextView textView;
private final RadioButton radioButton;
private final Consumer<Integer> onPressed;
@@ -72,16 +71,14 @@ final class AudioOutputAdapter extends RecyclerView.Adapter<AudioOutputAdapter.V
public ViewHolder(@NonNull View itemView, @NonNull Consumer<Integer> onPressed) {
super(itemView);
this.textView = itemView.findViewById(R.id.text);
this.radioButton = itemView.findViewById(R.id.radio);
this.onPressed = onPressed;
}
@CallSuper
void bind(@NonNull WebRtcAudioOutput audioOutput, @Nullable WebRtcAudioOutput selected) {
textView.setText(audioOutput.getLabelRes());
textView.setCompoundDrawablesRelativeWithIntrinsicBounds(audioOutput.getIconRes(), 0, 0, 0);
radioButton.setText(audioOutput.getLabelRes());
radioButton.setCompoundDrawablesRelativeWithIntrinsicBounds(audioOutput.getIconRes(), 0, 0, 0);
radioButton.setOnCheckedChangeListener(null);
radioButton.setChecked(audioOutput == selected);
radioButton.setOnCheckedChangeListener(this);

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
@@ -21,19 +22,19 @@ import java.util.Set;
*/
public final class CallParticipantListUpdate {
private final Set<Holder> added;
private final Set<Holder> removed;
private final Set<Wrapper> added;
private final Set<Wrapper> removed;
CallParticipantListUpdate(@NonNull Set<Holder> added, @NonNull Set<Holder> removed) {
CallParticipantListUpdate(@NonNull Set<Wrapper> added, @NonNull Set<Wrapper> removed) {
this.added = added;
this.removed = removed;
}
public @NonNull Set<Holder> getAdded() {
public @NonNull Set<Wrapper> getAdded() {
return added;
}
public @NonNull Set<Holder> getRemoved() {
public @NonNull Set<Wrapper> getRemoved() {
return removed;
}
@@ -68,66 +69,47 @@ public final class CallParticipantListUpdate {
public static @NonNull CallParticipantListUpdate computeDeltaUpdate(@NonNull List<CallParticipant> oldList,
@NonNull List<CallParticipant> newList)
{
Set<CallParticipantId> primaries = getPrimaries(oldList, newList);
Set<CallParticipantListUpdate.Holder> oldParticipants = Stream.of(oldList)
Set<CallParticipantListUpdate.Wrapper> oldParticipants = Stream.of(oldList)
.filter(p -> p.getCallParticipantId().getDemuxId() != CallParticipantId.DEFAULT_ID)
.map(p -> createHolder(p, primaries.contains(p.getCallParticipantId())))
.map(CallParticipantListUpdate::createWrapper)
.collect(Collectors.toSet());
Set<CallParticipantListUpdate.Holder> newParticipants = Stream.of(newList)
Set<CallParticipantListUpdate.Wrapper> newParticipants = Stream.of(newList)
.filter(p -> p.getCallParticipantId().getDemuxId() != CallParticipantId.DEFAULT_ID)
.map(p -> createHolder(p, primaries.contains(p.getCallParticipantId())))
.map(CallParticipantListUpdate::createWrapper)
.collect(Collectors.toSet());
Set<CallParticipantListUpdate.Holder> added = SetUtil.difference(newParticipants, oldParticipants);
Set<CallParticipantListUpdate.Holder> removed = SetUtil.difference(oldParticipants, newParticipants);
Set<CallParticipantListUpdate.Wrapper> added = SetUtil.difference(newParticipants, oldParticipants);
Set<CallParticipantListUpdate.Wrapper> removed = SetUtil.difference(oldParticipants, newParticipants);
return new CallParticipantListUpdate(added, removed);
}
static Holder createHolder(@NonNull CallParticipant callParticipant, boolean isPrimary) {
return new Holder(callParticipant.getCallParticipantId(), callParticipant.getRecipient(), isPrimary);
@VisibleForTesting
static Wrapper createWrapper(@NonNull CallParticipant callParticipant) {
return new Wrapper(callParticipant);
}
private static @NonNull Set<CallParticipantId> getPrimaries(@NonNull List<CallParticipant> oldList, @NonNull List<CallParticipant> newList) {
return Stream.concat(Stream.of(oldList), Stream.of(newList))
.map(CallParticipant::getCallParticipantId)
.distinctBy(CallParticipantId::getRecipientId)
.collect(Collectors.toSet());
}
static final class Wrapper {
private final CallParticipant callParticipant;
static final class Holder {
private final CallParticipantId callParticipantId;
private final Recipient recipient;
private final boolean isPrimary;
private Holder(@NonNull CallParticipantId callParticipantId, @NonNull Recipient recipient, boolean isPrimary) {
this.callParticipantId = callParticipantId;
this.recipient = recipient;
this.isPrimary = isPrimary;
private Wrapper(@NonNull CallParticipant callParticipant) {
this.callParticipant = callParticipant;
}
public @NonNull Recipient getRecipient() {
return recipient;
}
/**
* Denotes whether this was the first detected instance of this recipient when generating an update. See
* {@link CallParticipantListUpdate#computeDeltaUpdate(List, List)}
*/
public boolean isPrimary() {
return isPrimary;
public @NonNull CallParticipant getCallParticipant() {
return callParticipant;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Holder holder = (Holder) o;
return callParticipantId.equals(holder.callParticipantId);
Wrapper wrapper = (Wrapper) o;
return callParticipant.getCallParticipantId().equals(wrapper.callParticipant.getCallParticipantId());
}
@Override
public int hashCode() {
return Objects.hash(callParticipantId);
return Objects.hash(callParticipant.getCallParticipantId());
}
}
}

View File

@@ -29,10 +29,12 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* Encapsulates views needed to show a call participant including their
@@ -42,12 +44,15 @@ public class CallParticipantView extends ConstraintLayout {
private static final FallbackPhotoProvider FALLBACK_PHOTO_PROVIDER = new FallbackPhotoProvider();
private static final int SMALL_AVATAR = ViewUtil.dpToPx(96);
private static final int LARGE_AVATAR = ViewUtil.dpToPx(112);
private static final long DELAY_SHOWING_MISSING_MEDIA_KEYS = TimeUnit.SECONDS.toMillis(5);
private static final int SMALL_AVATAR = ViewUtil.dpToPx(96);
private static final int LARGE_AVATAR = ViewUtil.dpToPx(112);
private RecipientId recipientId;
private boolean infoMode;
private Runnable missingMediaKeysUpdater;
private AppCompatImageView backgroundAvatar;
private AvatarImageView avatar;
private TextureViewRenderer renderer;
private ImageView pipAvatar;
@@ -74,14 +79,16 @@ public class CallParticipantView extends ConstraintLayout {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
avatar = findViewById(R.id.call_participant_item_avatar);
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
renderer = findViewById(R.id.call_participant_renderer);
audioMuted = findViewById(R.id.call_participant_mic_muted);
infoOverlay = findViewById(R.id.call_participant_info_overlay);
infoIcon = findViewById(R.id.call_participant_info_icon);
infoMessage = findViewById(R.id.call_participant_info_message);
infoMoreInfo = findViewById(R.id.call_participant_info_more_info);
backgroundAvatar = findViewById(R.id.call_participant_background_avatar);
avatar = findViewById(R.id.call_participant_item_avatar);
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
renderer = findViewById(R.id.call_participant_renderer);
audioMuted = findViewById(R.id.call_participant_mic_muted);
infoOverlay = findViewById(R.id.call_participant_info_overlay);
infoIcon = findViewById(R.id.call_participant_info_icon);
infoMessage = findViewById(R.id.call_participant_info_message);
infoMoreInfo = findViewById(R.id.call_participant_info_more_info);
avatar.setFallbackPhotoProvider(FALLBACK_PHOTO_PROVIDER);
useLargeAvatar();
@@ -98,7 +105,7 @@ public class CallParticipantView extends ConstraintLayout {
void setCallParticipant(@NonNull CallParticipant participant) {
boolean participantChanged = recipientId == null || !recipientId.equals(participant.getRecipient().getId());
recipientId = participant.getRecipient().getId();
infoMode = participant.getRecipient().isBlocked() || !participant.isMediaKeysReceived();
infoMode = participant.getRecipient().isBlocked() || isMissingMediaKeys(participant);
if (infoMode) {
renderer.setVisibility(View.GONE);
@@ -139,12 +146,34 @@ public class CallParticipantView extends ConstraintLayout {
if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) {
avatar.setAvatarUsingProfile(participant.getRecipient());
AvatarUtil.loadBlurredIconIntoViewBackground(participant.getRecipient(), this, true);
AvatarUtil.loadBlurredIconIntoImageView(participant.getRecipient(), backgroundAvatar);
setPipAvatar(participant.getRecipient());
contactPhoto = participant.getRecipient().getContactPhoto();
}
}
private boolean isMissingMediaKeys(@NonNull CallParticipant participant) {
if (missingMediaKeysUpdater != null) {
Util.cancelRunnableOnMain(missingMediaKeysUpdater);
missingMediaKeysUpdater = null;
}
if (!participant.isMediaKeysReceived()) {
long time = System.currentTimeMillis() - participant.getAddedToCallTime();
if (time > DELAY_SHOWING_MISSING_MEDIA_KEYS) {
return true;
} else {
missingMediaKeysUpdater = () -> {
if (recipientId.equals(participant.getRecipient().getId())) {
setCallParticipant(participant);
}
};
Util.runOnMainDelayed(missingMediaKeysUpdater, DELAY_SHOWING_MISSING_MEDIA_KEYS - time);
}
}
return false;
}
void setRenderInPip(boolean shouldRenderInPip) {
if (infoMode) {
infoMessage.setVisibility(shouldRenderInPip ? View.GONE : View.VISIBLE);

View File

@@ -30,6 +30,7 @@ public class CallParticipantsLayout extends FlexboxLayout {
private static final int CORNER_RADIUS = ViewUtil.dpToPx(10);
private List<CallParticipant> callParticipants = Collections.emptyList();
private CallParticipant focusedParticipant = null;
private boolean shouldRenderInPip;
public CallParticipantsLayout(@NonNull Context context) {
@@ -44,9 +45,10 @@ public class CallParticipantsLayout extends FlexboxLayout {
super(context, attrs, defStyleAttr);
}
void update(@NonNull List<CallParticipant> callParticipants, boolean shouldRenderInPip) {
this.callParticipants = callParticipants;
this.shouldRenderInPip = shouldRenderInPip;
void update(@NonNull List<CallParticipant> callParticipants, @NonNull CallParticipant focusedParticipant, boolean shouldRenderInPip) {
this.callParticipants = callParticipants;
this.focusedParticipant = focusedParticipant;
this.shouldRenderInPip = shouldRenderInPip;
updateLayout();
}
@@ -55,7 +57,7 @@ public class CallParticipantsLayout extends FlexboxLayout {
if (shouldRenderInPip && Util.hasItems(callParticipants)) {
updateChildrenCount(1);
update(0, 1, callParticipants.get(0));
update(0, 1, focusedParticipant);
} else {
int count = callParticipants.size();
updateChildrenCount(count);

View File

@@ -31,8 +31,10 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
private final AvatarImageView avatarImageView;
private final TextView descriptionTextView;
private final Set<CallParticipantListUpdate.Holder> pendingAdditions = new HashSet<>();
private final Set<CallParticipantListUpdate.Holder> pendingRemovals = new HashSet<>();
private final Set<CallParticipantListUpdate.Wrapper> pendingAdditions = new HashSet<>();
private final Set<CallParticipantListUpdate.Wrapper> pendingRemovals = new HashSet<>();
private boolean isEnabled = true;
public CallParticipantsListUpdatePopupWindow(@NonNull ViewGroup parent) {
super(LayoutInflater.from(parent.getContext()).inflate(R.layout.call_participant_list_update, parent, false),
@@ -59,6 +61,14 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
}
}
public void setEnabled(boolean isEnabled) {
this.isEnabled = isEnabled;
if (!isEnabled) {
dismiss();
}
}
private void showPending() {
if (!pendingAdditions.isEmpty()) {
showAdditions();
@@ -82,6 +92,10 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
}
private void show() {
if (!isEnabled) {
return;
}
showAtLocation(parent, Gravity.TOP | Gravity.START, 0, 0);
measureChild();
update();
@@ -98,18 +112,18 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
avatarImageView.setVisibility(recipient == null ? View.GONE : View.VISIBLE);
}
private void setDescription(@NonNull Set<CallParticipantListUpdate.Holder> holders, boolean isAdded) {
if (holders.isEmpty()) {
private void setDescription(@NonNull Set<CallParticipantListUpdate.Wrapper> wrappers, boolean isAdded) {
if (wrappers.isEmpty()) {
descriptionTextView.setText("");
} else {
setDescriptionForRecipients(holders, isAdded);
setDescriptionForRecipients(wrappers, isAdded);
}
}
private void setDescriptionForRecipients(@NonNull Set<CallParticipantListUpdate.Holder> recipients, boolean isAdded) {
Iterator<CallParticipantListUpdate.Holder> iterator = recipients.iterator();
Context context = getContentView().getContext();
String description;
private void setDescriptionForRecipients(@NonNull Set<CallParticipantListUpdate.Wrapper> recipients, boolean isAdded) {
Iterator<CallParticipantListUpdate.Wrapper> iterator = recipients.iterator();
Context context = getContentView().getContext();
String description;
switch (recipients.size()) {
case 0:
@@ -130,22 +144,14 @@ public class CallParticipantsListUpdatePopupWindow extends PopupWindow {
descriptionTextView.setText(description);
}
private @NonNull Recipient getNextRecipient(@NonNull Iterator<CallParticipantListUpdate.Holder> holderIterator) {
return holderIterator.next().getRecipient();
private @NonNull Recipient getNextRecipient(@NonNull Iterator<CallParticipantListUpdate.Wrapper> wrapperIterator) {
return wrapperIterator.next().getCallParticipant().getRecipient();
}
private @NonNull String getNextDisplayName(@NonNull Iterator<CallParticipantListUpdate.Holder> holderIterator) {
CallParticipantListUpdate.Holder holder = holderIterator.next();
Recipient recipient = holder.getRecipient();
private @NonNull String getNextDisplayName(@NonNull Iterator<CallParticipantListUpdate.Wrapper> wrapperIterator) {
CallParticipantListUpdate.Wrapper wrapper = wrapperIterator.next();
if (recipient.isSelf()) {
return getContentView().getContext().getString(R.string.CallParticipantsListUpdatePopupWindow__you_on_another_device);
} else if (holder.isPrimary()) {
return recipient.getDisplayName(getContentView().getContext());
} else {
return getContentView().getContext().getString(R.string.CallParticipantsListUpdatePopupWindow__s_on_another_device,
recipient.getDisplayName(getContentView().getContext()));
}
return wrapper.getCallParticipant().getRecipientDisplayName(getContentView().getContext());
}
private static @StringRes int getOneMemberDescriptionResourceId(boolean isAdded) {

View File

@@ -6,12 +6,14 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.ComparatorCompat;
import com.annimon.stream.OptionalLong;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection;
import java.util.ArrayList;
import java.util.Collections;
@@ -28,33 +30,36 @@ public final class CallParticipantsState {
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
WebRtcViewModel.GroupCallState.IDLE,
Collections.emptyList(),
new ParticipantCollection(SMALL_GROUP_MAX),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
null,
WebRtcLocalRenderState.GONE,
false,
false,
false);
false,
OptionalLong.empty());
private final WebRtcViewModel.State callState;
private final WebRtcViewModel.GroupCallState groupCallState;
private final List<CallParticipant> remoteParticipants;
private final ParticipantCollection remoteParticipants;
private final CallParticipant localParticipant;
private final CallParticipant focusedParticipant;
private final WebRtcLocalRenderState localRenderState;
private final boolean isInPipMode;
private final boolean showVideoForOutgoing;
private final boolean isViewingFocusedParticipant;
private final OptionalLong remoteDevicesCount;
public CallParticipantsState(@NonNull WebRtcViewModel.State callState,
@NonNull WebRtcViewModel.GroupCallState groupCallState,
@NonNull List<CallParticipant> remoteParticipants,
@NonNull ParticipantCollection remoteParticipants,
@NonNull CallParticipant localParticipant,
@Nullable CallParticipant focusedParticipant,
@NonNull WebRtcLocalRenderState localRenderState,
boolean isInPipMode,
boolean showVideoForOutgoing,
boolean isViewingFocusedParticipant)
boolean isViewingFocusedParticipant,
OptionalLong remoteDevicesCount)
{
this.callState = callState;
this.groupCallState = groupCallState;
@@ -65,6 +70,7 @@ public final class CallParticipantsState {
this.isInPipMode = isInPipMode;
this.showVideoForOutgoing = showVideoForOutgoing;
this.isViewingFocusedParticipant = isViewingFocusedParticipant;
this.remoteDevicesCount = remoteDevicesCount;
}
public @NonNull WebRtcViewModel.State getCallState() {
@@ -76,26 +82,20 @@ public final class CallParticipantsState {
}
public @NonNull List<CallParticipant> getGridParticipants() {
if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
return getAllRemoteParticipants().subList(0, SMALL_GROUP_MAX);
} else {
return getAllRemoteParticipants();
}
return remoteParticipants.getGridParticipants();
}
public @NonNull List<CallParticipant> getListParticipants() {
List<CallParticipant> listParticipants = new ArrayList<>();
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
listParticipants.addAll(getAllRemoteParticipants().subList(1, getAllRemoteParticipants().size()));
} else if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
listParticipants.addAll(getAllRemoteParticipants().subList(SMALL_GROUP_MAX, getAllRemoteParticipants().size()));
listParticipants.addAll(getAllRemoteParticipants());
listParticipants.remove(focusedParticipant);
} else {
return Collections.emptyList();
listParticipants.addAll(remoteParticipants.getListParticipants());
}
listParticipants.add(CallParticipant.EMPTY);
Collections.reverse(listParticipants);
return listParticipants;
@@ -107,26 +107,26 @@ public final class CallParticipantsState {
return context.getString(R.string.WebRtcCallView__no_one_else_is_here);
case 1:
if (callState == WebRtcViewModel.State.CALL_PRE_JOIN && groupCallState.isNotIdle()) {
return context.getString(R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants.get(0).getRecipient().getShortDisplayName(context));
return context.getString(R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants.get(0).getShortRecipientDisplayName(context));
} else {
return remoteParticipants.get(0).getRecipient().getDisplayName(context);
return remoteParticipants.get(0).getRecipientDisplayName(context);
}
case 2:
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
remoteParticipants.get(1).getRecipient().getShortDisplayName(context));
remoteParticipants.get(0).getShortRecipientDisplayName(context),
remoteParticipants.get(1).getShortRecipientDisplayName(context));
default:
int others = remoteParticipants.size() - 2;
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
others,
remoteParticipants.get(0).getRecipient().getShortDisplayName(context),
remoteParticipants.get(1).getRecipient().getShortDisplayName(context),
remoteParticipants.get(0).getShortRecipientDisplayName(context),
remoteParticipants.get(1).getShortRecipientDisplayName(context),
others);
}
}
public @NonNull List<CallParticipant> getAllRemoteParticipants() {
return remoteParticipants;
return remoteParticipants.getAllParticipants();
}
public @NonNull CallParticipant getLocalParticipant() {
@@ -153,6 +153,17 @@ public final class CallParticipantsState {
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
}
public @NonNull OptionalLong getRemoteDevicesCount() {
return remoteDevicesCount;
}
public @NonNull OptionalLong getParticipantCount() {
boolean includeSelf = groupCallState == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
return remoteDevicesCount.map(l -> l + (includeSelf ? 1L : 0L))
.or(() -> includeSelf ? OptionalLong.of(1L) : OptionalLong.empty());
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState,
@NonNull WebRtcViewModel webRtcViewModel,
boolean enableVideo)
@@ -170,7 +181,8 @@ public final class CallParticipantsState {
webRtcViewModel.getGroupState().isNotIdle(),
webRtcViewModel.getState(),
webRtcViewModel.getRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
oldState.isViewingFocusedParticipant,
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
List<CallParticipant> participantsByLastSpoke = new ArrayList<>(webRtcViewModel.getRemoteParticipants());
Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke())));
@@ -179,13 +191,14 @@ public final class CallParticipantsState {
return new CallParticipantsState(webRtcViewModel.getState(),
webRtcViewModel.getGroupState(),
webRtcViewModel.getRemoteParticipants(),
oldState.remoteParticipants.getNext(webRtcViewModel.getRemoteParticipants()),
webRtcViewModel.getLocalParticipant(),
focused,
localRenderState,
oldState.isInPipMode,
newShowVideoForOutgoing,
oldState.isViewingFocusedParticipant);
oldState.isViewingFocusedParticipant,
webRtcViewModel.getRemoteDevicesCount());
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, boolean isInPip) {
@@ -195,7 +208,8 @@ public final class CallParticipantsState {
oldState.getGroupCallState().isNotIdle(),
oldState.callState,
oldState.getAllRemoteParticipants().size(),
oldState.isViewingFocusedParticipant);
oldState.isViewingFocusedParticipant,
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
@@ -207,7 +221,30 @@ public final class CallParticipantsState {
localRenderState,
isInPip,
oldState.showVideoForOutgoing,
oldState.isViewingFocusedParticipant);
oldState.isViewingFocusedParticipant,
oldState.remoteDevicesCount);
}
public static @NonNull CallParticipantsState setExpanded(@NonNull CallParticipantsState oldState, boolean expanded) {
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
oldState.getGroupCallState().isNotIdle(),
oldState.callState,
oldState.getAllRemoteParticipants().size(),
oldState.isViewingFocusedParticipant,
expanded);
return new CallParticipantsState(oldState.callState,
oldState.groupCallState,
oldState.remoteParticipants,
oldState.localParticipant,
oldState.focusedParticipant,
localRenderState,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
oldState.isViewingFocusedParticipant,
oldState.remoteDevicesCount);
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
@@ -219,7 +256,8 @@ public final class CallParticipantsState {
oldState.getGroupCallState().isNotIdle(),
oldState.callState,
oldState.getAllRemoteParticipants().size(),
selectedPage == SelectedPage.FOCUSED);
selectedPage == SelectedPage.FOCUSED,
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
return new CallParticipantsState(oldState.callState,
oldState.groupCallState,
@@ -229,7 +267,8 @@ public final class CallParticipantsState {
localRenderState,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
selectedPage == SelectedPage.FOCUSED);
selectedPage == SelectedPage.FOCUSED,
oldState.remoteDevicesCount);
}
private static @NonNull WebRtcLocalRenderState determineLocalRenderMode(@NonNull CallParticipant localParticipant,
@@ -238,12 +277,15 @@ public final class CallParticipantsState {
boolean isNonIdleGroupCall,
@NonNull WebRtcViewModel.State callState,
int numberOfRemoteParticipants,
boolean isViewingFocusedParticipant)
boolean isViewingFocusedParticipant,
boolean isExpanded)
{
boolean displayLocal = (numberOfRemoteParticipants == 0 || !isInPip) && (isNonIdleGroupCall || localParticipant.isVideoEnabled());
WebRtcLocalRenderState localRenderState = WebRtcLocalRenderState.GONE;
if (displayLocal || showVideoForOutgoing) {
if (isExpanded && (localParticipant.isVideoEnabled() || isNonIdleGroupCall)) {
return WebRtcLocalRenderState.EXPANDED;
} else if (displayLocal || showVideoForOutgoing) {
if (callState == WebRtcViewModel.State.CALL_CONNECTED) {
if (isViewingFocusedParticipant || numberOfRemoteParticipants > 1) {
localRenderState = WebRtcLocalRenderState.SMALLER_RECTANGLE;

View File

@@ -0,0 +1,197 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
/**
* Helps manage the expansion and shrinking of the in-app pip.
*/
@MainThread
final class PictureInPictureExpansionHelper {
private State state = State.IS_SHRUNKEN;
public boolean isExpandedOrExpanding() {
return state == State.IS_EXPANDED || state == State.IS_EXPANDING;
}
public boolean isShrunkenOrShrinking() {
return state == State.IS_SHRUNKEN || state == State.IS_SHRINKING;
}
public void expand(@NonNull View toExpand, @NonNull Callback callback) {
if (isExpandedOrExpanding()) {
return;
}
performExpandAnimation(toExpand, new Callback() {
@Override
public void onAnimationWillStart() {
state = State.IS_EXPANDING;
callback.onAnimationWillStart();
}
@Override
public void onPictureInPictureExpanded() {
callback.onPictureInPictureExpanded();
}
@Override
public void onPictureInPictureNotVisible() {
callback.onPictureInPictureNotVisible();
}
@Override
public void onAnimationHasFinished() {
state = State.IS_EXPANDED;
callback.onAnimationHasFinished();
}
});
}
public void shrink(@NonNull View toExpand, @NonNull Callback callback) {
if (isShrunkenOrShrinking()) {
return;
}
performShrinkAnimation(toExpand, new Callback() {
@Override
public void onAnimationWillStart() {
state = State.IS_SHRINKING;
callback.onAnimationWillStart();
}
@Override
public void onPictureInPictureExpanded() {
callback.onPictureInPictureExpanded();
}
@Override
public void onPictureInPictureNotVisible() {
callback.onPictureInPictureNotVisible();
}
@Override
public void onAnimationHasFinished() {
state = State.IS_SHRUNKEN;
callback.onAnimationHasFinished();
}
});
}
private void performExpandAnimation(@NonNull View target, @NonNull Callback callback) {
ViewGroup parent = (ViewGroup) target.getParent();
float x = target.getX();
float y = target.getY();
float scaleX = parent.getMeasuredWidth() / (float) target.getMeasuredWidth();
float scaleY = parent.getMeasuredHeight() / (float) target.getMeasuredHeight();
float scale = Math.max(scaleX, scaleY);
callback.onAnimationWillStart();
target.animate()
.setDuration(200)
.x((parent.getMeasuredWidth() - target.getMeasuredWidth()) / 2f)
.y((parent.getMeasuredHeight() - target.getMeasuredHeight()) / 2f)
.scaleX(scale)
.scaleY(scale)
.withEndAction(() -> {
callback.onPictureInPictureExpanded();
target.animate()
.setDuration(100)
.alpha(0f)
.withEndAction(() -> {
callback.onPictureInPictureNotVisible();
target.setX(x);
target.setY(y);
target.setScaleX(0f);
target.setScaleY(0f);
target.setAlpha(1f);
target.animate()
.setDuration(200)
.scaleX(1f)
.scaleY(1f)
.withEndAction(callback::onAnimationHasFinished);
});
});
}
private void performShrinkAnimation(@NonNull View target, @NonNull Callback callback) {
ViewGroup parent = (ViewGroup) target.getParent();
float x = target.getX();
float y = target.getY();
float scaleX = parent.getMeasuredWidth() / (float) target.getMeasuredWidth();
float scaleY = parent.getMeasuredHeight() / (float) target.getMeasuredHeight();
float scale = Math.max(scaleX, scaleY);
callback.onAnimationWillStart();
target.animate()
.setDuration(200)
.scaleX(0f)
.scaleY(0f)
.withEndAction(() -> {
target.setX((parent.getMeasuredWidth() - target.getMeasuredWidth()) / 2f);
target.setY((parent.getMeasuredHeight() - target.getMeasuredHeight()) / 2f);
target.setAlpha(0f);
target.setScaleX(scale);
target.setScaleY(scale);
callback.onPictureInPictureNotVisible();
target.animate()
.setDuration(100)
.alpha(1f)
.withEndAction(() -> {
callback.onPictureInPictureExpanded();
target.animate()
.scaleX(1f)
.scaleY(1f)
.x(x)
.y(y)
.withEndAction(callback::onAnimationHasFinished);
});
});
}
enum State {
IS_EXPANDING,
IS_EXPANDED,
IS_SHRINKING,
IS_SHRUNKEN
}
public interface Callback {
/**
* Called when an animation (shrink or expand) will begin. This happens before any animation
* is executed.
*/
void onAnimationWillStart();
/**
* Called when the PiP is covering the whole screen. This is when any staging / teardown of the
* large local renderer should occur.
*/
void onPictureInPictureExpanded();
/**
* Called when the PiP is not visible on the screen anymore. This is when any staging / teardown
* of the pip should occur.
*/
void onPictureInPictureNotVisible();
/**
* Called when the animation is complete. Useful for e.g. adjusting the pip's final location to
* make sure it is respecting the screen space available.
*/
void onAnimationHasFinished();
}
}

View File

@@ -222,8 +222,9 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
@Override
public boolean onSingleTapUp(MotionEvent e) {
child.performClick();
isDragging = false;
child.performClick();
return true;
}

View File

@@ -113,7 +113,7 @@ public class WebRtcAudioOutputToggleButton extends AppCompatImageView {
rv.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
rv.setAdapter(adapter);
picker = new AlertDialog.Builder(getContext())
picker = new AlertDialog.Builder(getContext(), R.style.Theme_Signal_AlertDialog_Dark_Cornered)
.setTitle(R.string.WebRtcAudioOutputToggle__audio_output)
.setView(rv)
.setCancelable(true)

View File

@@ -11,34 +11,42 @@ import java.util.Objects;
class WebRtcCallParticipantsPage {
private final List<CallParticipant> callParticipants;
private final CallParticipant focusedParticipant;
private final boolean isSpeaker;
private final boolean isRenderInPip;
static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List<CallParticipant> callParticipants,
@NonNull CallParticipant focusedParticipant,
boolean isRenderInPip)
{
return new WebRtcCallParticipantsPage(callParticipants, false, isRenderInPip);
return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip);
}
static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant,
boolean isRenderInPip)
{
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), true, isRenderInPip);
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), singleParticipant, true, isRenderInPip);
}
private WebRtcCallParticipantsPage(@NonNull List<CallParticipant> callParticipants,
@NonNull CallParticipant focusedParticipant,
boolean isSpeaker,
boolean isRenderInPip)
{
this.callParticipants = callParticipants;
this.isSpeaker = isSpeaker;
this.isRenderInPip = isRenderInPip;
this.callParticipants = callParticipants;
this.focusedParticipant = focusedParticipant;
this.isSpeaker = isSpeaker;
this.isRenderInPip = isRenderInPip;
}
public @NonNull List<CallParticipant> getCallParticipants() {
return callParticipants;
}
public @NonNull CallParticipant getFocusedParticipant() {
return focusedParticipant;
}
public boolean isRenderInPip() {
return isRenderInPip;
}
@@ -54,11 +62,12 @@ class WebRtcCallParticipantsPage {
WebRtcCallParticipantsPage that = (WebRtcCallParticipantsPage) o;
return isSpeaker == that.isSpeaker &&
isRenderInPip == that.isRenderInPip &&
focusedParticipant.equals(that.focusedParticipant) &&
callParticipants.equals(that.callParticipants);
}
@Override
public int hashCode() {
return Objects.hash(callParticipants, isSpeaker);
return Objects.hash(callParticipants, isSpeaker, focusedParticipant, isRenderInPip);
}
}

View File

@@ -84,7 +84,7 @@ class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipa
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantsLayout.update(page.getCallParticipants(), page.isRenderInPip());
callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip());
}
}

View File

@@ -60,6 +60,7 @@ class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant,
@Override
void bind(@NonNull CallParticipant callParticipant) {
callParticipantView.setCallParticipant(callParticipant);
callParticipantView.setRenderInPip(true);
}
}

View File

@@ -1,14 +1,24 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -47,6 +57,7 @@ import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.util.BlurTransformation;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.Stub;
import org.webrtc.RendererCommon;
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
@@ -93,11 +104,16 @@ public class WebRtcCallView extends FrameLayout {
private Toolbar toolbar;
private MaterialButton startCall;
private TextView participantCount;
private Stub<FrameLayout> groupCallSpeakerHint;
private Stub<View> groupCallFullStub;
private View errorButton;
private int pagerBottomMarginDp;
private boolean controlsVisible = true;
private WebRtcCallParticipantsPagerAdapter pagerAdapter;
private WebRtcCallParticipantsRecyclerAdapter recyclerAdapter;
private PictureInPictureExpansionHelper pictureInPictureExpansionHelper;
private final Set<View> incomingCallViews = new HashSet<>();
private final Set<View> topViews = new HashSet<>();
@@ -116,7 +132,7 @@ public class WebRtcCallView extends FrameLayout {
public WebRtcCallView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(context).inflate(R.layout.webrtc_call_view, this, true);
inflate(context, R.layout.webrtc_call_view, this);
}
@SuppressWarnings("CodeBlock2Expr")
@@ -148,12 +164,14 @@ public class WebRtcCallView extends FrameLayout {
callParticipantsRecycler = findViewById(R.id.call_screen_participants_recycler);
toolbar = findViewById(R.id.call_screen_toolbar);
startCall = findViewById(R.id.call_screen_start_call_start_call);
errorButton = findViewById(R.id.call_screen_error_cancel);
groupCallSpeakerHint = new Stub<>(findViewById(R.id.call_screen_group_call_speaker_hint));
groupCallFullStub = new Stub<>(findViewById(R.id.group_call_call_full_view));
View topGradient = findViewById(R.id.call_screen_header_gradient);
View decline = findViewById(R.id.call_screen_decline_call);
View answerLabel = findViewById(R.id.call_screen_answer_call_label);
View declineLabel = findViewById(R.id.call_screen_decline_call_label);
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
View cancelStartCall = findViewById(R.id.call_screen_start_call_cancel);
callParticipantsPager.setPageTransformer(new MarginPageTransformer(ViewUtil.dpToPx(4)));
@@ -205,7 +223,14 @@ public class WebRtcCallView extends FrameLayout {
answer.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallPressed));
answerWithAudio.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onAcceptCallWithVoiceOnlyPressed));
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
pictureInPictureGestureHelper = PictureInPictureGestureHelper.applyTo(smallLocalRenderFrame);
pictureInPictureExpansionHelper = new PictureInPictureExpansionHelper();
smallLocalRenderFrame.setOnClickListener(v -> {
if (controlsListener != null) {
controlsListener.onLocalPictureInPictureClicked();
}
});
startCall.setOnClickListener(v -> {
if (controlsListener != null) {
@@ -220,8 +245,11 @@ public class WebRtcCallView extends FrameLayout {
largeLocalRenderNoVideoAvatar.setAlpha(0.6f);
largeLocalRenderNoVideoAvatar.setColorFilter(new ColorMatrixColorFilter(greyScaleMatrix));
int statusBarHeight = ViewUtil.getStatusBarHeight(this);
statusBarGuideline.setGuidelineBegin(statusBarHeight);
errorButton.setOnClickListener(v -> {
if (controlsListener != null) {
controlsListener.onCancelStartCall();
}
});
}
@Override
@@ -233,6 +261,26 @@ public class WebRtcCallView extends FrameLayout {
}
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Guideline statusBarGuideline = findViewById(R.id.call_screen_status_bar_guideline);
Guideline navigationBarGuideline = findViewById(R.id.call_screen_navigation_bar_guideline);
statusBarGuideline.setGuidelineBegin(insets.top);
navigationBarGuideline.setGuidelineEnd(insets.bottom);
return true;
}
@Override
public void onWindowSystemUiVisibilityChanged(int visible) {
if ((visible & SYSTEM_UI_FLAG_HIDE_NAVIGATION) == 0) {
pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop());
} else {
pictureInPictureGestureHelper.clearVerticalBoundaries();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
@@ -251,26 +299,28 @@ public class WebRtcCallView extends FrameLayout {
List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
if (!state.getGridParticipants().isEmpty()) {
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.isInPipMode()));
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode()));
}
if (state.getFocusedParticipant() != null && state.getAllRemoteParticipants().size() > 1) {
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
}
if (state.getGroupCallState().isConnected()) {
if ((state.getGroupCallState().isNotIdle() && state.getRemoteDevicesCount().orElse(0) > 0) || state.getGroupCallState().isConnected()) {
recipientName.setText(state.getRemoteParticipantsDescription(getContext()));
} else if (state.getGroupCallState().isNotIdle()) {
recipientName.setText(getContext().getString(R.string.WebRtcCallView__s_group_call, Recipient.resolved(recipientId).getDisplayName(getContext())));
}
if (state.getGroupCallState().isNotIdle() && participantCount != null) {
boolean includeSelf = state.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
participantCount.setText(String.valueOf(state.getAllRemoteParticipants().size() + (includeSelf ? 1 : 0)));
participantCount.setText(state.getParticipantCount()
.mapToObj(String::valueOf).orElse("\u2014"));
participantCount.setEnabled(state.getParticipantCount().isPresent());
}
pagerAdapter.submitList(pages);
recyclerAdapter.submitList(state.getListParticipants());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant());
updateLocalCallParticipant(state.getLocalRenderState(), state.getLocalParticipant(), state.getFocusedParticipant());
if (state.isLargeVideoGroup() && !state.isInPipMode()) {
layoutParticipantsForLargeCount();
@@ -279,7 +329,7 @@ public class WebRtcCallView extends FrameLayout {
}
}
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) {
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
@@ -290,9 +340,18 @@ public class WebRtcCallView extends FrameLayout {
largeLocalRender.init(localCallParticipant.getVideoSink().getEglBase());
}
smallLocalRender.setCallParticipant(localCallParticipant);
smallLocalRender.setRenderInPip(true);
videoToggle.setChecked(localCallParticipant.isVideoEnabled(), false);
smallLocalRender.setRenderInPip(true);
if (state == WebRtcLocalRenderState.EXPANDED) {
expandPip(localCallParticipant, focusedParticipant);
return;
} else if (state == WebRtcLocalRenderState.SMALL_RECTANGLE && pictureInPictureExpansionHelper.isExpandedOrExpanding()) {
shrinkPip(localCallParticipant);
return;
} else {
smallLocalRender.setCallParticipant(localCallParticipant);
}
switch (state) {
case GONE:
@@ -350,7 +409,6 @@ public class WebRtcCallView extends FrameLayout {
recipientId = recipient.getId();
if (recipient.isGroup()) {
recipientName.setText(getContext().getString(R.string.WebRtcCallView__s_group_call, recipient.getDisplayName(getContext())));
if (toolbar.getMenu().findItem(R.id.menu_group_call_participants_list) == null) {
toolbar.inflateMenu(R.menu.group_call);
@@ -417,7 +475,19 @@ public class WebRtcCallView extends FrameLayout {
visibleViewSet.add(startCallControls);
startCall.setText(webRtcControls.getStartCallButtonText());
startCall.setEnabled(true);
startCall.setEnabled(webRtcControls.isStartCallEnabled());
}
if (webRtcControls.displayErrorControls()) {
visibleViewSet.add(footerGradient);
visibleViewSet.add(errorButton);
}
if (webRtcControls.displayGroupCallFull()) {
groupCallFullStub.get().setVisibility(View.VISIBLE);
((TextView) groupCallFullStub.get().findViewById(R.id.group_call_call_full_message)).setText(webRtcControls.getGroupCallFullMessage(getContext()));
} else if (groupCallFullStub.resolved()) {
groupCallFullStub.get().setVisibility(View.GONE);
}
MenuItem item = toolbar.getMenu().findItem(R.id.menu_group_call_participants_list);
@@ -489,6 +559,10 @@ public class WebRtcCallView extends FrameLayout {
}
} else {
cancelFadeOut();
if (controlsListener != null) {
controlsListener.showSystemUI();
}
}
controls = webRtcControls;
@@ -503,6 +577,64 @@ public class WebRtcCallView extends FrameLayout {
return videoToggle;
}
public void showSpeakerViewHint() {
groupCallSpeakerHint.get().setVisibility(View.VISIBLE);
}
public void hideSpeakerViewHint() {
if (groupCallSpeakerHint.resolved()) {
groupCallSpeakerHint.get().setVisibility(View.GONE);
}
}
private void expandPip(@NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) {
pictureInPictureExpansionHelper.expand(smallLocalRenderFrame, new PictureInPictureExpansionHelper.Callback() {
@Override
public void onAnimationWillStart() {
largeLocalRender.attachBroadcastVideoSink(localCallParticipant.getVideoSink());
}
@Override
public void onPictureInPictureExpanded() {
largeLocalRenderFrame.setVisibility(View.VISIBLE);
}
@Override
public void onPictureInPictureNotVisible() {
smallLocalRender.setCallParticipant(focusedParticipant);
}
@Override
public void onAnimationHasFinished() {
pictureInPictureGestureHelper.adjustPip();
}
});
}
private void shrinkPip(@NonNull CallParticipant localCallParticipant) {
pictureInPictureExpansionHelper.shrink(smallLocalRenderFrame, new PictureInPictureExpansionHelper.Callback() {
@Override
public void onAnimationWillStart() {
}
@Override
public void onPictureInPictureExpanded() {
largeLocalRenderFrame.setVisibility(View.GONE);
largeLocalRender.attachBroadcastVideoSink(null);
}
@Override
public void onPictureInPictureNotVisible() {
smallLocalRender.setCallParticipant(localCallParticipant);
}
@Override
public void onAnimationHasFinished() {
pictureInPictureGestureHelper.adjustPip();
}
});
}
private void animatePipToLargeRectangle() {
ResizeAnimation animation = new ResizeAnimation(smallLocalRenderFrame, ViewUtil.dpToPx(90), ViewUtil.dpToPx(160));
animation.setDuration(PIP_RESIZE_DURATION);
@@ -545,12 +677,10 @@ public class WebRtcCallView extends FrameLayout {
private void fadeOutControls() {
fadeControls(ConstraintSet.GONE);
controlsListener.onControlsFadeOut();
pictureInPictureGestureHelper.clearVerticalBoundaries();
}
private void fadeInControls() {
fadeControls(ConstraintSet.VISIBLE);
pictureInPictureGestureHelper.setVerticalBoundaries(toolbar.getBottom(), videoToggle.getTop());
scheduleFadeOut();
}
@@ -594,6 +724,15 @@ public class WebRtcCallView extends FrameLayout {
.setDuration(TRANSITION_DURATION_MILLIS);
TransitionManager.endTransitions(parent);
if (controlsListener != null) {
if (controlsVisible) {
controlsListener.showSystemUI();
} else {
controlsListener.hideSystemUI();
}
}
TransitionManager.beginDelayedTransition(parent, transition);
ConstraintSet constraintSet = new ConstraintSet();
@@ -678,6 +817,8 @@ public class WebRtcCallView extends FrameLayout {
void onStartCall(boolean isVideoCall);
void onCancelStartCall();
void onControlsFadeOut();
void showSystemUI();
void hideSystemUI();
void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput);
void onVideoChanged(boolean isVideoEnabled);
void onMicChanged(boolean isMicEnabled);
@@ -688,5 +829,6 @@ public class WebRtcCallView extends FrameLayout {
void onAcceptCallPressed();
void onShowParticipantsList();
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
void onLocalPictureInPictureClicked();
}
}

View File

@@ -5,6 +5,7 @@ import android.os.Looper;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
@@ -17,6 +18,9 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -30,17 +34,20 @@ import java.util.List;
public class WebRtcCallViewModel extends ViewModel {
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
private final SingleLiveEvent<CallParticipantListUpdate> callParticipantListUpdate = new SingleLiveEvent<>();
private final MutableLiveData<Collection<RecipientId>> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList());
private final LiveData<SafetyNumberChangeEvent> safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new);
private final MutableLiveData<Boolean> microphoneEnabled = new MutableLiveData<>(true);
private final MutableLiveData<Boolean> isInPipMode = new MutableLiveData<>(false);
private final MutableLiveData<WebRtcControls> webRtcControls = new MutableLiveData<>(WebRtcControls.NONE);
private final LiveData<WebRtcControls> realWebRtcControls = LiveDataUtil.combineLatest(isInPipMode, webRtcControls, this::getRealWebRtcControls);
private final SingleLiveEvent<Event> events = new SingleLiveEvent<Event>();
private final MutableLiveData<Long> elapsed = new MutableLiveData<>(-1L);
private final MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>(Recipient.UNKNOWN.live());
private final MutableLiveData<CallParticipantsState> participantsState = new MutableLiveData<>(CallParticipantsState.STARTING_STATE);
private final SingleLiveEvent<CallParticipantListUpdate> callParticipantListUpdate = new SingleLiveEvent<>();
private final MutableLiveData<Collection<RecipientId>> identityChangedRecipients = new MutableLiveData<>(Collections.emptyList());
private final LiveData<SafetyNumberChangeEvent> safetyNumberChangeEvent = LiveDataUtil.combineLatest(isInPipMode, identityChangedRecipients, SafetyNumberChangeEvent::new);
private final LiveData<Recipient> groupRecipient = LiveDataUtil.filter(Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData), Recipient::isActiveGroup);
private final LiveData<List<GroupMemberEntry.FullMember>> groupMembers = LiveDataUtil.skip(Transformations.switchMap(groupRecipient, r -> Transformations.distinctUntilChanged(new LiveGroup(r.requireGroupId()).getFullMembers())), 1);
private final LiveData<Boolean> shouldShowSpeakerHint = Transformations.map(participantsState, this::shouldShowSpeakerHint);
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
@@ -49,8 +56,8 @@ public class WebRtcCallViewModel extends ViewModel {
private boolean answerWithVideoAvailable = false;
private Runnable elapsedTimeRunnable = this::handleTick;
private boolean canEnterPipMode = false;
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
private boolean callingStarted = false;
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
private boolean callStarting = false;
private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication());
@@ -90,6 +97,14 @@ public class WebRtcCallViewModel extends ViewModel {
return safetyNumberChangeEvent;
}
public LiveData<List<GroupMemberEntry.FullMember>> getGroupMembers() {
return groupMembers;
}
public LiveData<Boolean> shouldShowSpeakerHint() {
return shouldShowSpeakerHint;
}
public boolean canEnterPipMode() {
return canEnterPipMode;
}
@@ -98,8 +113,8 @@ public class WebRtcCallViewModel extends ViewModel {
return answerWithVideoAvailable;
}
public boolean isCallingStarted() {
return callingStarted;
public boolean isCallStarting() {
return callStarting;
}
@MainThread
@@ -112,17 +127,34 @@ public class WebRtcCallViewModel extends ViewModel {
@MainThread
public void setIsViewingFocusedParticipant(@NonNull CallParticipantsState.SelectedPage page) {
if (page == CallParticipantsState.SelectedPage.FOCUSED) {
SignalStore.tooltips().markGroupCallSpeakerViewSeen();
}
//noinspection ConstantConditions
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page));
}
public void onLocalPictureInPictureClicked() {
CallParticipantsState state = participantsState.getValue();
if (state.getGroupCallState() != WebRtcViewModel.GroupCallState.IDLE) {
return;
}
participantsState.setValue(CallParticipantsState.setExpanded(participantsState.getValue(),
state.getLocalRenderState() != WebRtcLocalRenderState.EXPANDED));
}
public void onDismissedVideoTooltip() {
canDisplayTooltipIfNeeded = false;
}
@MainThread
public void updateFromWebRtcViewModel(@NonNull WebRtcViewModel webRtcViewModel, boolean enableVideo) {
canEnterPipMode = webRtcViewModel.getState() != WebRtcViewModel.State.CALL_PRE_JOIN;
canEnterPipMode = !webRtcViewModel.getState().isPreJoinOrNetworkUnavailable();
if (callStarting && webRtcViewModel.getState().isPassedPreJoin()) {
callStarting = false;
}
CallParticipant localParticipant = webRtcViewModel.getLocalParticipant();
@@ -150,7 +182,9 @@ public class WebRtcCallViewModel extends ViewModel {
localParticipant.isMoreThanOneCameraAvailable(),
webRtcViewModel.isBluetoothAvailable(),
Util.hasItems(webRtcViewModel.getRemoteParticipants()),
repository.getAudioOutput());
repository.getAudioOutput(),
webRtcViewModel.getRemoteDevicesCount().orElse(0),
webRtcViewModel.getParticipantLimit());
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
callConnectedTime = webRtcViewModel.getCallConnectedTime();
@@ -185,7 +219,9 @@ public class WebRtcCallViewModel extends ViewModel {
boolean isMoreThanOneCameraAvailable,
boolean isBluetoothAvailable,
boolean hasAtLeastOneRemote,
@NonNull WebRtcAudioOutput audioOutput)
@NonNull WebRtcAudioOutput audioOutput,
long remoteDevicesCount,
@Nullable Long participantLimit)
{
final WebRtcControls.CallState callState;
@@ -209,6 +245,9 @@ public class WebRtcCallViewModel extends ViewModel {
case CALL_DISCONNECTED:
callState = WebRtcControls.CallState.ENDING;
break;
case NETWORK_FAILURE:
callState = WebRtcControls.CallState.ERROR;
break;
default:
callState = WebRtcControls.CallState.ONGOING;
}
@@ -221,7 +260,8 @@ public class WebRtcCallViewModel extends ViewModel {
break;
case CONNECTING:
case RECONNECTING:
groupCallState = WebRtcControls.GroupCallState.CONNECTING;
groupCallState = (participantLimit == null || remoteDevicesCount < participantLimit) ? WebRtcControls.GroupCallState.CONNECTING
: WebRtcControls.GroupCallState.FULL;
break;
case CONNECTED:
case CONNECTED_AND_JOINING:
@@ -241,13 +281,21 @@ public class WebRtcCallViewModel extends ViewModel {
hasAtLeastOneRemote,
callState,
groupCallState,
audioOutput));
audioOutput,
participantLimit));
}
private @NonNull WebRtcControls getRealWebRtcControls(boolean isInPipMode, @NonNull WebRtcControls controls) {
return isInPipMode ? WebRtcControls.PIP : controls;
}
private boolean shouldShowSpeakerHint(@NonNull CallParticipantsState state) {
return !state.isInPipMode() &&
state.getRemoteDevicesCount().orElse(0) > 1 &&
state.getGroupCallState().isConnected() &&
!SignalStore.tooltips().hasSeenGroupCallSpeakerView();
}
private void startTimer() {
cancelTimer();
@@ -277,7 +325,7 @@ public class WebRtcCallViewModel extends ViewModel {
}
public void startCall(boolean isVideoCall) {
callingStarted = true;
callStarting = true;
Recipient recipient = getRecipient().get();
if (recipient.isGroup()) {
repository.getIdentityRecords(recipient, identityRecords -> {

View File

@@ -1,6 +1,9 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.thoughtcrime.securesms.R;
@@ -8,7 +11,7 @@ import org.thoughtcrime.securesms.R;
public final class WebRtcControls {
public static final WebRtcControls NONE = new WebRtcControls();
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET);
public static final WebRtcControls PIP = new WebRtcControls(false, false, false, false, true, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null);
private final boolean isRemoteVideoEnabled;
private final boolean isLocalVideoEnabled;
@@ -19,9 +22,10 @@ public final class WebRtcControls {
private final CallState callState;
private final GroupCallState groupCallState;
private final WebRtcAudioOutput audioOutput;
private final Long participantLimit;
private WebRtcControls() {
this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET);
this(false, false, false, false, false, false, CallState.NONE, GroupCallState.NONE, WebRtcAudioOutput.HANDSET, null);
}
WebRtcControls(boolean isLocalVideoEnabled,
@@ -32,7 +36,8 @@ public final class WebRtcControls {
boolean hasAtLeastOneRemote,
@NonNull CallState callState,
@NonNull GroupCallState groupCallState,
@NonNull WebRtcAudioOutput audioOutput)
@NonNull WebRtcAudioOutput audioOutput,
@Nullable Long participantLimit)
{
this.isLocalVideoEnabled = isLocalVideoEnabled;
this.isRemoteVideoEnabled = isRemoteVideoEnabled;
@@ -43,6 +48,11 @@ public final class WebRtcControls {
this.callState = callState;
this.groupCallState = groupCallState;
this.audioOutput = audioOutput;
this.participantLimit = participantLimit;
}
boolean displayErrorControls() {
return isError();
}
boolean displayStartCallControls() {
@@ -50,12 +60,31 @@ public final class WebRtcControls {
}
@StringRes int getStartCallButtonText() {
if (isGroupCall() && hasAtLeastOneRemote) {
return R.string.WebRtcCallView__join_call;
if (isGroupCall()) {
if (groupCallState == GroupCallState.FULL) {
return R.string.WebRtcCallView__call_is_full;
} else if (hasAtLeastOneRemote) {
return R.string.WebRtcCallView__join_call;
}
}
return R.string.WebRtcCallView__start_call;
}
boolean isStartCallEnabled() {
return groupCallState != GroupCallState.FULL;
}
boolean displayGroupCallFull() {
return groupCallState == GroupCallState.FULL;
}
@NonNull String getGroupCallFullMessage(@NonNull Context context) {
if (participantLimit != null) {
return context.getString(R.string.WebRtcCallView__the_maximum_number_of_d_participants_has_been_Reached_for_this_call, participantLimit);
}
return "";
}
boolean displayGroupMembersButton() {
return groupCallState.isAtLeast(GroupCallState.CONNECTING);
}
@@ -120,6 +149,10 @@ public final class WebRtcControls {
return audioOutput;
}
private boolean isError() {
return callState == CallState.ERROR;
}
private boolean isPreJoin() {
return callState == CallState.PRE_JOIN;
}
@@ -142,6 +175,7 @@ public final class WebRtcControls {
public enum CallState {
NONE,
ERROR,
PRE_JOIN,
INCOMING,
OUTGOING,
@@ -158,6 +192,7 @@ public final class WebRtcControls {
DISCONNECTED,
RECONNECTING,
CONNECTING,
FULL,
CONNECTED;
boolean isAtLeast(@SuppressWarnings("SameParameterValue") @NonNull GroupCallState other) {

View File

@@ -5,5 +5,6 @@ public enum WebRtcLocalRenderState {
SMALL_RECTANGLE,
SMALLER_RECTANGLE,
LARGE,
LARGE_NO_VIDEO
LARGE_NO_VIDEO,
EXPANDED
}

View File

@@ -25,8 +25,7 @@ public final class CallParticipantViewState extends RecipientMappingModel<CallPa
@Override
public @NonNull String getName(@NonNull Context context) {
return callParticipant.getRecipient().isSelf() ? context.getString(R.string.GroupMembersDialog_you)
: super.getName(context);
return callParticipant.getRecipientDisplayName(context);
}
public int getVideoMutedVisibility() {
@@ -36,4 +35,16 @@ public final class CallParticipantViewState extends RecipientMappingModel<CallPa
public int getAudioMutedVisibility() {
return callParticipant.isMicrophoneEnabled() ? View.GONE : View.VISIBLE;
}
@Override
public boolean areItemsTheSame(@NonNull CallParticipantViewState newItem) {
return callParticipant.getCallParticipantId().equals(newItem.callParticipant.getCallParticipantId());
}
@Override
public boolean areContentsTheSame(@NonNull CallParticipantViewState newItem) {
return super.areContentsTheSame(newItem) &&
callParticipant.isVideoEnabled() == newItem.callParticipant.isVideoEnabled() &&
callParticipant.isMicrophoneEnabled() == newItem.callParticipant.isMicrophoneEnabled();
}
}

View File

@@ -9,11 +9,13 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.OptionalLong;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.thoughtcrime.securesms.R;
@@ -38,6 +40,13 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment {
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
public static void dismiss(@NonNull FragmentManager manager) {
Fragment fragment = manager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
if (fragment instanceof CallParticipantsListDialog) {
((CallParticipantsListDialog) fragment).dismissAllowingStateLoss();
}
}
@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
BottomSheetUtil.show(manager, tag, this);
@@ -80,19 +89,21 @@ public class CallParticipantsListDialog extends BottomSheetDialogFragment {
private void updateList(@NonNull CallParticipantsState callParticipantsState) {
List<MappingModel<?>> items = new ArrayList<>();
boolean includeSelf = callParticipantsState.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
boolean includeSelf = callParticipantsState.getGroupCallState() == WebRtcViewModel.GroupCallState.CONNECTED_AND_JOINED;
OptionalLong headerCount = callParticipantsState.getParticipantCount();
items.add(new CallParticipantsListHeader(callParticipantsState.getAllRemoteParticipants().size() + (includeSelf ? 1 : 0)));
headerCount.executeIfPresent(count -> {
items.add(new CallParticipantsListHeader((int) count));
if (includeSelf) {
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
}
if (includeSelf) {
items.add(new CallParticipantViewState(callParticipantsState.getLocalParticipant()));
}
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
items.add(new CallParticipantViewState(callParticipant));
}
for (CallParticipant callParticipant : callParticipantsState.getAllRemoteParticipants()) {
items.add(new CallParticipantViewState(callParticipant));
}
});
adapter.submitList(items);
}
}

View File

@@ -43,7 +43,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.sms.IncomingJoinedMessage;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.Stopwatch;
@@ -73,7 +72,6 @@ import java.util.concurrent.TimeoutException;
/**
* Manages all the stuff around determining if a user is registered or not.
*/
@Trace
public class DirectoryHelper {
private static final String TAG = Log.tag(DirectoryHelper.class);
@@ -254,6 +252,8 @@ public class DirectoryHelper {
stopwatch.split("handle-unlisted");
Set<RecipientId> preExistingRegisteredUsers = new HashSet<>(recipientDatabase.getRegistered());
recipientDatabase.bulkUpdatedRegisteredStatus(uuidMap, inactiveIds);
stopwatch.split("update-registered");
@@ -267,14 +267,13 @@ public class DirectoryHelper {
}
if (TextSecurePreferences.hasSuccessfullyRetrievedDirectory(context) && notifyOfNewUsers) {
Set<RecipientId> existingSignalIds = new HashSet<>(recipientDatabase.getRegistered());
Set<RecipientId> existingSystemIds = new HashSet<>(recipientDatabase.getSystemContacts());
Set<RecipientId> newlyActiveIds = new HashSet<>(activeIds);
Set<RecipientId> systemContacts = new HashSet<>(recipientDatabase.getSystemContacts());
Set<RecipientId> newlyRegisteredSystemContacts = new HashSet<>(activeIds);
newlyActiveIds.removeAll(existingSignalIds);
newlyActiveIds.retainAll(existingSystemIds);
newlyRegisteredSystemContacts.removeAll(preExistingRegisteredUsers);
newlyRegisteredSystemContacts.retainAll(systemContacts);
notifyNewUsers(context, newlyActiveIds);
notifyNewUsers(context, newlyRegisteredSystemContacts);
} else {
TextSecurePreferences.setHasSuccessfullyRetrievedDirectory(context, true);
}
@@ -297,6 +296,11 @@ public class DirectoryHelper {
boolean removeMissing,
@NonNull Map<String, String> rewrites)
{
if (!Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
Log.w(TAG, "[updateContactsDatabase] No contact permissions. Skipping.");
return;
}
AccountHolder account = getOrCreateSystemAccount(context);
if (account == null) {
@@ -398,8 +402,11 @@ public class DirectoryHelper {
for (RecipientId newUser: newUsers) {
Recipient recipient = Recipient.resolved(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) && !recipient.isSelf()) {
IncomingJoinedMessage message = new IncomingJoinedMessage(newUser);
if (!SessionUtil.hasSession(context, recipient.getId()) &&
!recipient.isSelf() &&
recipient.hasAUserSetDisplayName(context))
{
IncomingJoinedMessage message = new IncomingJoinedMessage(recipient.getId());
Optional<InsertResult> insertResult = DatabaseFactory.getSmsDatabase(context).insertMessageInbox(message);
if (insertResult.isPresent()) {

View File

@@ -243,7 +243,6 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.BitmapUtil;
@@ -302,7 +301,6 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
* @author Moxie Marlinspike
*
*/
@Trace
@SuppressLint("StaticFieldLeak")
public class ConversationActivity extends PassphraseRequiredActivity
implements ConversationFragment.ConversationFragmentListener,
@@ -374,6 +372,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private View cancelJoinRequest;
private Stub<View> mentionsSuggestions;
private MaterialButton joinGroupCallButton;
private boolean callingTooltipShown;
private LinkPreviewViewModel linkPreviewViewModel;
private ConversationSearchViewModel searchViewModel;
@@ -408,7 +407,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (ConversationIntents.isInvalid(getIntent())) {
Log.w(TAG, "[onCreate] Missing recipientId!");
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
startActivity(MainActivity.clearTop(this));
finish();
return;
}
@@ -487,7 +486,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (ConversationIntents.isInvalid(intent)) {
Log.w(TAG, "[onNewIntent] Missing recipientId!");
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
startActivity(MainActivity.clearTop(this));
finish();
return;
}
@@ -537,6 +536,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
.startChain(new RequestGroupV2InfoJob(groupId))
.then(new GroupV2UpdateSelfProfileKeyJob(groupId))
.enqueue();
if (viewModel.getArgs().isFirstTimeInSelfCreatedGroup()) {
groupViewModel.inviteFriendsOneTimeIfJustSelfInGroup(getSupportFragmentManager(), groupId);
}
}
if (groupCallViewModel != null) {
@@ -830,6 +833,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (groupCallViewModel != null && Boolean.TRUE.equals(groupCallViewModel.hasActiveGroupCall().getValue())) {
hideMenuItem(menu, R.id.menu_video_secure);
}
showGroupCallingTooltip();
}
inflater.inflate(R.menu.conversation_group_options, menu);
@@ -1085,6 +1089,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
final long thread = this.threadId;
ExpirationDialog.show(this, recipient.get().getExpireMessages(),
expirationTime ->
SimpleTask.run(
@@ -1100,7 +1106,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
} else {
DatabaseFactory.getRecipientDatabase(ConversationActivity.this).setExpireMessages(recipient.getId(), expirationTime);
OutgoingExpirationUpdateMessage outgoingMessage = new OutgoingExpirationUpdateMessage(getRecipient(), System.currentTimeMillis(), expirationTime * 1000L);
MessageSender.send(ConversationActivity.this, outgoingMessage, threadId, false, null);
MessageSender.send(ConversationActivity.this, outgoingMessage, thread, false, null);
}
return GroupChangeResult.SUCCESS;
},
@@ -1780,8 +1786,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
reminderView.get().setOnActionClickListener(actionId -> {
if (actionId == R.id.reminder_action_gv1_suggestion_add_members) {
GroupsV1MigrationSuggestionsDialog.show(this, recipient.get().requireGroupId().requireV2(), gv1MigrationSuggestions);
} else if (actionId == R.id.reminder_action_gv1_suggestion_not_now) {
groupViewModel.onSuggestedMembersBannerDismissed(recipient.get().requireGroupId());
} else if (actionId == R.id.reminder_action_gv1_suggestion_no_thanks) {
groupViewModel.onSuggestedMembersBannerDismissed(recipient.get().requireGroupId(), gv1MigrationSuggestions);
}
});
reminderView.get().setOnDismissListener(() -> {
@@ -1880,31 +1886,31 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void initializeViews() {
titleView = findViewById(R.id.conversation_title_view);
buttonToggle = ViewUtil.findById(this, R.id.button_toggle);
sendButton = ViewUtil.findById(this, R.id.send_button);
attachButton = ViewUtil.findById(this, R.id.attach_button);
composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
charactersLeft = ViewUtil.findById(this, R.id.space_left);
buttonToggle = findViewById(R.id.button_toggle);
sendButton = findViewById(R.id.send_button);
attachButton = findViewById(R.id.attach_button);
composeText = findViewById(R.id.embedded_text_editor);
charactersLeft = findViewById(R.id.space_left);
emojiDrawerStub = ViewUtil.findStubById(this, R.id.emoji_drawer_stub);
attachmentKeyboardStub = ViewUtil.findStubById(this, R.id.attachment_keyboard_stub);
unblockButton = ViewUtil.findById(this, R.id.unblock_button);
makeDefaultSmsButton = ViewUtil.findById(this, R.id.make_default_sms_button);
registerButton = ViewUtil.findById(this, R.id.register_button);
container = ViewUtil.findById(this, R.id.layout_container);
unblockButton = findViewById(R.id.unblock_button);
makeDefaultSmsButton = findViewById(R.id.make_default_sms_button);
registerButton = findViewById(R.id.register_button);
container = findViewById(R.id.layout_container);
reminderView = ViewUtil.findStubById(this, R.id.reminder_stub);
unverifiedBannerView = ViewUtil.findStubById(this, R.id.unverified_banner_stub);
reviewBanner = ViewUtil.findStubById(this, R.id.review_banner_stub);
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inlineAttachmentToggle = ViewUtil.findById(this, R.id.inline_attachment_container);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
panelParent = ViewUtil.findById(this, R.id.conversation_activity_panel_parent);
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
messageRequestBottomView = ViewUtil.findById(this, R.id.conversation_activity_message_request_bottom_bar);
reactionOverlay = ViewUtil.findById(this, R.id.conversation_reaction_scrubber);
quickAttachmentToggle = findViewById(R.id.quick_attachment_toggle);
inlineAttachmentToggle = findViewById(R.id.inline_attachment_container);
inputPanel = findViewById(R.id.bottom_panel);
panelParent = findViewById(R.id.conversation_activity_panel_parent);
searchNav = findViewById(R.id.conversation_search_nav);
messageRequestBottomView = findViewById(R.id.conversation_activity_message_request_bottom_bar);
reactionOverlay = findViewById(R.id.conversation_reaction_scrubber);
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
ImageButton quickCameraToggle = findViewById(R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = findViewById(R.id.inline_attachment_button);
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
@@ -1980,7 +1986,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isInBubble()) {
supportActionBar.setHomeAsUpIndicator(ContextCompat.getDrawable(this, R.drawable.ic_notification));
toolbar.setNavigationOnClickListener(unused -> startActivity(new Intent(Intent.ACTION_MAIN).setClass(this, MainActivity.class)));
toolbar.setNavigationOnClickListener(unused -> startActivity(MainActivity.clearTop(this)));
}
}
@@ -2134,7 +2140,29 @@ public class ConversationActivity extends PassphraseRequiredActivity
joinGroupCallButton.setVisibility(hasActiveCall ? View.VISIBLE : View.GONE);
});
groupCallViewModel.canJoinGroupCall().observe(this, canJoin -> joinGroupCallButton.setText(canJoin ? R.string.ConversationActivity_join : R.string.ConversationActivity_full));
groupCallViewModel.groupCallHasCapacity().observe(this, hasCapacity -> joinGroupCallButton.setText(hasCapacity ? R.string.ConversationActivity_join : R.string.ConversationActivity_full));
}
private void showGroupCallingTooltip() {
if (!FeatureFlags.groupCalling() || !SignalStore.tooltips().shouldShowGroupCallingTooltip() || callingTooltipShown) {
return;
}
View anchor = findViewById(R.id.menu_video_secure);
if (anchor == null) {
Log.w(TAG, "Video Call tooltip anchor is null. Skipping tooltip...");
return;
}
callingTooltipShown = true;
SignalStore.tooltips().markGroupCallSpeakerViewSeen();
TooltipPopup.forTarget(anchor)
.setBackgroundTint(ContextCompat.getColor(this, R.color.signal_accent_green))
.setTextColor(getResources().getColor(R.color.core_white))
.setText(R.string.ConversationActivity__tap_here_to_start_a_group_call)
.setOnDismissListener(() -> SignalStore.tooltips().markGroupCallingTooltipSeen())
.show(TooltipPopup.POSITION_BELOW);
}
private void showStickerIntroductionTooltip() {
@@ -2651,13 +2679,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
private void sendMediaMessage(@NonNull MediaSendActivityResult result) {
long thread = this.threadId;
long expiresIn = recipient.get().getExpireMessages() * 1000L;
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
List<Mention> mentions = new ArrayList<>(result.getMentions());
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList(), mentions);
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(threadId);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
inputPanel.clearQuote();
attachmentManager.clear(glideRequests, false);
@@ -2666,7 +2695,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
long id = fragment.stageOutgoingMessage(message);
SimpleTask.run(() -> {
long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), threadId, () -> fragment.releaseOutgoingMessage(id));
long resultId = MessageSender.sendPushWithPreUploadedMedia(this, secureMessage, result.getPreUploadResults(), thread, () -> fragment.releaseOutgoingMessage(id));
int deleted = DatabaseFactory.getAttachmentDatabase(this).deleteAbandonedPreuploadedAttachments();
Log.i(TAG, "Deleted " + deleted + " abandoned attachments.");
@@ -2711,6 +2740,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
return new SettableFuture<>(null);
}
final long thread = this.threadId;
if (isSecureText && !forceSms) {
MessageUtil.SplitResult splitMessage = MessageUtil.getSplitMessage(this, body, sendButton.getSelectedTransport().calculateCharacters(body).maxPrimaryMessageSize);
body = splitMessage.getBody();
@@ -2729,7 +2760,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isSecureText && !forceSms) {
outgoingMessage = new OutgoingSecureMediaMessage(outgoingMessageCandidate);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(threadId);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
} else {
outgoingMessage = outgoingMessageCandidate;
}
@@ -2748,7 +2779,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
final long id = fragment.stageOutgoingMessage(outgoingMessage);
SimpleTask.run(() -> {
return MessageSender.send(context, outgoingMessage, threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
return MessageSender.send(context, outgoingMessage, thread, forceSms, () -> fragment.releaseOutgoingMessage(id));
}, result -> {
sendComplete(result);
future.set(null);
@@ -2768,6 +2799,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
final long thread = this.threadId;
final Context context = getApplicationContext();
final String messageBody = getMessage();
@@ -2775,7 +2807,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (isSecureText && !forceSms) {
message = new OutgoingEncryptedMessage(recipient.get(), messageBody, expiresIn);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(threadId);
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
} else {
message = new OutgoingTextMessage(recipient.get(), messageBody, expiresIn, subscriptionId);
}
@@ -2791,7 +2823,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
new AsyncTask<OutgoingTextMessage, Void, Long>() {
@Override
protected Long doInBackground(OutgoingTextMessage... messages) {
return MessageSender.send(context, messages[0], threadId, forceSms, () -> fragment.releaseOutgoingMessage(id));
return MessageSender.send(context, messages[0], thread, forceSms, () -> fragment.releaseOutgoingMessage(id));
}
@Override

View File

@@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.Stopwatch;
import java.util.ArrayList;
@@ -27,7 +26,6 @@ import java.util.Map;
/**
* Core data source for loading an individual conversation.
*/
@Trace
class ConversationDataSource implements PagedDataSource<ConversationMessage> {
private static final String TAG = Log.tag(ConversationDataSource.class);

View File

@@ -66,9 +66,11 @@ import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.ConversationScrollToView;
import org.thoughtcrime.securesms.components.ConversationTypingView;
@@ -93,6 +95,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
@@ -121,7 +124,6 @@ import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.HtmlUtil;
@@ -147,9 +149,9 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
@Trace
@SuppressLint("StaticFieldLeak")
public class ConversationFragment extends LoggingFragment {
private static final String TAG = ConversationFragment.class.getSimpleName();
@@ -250,8 +252,11 @@ public class ConversationFragment extends LoggingFragment {
this.messageCountsViewModel = ViewModelProviders.of(requireActivity()).get(MessageCountsViewModel.class);
this.conversationViewModel = ViewModelProviders.of(requireActivity(), new ConversationViewModel.Factory()).get(ConversationViewModel.class);
conversationViewModel.getMessages().observe(this, list -> {
getListAdapter().submitList(list);
conversationViewModel.getMessages().observe(this, messages -> {
ConversationAdapter adapter = getListAdapter();
if (adapter != null) {
getListAdapter().submitList(messages);
}
});
conversationViewModel.getConversationMetadata().observe(this, this::presentConversationMetadata);
@@ -1005,7 +1010,7 @@ public class ConversationFragment extends LoggingFragment {
private void moveToPosition(int position, @Nullable Runnable onMessageNotFound) {
Log.d(TAG, "moveToPosition(" + position + ")");
conversationViewModel.onConversationDataAvailable(threadId, position);
conversationViewModel.getPagingController().onDataNeededAroundIndex(position);
snapToTopDataObserver.buildScrollPosition(position)
.withOnPerformScroll(((layoutManager, p) ->
list.post(() -> {
@@ -1413,10 +1418,63 @@ public class ConversationFragment extends LoggingFragment {
GroupsV1MigrationInfoBottomSheetDialogFragment.show(requireFragmentManager(), membershipChange);
}
@Override
public void onDecryptionFailedLearnMoreClicked() {
new AlertDialog.Builder(requireContext())
.setView(R.layout.decryption_failed_dialog)
.setPositiveButton(android.R.string.ok, (d, w) -> {
d.dismiss();
})
.setNeutralButton(R.string.ConversationFragment_contact_us, (d, w) -> {
Intent intent = new Intent(requireContext(), ApplicationPreferencesActivity.class);
intent.putExtra(ApplicationPreferencesActivity.LAUNCH_TO_HELP_FRAGMENT, true);
startActivity(intent);
d.dismiss();
})
.show();
}
@Override
public void onSafetyNumberLearnMoreClicked(@NonNull Recipient recipient) {
if (recipient.isGroup()) {
throw new AssertionError("Must be individual");
}
AlertDialog dialog = new AlertDialog.Builder(requireContext())
.setView(R.layout.safety_number_changed_learn_more_dialog)
.setPositiveButton(R.string.ConversationFragment_verify, (d, w) -> {
SimpleTask.run(getLifecycle(), () -> {
return DatabaseFactory.getIdentityDatabase(requireContext()).getIdentity(recipient.getId());
}, identityRecord -> {
if (identityRecord.isPresent()) {
startActivity(VerifyIdentityActivity.newIntent(requireContext(), identityRecord.get()));
}});
d.dismiss();
})
.setNegativeButton(R.string.ConversationFragment_not_now, (d, w) -> {
d.dismiss();
})
.create();
dialog.setOnShowListener(d -> {
TextView title = Objects.requireNonNull(dialog.findViewById(R.id.safety_number_learn_more_title));
TextView body = Objects.requireNonNull(dialog.findViewById(R.id.safety_number_learn_more_body));
title.setText(getString(R.string.ConversationFragment_your_safety_number_with_s_changed, recipient.getDisplayName(requireContext())));
body.setText(getString(R.string.ConversationFragment_your_safety_number_with_s_changed_likey_because_they_reinstalled_signal, recipient.getDisplayName(requireContext())));
});
dialog.show();
}
@Override
public void onJoinGroupCallClicked() {
CommunicationActions.startVideoCall(requireActivity(), recipient.get());
}
@Override
public void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId) {
GroupLinkInviteFriendsBottomSheetDialogFragment.show(requireActivity().getSupportFragmentManager(), groupId);
}
}
@Override

View File

@@ -6,6 +6,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
@@ -25,19 +26,19 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
final class ConversationGroupViewModel extends ViewModel {
@@ -52,6 +53,8 @@ final class ConversationGroupViewModel extends ViewModel {
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
private final LiveData<Boolean> gv1MigrationReminder;
private boolean firstTimeInviteFriendsTriggered;
private ConversationGroupViewModel() {
this.liveRecipient = new MutableLiveData<>();
@@ -83,10 +86,10 @@ final class ConversationGroupViewModel extends ViewModel {
liveRecipient.setValue(recipient);
}
void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId) {
void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId, @NonNull List<RecipientId> suggestions) {
SignalExecutors.BOUNDED.execute(() -> {
if (groupId.isV2()) {
DatabaseFactory.getGroupDatabase(ApplicationDependencies.getApplication()).clearFormerV1Members(groupId.requireV2());
DatabaseFactory.getGroupDatabase(ApplicationDependencies.getApplication()).removeUnmigratedV1Members(groupId.requireV2(), suggestions);
liveRecipient.postValue(liveRecipient.getValue());
}
});
@@ -177,9 +180,9 @@ final class ConversationGroupViewModel extends ViewModel {
return Collections.emptyList();
}
Set<RecipientId> difference = SetUtil.difference(record.getFormerV1Members(), record.getMembers());
return Stream.of(Recipient.resolvedList(difference))
return Stream.of(record.getUnmigratedV1Members())
.filterNot(m -> record.getMembers().contains(m))
.map(Recipient::resolved)
.filter(GroupsV1MigrationUtil::isAutoMigratable)
.map(Recipient::getId)
.toList();
@@ -227,6 +230,28 @@ final class ConversationGroupViewModel extends ViewModel {
});
}
void inviteFriendsOneTimeIfJustSelfInGroup(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
if (firstTimeInviteFriendsTriggered) {
return;
}
firstTimeInviteFriendsTriggered = true;
SimpleTask.run(() -> DatabaseFactory.getGroupDatabase(ApplicationDependencies.getApplication())
.requireGroup(groupId)
.getMembers().equals(Collections.singletonList(Recipient.self().getId())),
justSelf -> {
if (justSelf) {
inviteFriends(supportFragmentManager, groupId);
}
}
);
}
void inviteFriends(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
GroupLinkInviteFriendsBottomSheetDialogFragment.show(supportFragmentManager, groupId);
}
static final class ReviewState {
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);

View File

@@ -18,15 +18,16 @@ import java.util.Objects;
public class ConversationIntents {
private static final String BUBBLE_AUTHORITY = "bubble";
private static final String EXTRA_RECIPIENT = "recipient_id";
private static final String EXTRA_THREAD_ID = "thread_id";
private static final String EXTRA_TEXT = "draft_text";
private static final String EXTRA_MEDIA = "media_list";
private static final String EXTRA_STICKER = "sticker_extra";
private static final String EXTRA_BORDERLESS = "borderless_extra";
private static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
private static final String EXTRA_STARTING_POSITION = "starting_position";
private static final String BUBBLE_AUTHORITY = "bubble";
private static final String EXTRA_RECIPIENT = "recipient_id";
private static final String EXTRA_THREAD_ID = "thread_id";
private static final String EXTRA_TEXT = "draft_text";
private static final String EXTRA_MEDIA = "media_list";
private static final String EXTRA_STICKER = "sticker_extra";
private static final String EXTRA_BORDERLESS = "borderless_extra";
private static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
private static final String EXTRA_STARTING_POSITION = "starting_position";
private static final String EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP = "first_time_in_group";
private ConversationIntents() {
}
@@ -63,7 +64,8 @@ public class ConversationIntents {
private final StickerLocator stickerLocator;
private final boolean isBorderless;
private final int distributionType;
private final int startingPosition;
private final int startingPosition;
private final boolean firstTimeInSelfCreatedGroup;
static Args from(@NonNull Intent intent) {
if (isBubbleIntent(intent)) {
@@ -74,7 +76,8 @@ public class ConversationIntents {
null,
false,
ThreadDatabase.DistributionTypes.DEFAULT,
-1);
-1,
false);
}
return new Args(RecipientId.from(Objects.requireNonNull(intent.getStringExtra(EXTRA_RECIPIENT))),
@@ -84,7 +87,8 @@ public class ConversationIntents {
intent.getParcelableExtra(EXTRA_STICKER),
intent.getBooleanExtra(EXTRA_BORDERLESS, false),
intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, ThreadDatabase.DistributionTypes.DEFAULT),
intent.getIntExtra(EXTRA_STARTING_POSITION, -1));
intent.getIntExtra(EXTRA_STARTING_POSITION, -1),
intent.getBooleanExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, false));
}
private Args(@NonNull RecipientId recipientId,
@@ -94,16 +98,18 @@ public class ConversationIntents {
@Nullable StickerLocator stickerLocator,
boolean isBorderless,
int distributionType,
int startingPosition)
int startingPosition,
boolean firstTimeInSelfCreatedGroup)
{
this.recipientId = recipientId;
this.threadId = threadId;
this.draftText = draftText;
this.media = media;
this.stickerLocator = stickerLocator;
this.isBorderless = isBorderless;
this.distributionType = distributionType;
this.startingPosition = startingPosition;
this.recipientId = recipientId;
this.threadId = threadId;
this.draftText = draftText;
this.media = media;
this.stickerLocator = stickerLocator;
this.isBorderless = isBorderless;
this.distributionType = distributionType;
this.startingPosition = startingPosition;
this.firstTimeInSelfCreatedGroup = firstTimeInSelfCreatedGroup;
}
public @NonNull RecipientId getRecipientId() {
@@ -137,6 +143,10 @@ public class ConversationIntents {
public boolean isBorderless() {
return isBorderless;
}
public boolean isFirstTimeInSelfCreatedGroup() {
return firstTimeInSelfCreatedGroup;
}
}
public final static class Builder {
@@ -153,6 +163,7 @@ public class ConversationIntents {
private int startingPosition = -1;
private Uri dataUri;
private String dataType;
private boolean firstTimeInSelfCreatedGroup;
private Builder(@NonNull Context context,
@NonNull RecipientId recipientId,
@@ -212,6 +223,11 @@ public class ConversationIntents {
return this;
}
public Builder firstTimeInSelfCreatedGroup() {
this.firstTimeInSelfCreatedGroup = true;
return this;
}
public @NonNull Intent build() {
if (stickerLocator != null && media != null) {
throw new IllegalStateException("Cannot have both sticker and media array");
@@ -235,6 +251,7 @@ public class ConversationIntents {
intent.putExtra(EXTRA_DISTRIBUTION_TYPE, distributionType);
intent.putExtra(EXTRA_STARTING_POSITION, startingPosition);
intent.putExtra(EXTRA_BORDERLESS, isBorderless);
intent.putExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, firstTimeInSelfCreatedGroup);
if (draftText != null) {
intent.putExtra(EXTRA_TEXT, draftText);

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.database.ContentObserver;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import androidx.annotation.NonNull;
@@ -35,7 +36,7 @@ class ConversationStickerViewModel extends ViewModel {
this.stickers = new MutableLiveData<>();
this.stickersAvailable = new MutableLiveData<>();
this.availabilityThrottler = new Throttler(500);
this.packObserver = new ContentObserver(new Handler()) {
this.packObserver = new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
availabilityThrottler.publish(() -> repository.getStickerFeatureAvailability(stickersAvailable::postValue));

View File

@@ -50,15 +50,18 @@ public final class ConversationUpdateItem extends LinearLayout
private TextView body;
private TextView actionButton;
private LiveRecipient sender;
private ConversationMessage conversationMessage;
private Recipient conversationRecipient;
private Optional<MessageRecord> nextMessageRecord;
private MessageRecord messageRecord;
private LiveData<Spannable> displayBody;
private EventListener eventListener;
private final UpdateObserver updateObserver = new UpdateObserver();
private final SenderObserver senderObserver = new SenderObserver();
private final PresentOnChange presentOnChange = new PresentOnChange();
private final RecipientObserverManager senderObserver = new RecipientObserverManager(presentOnChange);
private final RecipientObserverManager groupObserver = new RecipientObserverManager(presentOnChange);
public ConversationUpdateItem(Context context) {
super(context);
@@ -91,7 +94,7 @@ public final class ConversationUpdateItem extends LinearLayout
{
this.batchSelected = batchSelected;
bind(lifecycleOwner, conversationMessage, nextMessageRecord);
bind(lifecycleOwner, conversationMessage, nextMessageRecord, conversationRecipient);
}
@Override
@@ -106,20 +109,26 @@ public final class ConversationUpdateItem extends LinearLayout
private void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ConversationMessage conversationMessage,
@NonNull Optional<MessageRecord> nextMessageRecord)
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull Recipient conversationRecipient)
{
this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord();
this.nextMessageRecord = nextMessageRecord;
this.conversationMessage = conversationMessage;
this.messageRecord = conversationMessage.getMessageRecord();
this.nextMessageRecord = nextMessageRecord;
this.conversationRecipient = conversationRecipient;
observeSender(lifecycleOwner, messageRecord.getIndividualRecipient());
senderObserver.observe(lifecycleOwner, messageRecord.getIndividualRecipient());
if (conversationRecipient.isActiveGroup() && conversationMessage.getMessageRecord().isGroupCall()) {
groupObserver.observe(lifecycleOwner, conversationRecipient);
} else {
groupObserver.observe(lifecycleOwner, null);
}
UpdateDescription updateDescription = Objects.requireNonNull(messageRecord.getUpdateDisplayBody(getContext()));
LiveData<Spannable> liveUpdateMessage = LiveUpdateMessage.fromMessageDescription(getContext(), updateDescription, ContextCompat.getColor(getContext(), R.color.conversation_item_update_text_color));
LiveData<Spannable> spannableMessage = loading(liveUpdateMessage);
present(conversationMessage, nextMessageRecord);
observeDisplayBody(lifecycleOwner, spannableMessage);
}
@@ -132,16 +141,31 @@ public final class ConversationUpdateItem extends LinearLayout
public void unbind() {
}
private void observeSender(@NonNull LifecycleOwner lifecycleOwner, @Nullable Recipient recipient) {
if (sender != null) {
sender.getLiveData().removeObserver(senderObserver);
static final class RecipientObserverManager {
private final Observer<Recipient> recipientObserver;
private LiveRecipient recipient;
RecipientObserverManager(@NonNull Observer<Recipient> observer){
this.recipientObserver = observer;
}
if (recipient != null) {
sender = recipient.live();
sender.getLiveData().observe(lifecycleOwner, senderObserver);
} else {
sender = null;
public void observe(@NonNull LifecycleOwner lifecycleOwner, @Nullable Recipient recipient) {
if (this.recipient != null) {
this.recipient.getLiveData().removeObserver(recipientObserver);
}
if (recipient != null) {
this.recipient = recipient.live();
this.recipient.getLiveData().observe(lifecycleOwner, recipientObserver);
} else {
this.recipient = null;
}
}
@NonNull Recipient getObservedRecipient() {
return recipient.get();
}
}
@@ -168,7 +192,7 @@ public final class ConversationUpdateItem extends LinearLayout
}
}
private void present(ConversationMessage conversationMessage, @NonNull Optional<MessageRecord> nextMessageRecord) {
private void present(ConversationMessage conversationMessage, @NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Recipient conversationRecipient) {
if (batchSelected.contains(conversationMessage)) setSelected(true);
else setSelected(false);
@@ -182,20 +206,40 @@ public final class ConversationUpdateItem extends LinearLayout
eventListener.onGroupMigrationLearnMoreClicked(conversationMessage.getMessageRecord().getGroupV1MigrationMembershipChanges());
}
});
} else if (conversationMessage.getMessageRecord().isFailedDecryptionType() &&
(!nextMessageRecord.isPresent() || !nextMessageRecord.get().isFailedDecryptionType()))
{
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onDecryptionFailedLearnMoreClicked();
}
});
} else if (conversationMessage.getMessageRecord().isIdentityUpdate()) {
actionButton.setText(R.string.ConversationUpdateItem_learn_more);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onSafetyNumberLearnMoreClicked(conversationMessage.getMessageRecord().getIndividualRecipient());
}
});
} else if (conversationMessage.getMessageRecord().isGroupCall()) {
UpdateDescription updateDescription = MessageRecord.getGroupCallUpdateDescription(getContext(), conversationMessage.getMessageRecord().getBody(), true);
Collection<UUID> uuids = updateDescription.getMentioned();
int text = 0;
if (Util.hasItems(uuids)) {
if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) {
if (uuids.contains(TextSecurePreferences.getLocalUuid(getContext()))) {
text = R.string.ConversationUpdateItem_return_to_call;
} else if (GroupCallUpdateDetailsUtil.parse(conversationMessage.getMessageRecord().getBody()).getIsCallFull()) {
text = R.string.ConversationUpdateItem_call_is_full;
} else {
text = uuids.contains(TextSecurePreferences.getLocalUuid(getContext())) ? R.string.ConversationUpdateItem_return_to_call : R.string.ConversationUpdateItem_join_call;
text = R.string.ConversationUpdateItem_join_call;
}
}
if (text != 0) {
if (text != 0 && conversationRecipient.isGroup() && conversationRecipient.isActiveGroup()) {
actionButton.setText(text);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
@@ -207,6 +251,14 @@ public final class ConversationUpdateItem extends LinearLayout
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
}
} else if (conversationMessage.getMessageRecord().isSelfCreatedGroup()) {
actionButton.setText(R.string.ConversationUpdateItem_invite_friends);
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
eventListener.onInviteFriendsToGroupClicked(conversationRecipient.requireGroupId().requireV2());
}
});
} else {
actionButton.setVisibility(GONE);
actionButton.setOnClickListener(null);
@@ -218,11 +270,14 @@ public final class ConversationUpdateItem extends LinearLayout
super.setOnClickListener(new InternalClickListener(l));
}
private final class SenderObserver implements Observer<Recipient> {
private final class PresentOnChange implements Observer<Recipient> {
@Override
public void onChanged(Recipient recipient) {
present(conversationMessage, nextMessageRecord);
if (recipient.getId() == conversationRecipient.getId()) {
conversationRecipient = recipient;
}
present(conversationMessage, nextMessageRecord, conversationRecipient);
}
}
@@ -253,7 +308,7 @@ public final class ConversationUpdateItem extends LinearLayout
return;
}
final Recipient sender = ConversationUpdateItem.this.sender.get();
final Recipient sender = ConversationUpdateItem.this.senderObserver.getObservedRecipient();
IdentityUtil.getRemoteIdentityKey(getContext(), sender).addListener(new ListenableFuture.Listener<Optional<IdentityRecord>>() {
@Override

View File

@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Application;
import android.database.ContentObserver;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
@@ -16,7 +15,7 @@ import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.signal.paging.ProxyPagingController;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.mediasend.MediaRepository;
@@ -41,11 +40,10 @@ class ConversationViewModel extends ViewModel {
private final MutableLiveData<Boolean> hasUnreadMentions;
private final LiveData<Boolean> canShowAsBubble;
private final ProxyPagingController pagingController;
private final ContentObserver messageObserver;
private final DatabaseObserver.Observer messageObserver;
private ConversationIntents.Args args;
private int jumpToPosition;
private boolean hasRegisteredObserver;
private ConversationViewModel() {
this.context = ApplicationDependencies.getApplication();
@@ -56,12 +54,7 @@ class ConversationViewModel extends ViewModel {
this.showScrollButtons = new MutableLiveData<>(false);
this.hasUnreadMentions = new MutableLiveData<>(false);
this.pagingController = new ProxyPagingController();
this.messageObserver = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
pagingController.onDataInvalidated();
}
};
this.messageObserver = pagingController::onDataInvalidated;
LiveData<ConversationData> metadata = Transformations.switchMap(threadId, thread -> {
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(thread, jumpToPosition);
@@ -83,17 +76,13 @@ class ConversationViewModel extends ViewModel {
startPosition = data.getThreadSize();
}
if (hasRegisteredObserver) {
context.getContentResolver().unregisterContentObserver(messageObserver);
}
context.getContentResolver().registerContentObserver(DatabaseContentProviders.Conversation.getUriForThread(data.getThreadId()), true, messageObserver);
hasRegisteredObserver = true;
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver);
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), messageObserver);
ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId());
PagingConfig config = new PagingConfig.Builder()
.setPageSize(25)
.setBufferPages(1)
.setBufferPages(3)
.setStartIndex(Math.max(startPosition, 0))
.build();
@@ -182,7 +171,7 @@ class ConversationViewModel extends ViewModel {
@Override
protected void onCleared() {
super.onCleared();
context.getContentResolver().unregisterContentObserver(messageObserver);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver);
}
static class Factory extends ViewModelProvider.NewInstanceFactory {

View File

@@ -131,7 +131,8 @@ final class MenuState {
messageRecord.isIdentityUpdate() ||
messageRecord.isIdentityVerified() ||
messageRecord.isIdentityDefault() ||
messageRecord.isProfileChange();
messageRecord.isProfileChange() ||
messageRecord.isFailedDecryptionType();
}
private final static class Builder {

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.WebRtcCallService;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Objects;
@@ -23,31 +24,37 @@ public class GroupCallViewModel extends ViewModel {
private static final String TAG = Log.tag(GroupCallViewModel.class);
private final MutableLiveData<Boolean> activeGroupCall;
private final MutableLiveData<Boolean> canJoin;
private final MutableLiveData<Boolean> activeGroup;
private final MutableLiveData<Boolean> ongoingGroupCall;
private final LiveData<Boolean> activeGroupCall;
private final MutableLiveData<Boolean> groupCallHasCapacity;
private @Nullable Recipient currentRecipient;
GroupCallViewModel() {
this.activeGroupCall = new MutableLiveData<>(false);
this.canJoin = new MutableLiveData<>(false);
this.activeGroup = new MutableLiveData<>(false);
this.ongoingGroupCall = new MutableLiveData<>(false);
this.groupCallHasCapacity = new MutableLiveData<>(false);
this.activeGroupCall = LiveDataUtil.combineLatest(activeGroup, ongoingGroupCall, (active, ongoing) -> active && ongoing);
}
public @NonNull LiveData<Boolean> hasActiveGroupCall() {
return activeGroupCall;
}
public @NonNull LiveData<Boolean> canJoinGroupCall() {
return canJoin;
public @NonNull LiveData<Boolean> groupCallHasCapacity() {
return groupCallHasCapacity;
}
public void onRecipientChange(@NonNull Context context, @Nullable Recipient recipient) {
activeGroup.postValue(recipient != null && recipient.isActiveGroup());
if (Objects.equals(currentRecipient, recipient)) {
return;
}
activeGroupCall.postValue(false);
canJoin.postValue(false);
ongoingGroupCall.postValue(false);
groupCallHasCapacity.postValue(false);
currentRecipient = recipient;
@@ -67,10 +74,10 @@ public class GroupCallViewModel extends ViewModel {
public void onGroupCallPeekEvent(@NonNull GroupCallPeekEvent groupCallPeekEvent) {
if (isGroupCallCapable(currentRecipient) && groupCallPeekEvent.getGroupRecipientId().equals(currentRecipient.getId())) {
Log.i(TAG, "update UI with call event: active call: " + groupCallPeekEvent.hasActiveCall() + " canJoin: " + groupCallPeekEvent.canJoinCall());
Log.i(TAG, "update UI with call event: ongoing call: " + groupCallPeekEvent.isOngoing() + " hasCapacity: " + groupCallPeekEvent.callHasCapacity());
activeGroupCall.postValue(groupCallPeekEvent.hasActiveCall());
canJoin.postValue(groupCallPeekEvent.canJoinCall());
ongoingGroupCall.postValue(groupCallPeekEvent.isOngoing());
groupCallHasCapacity.postValue(groupCallPeekEvent.callHasCapacity());
} else {
Log.i(TAG, "Ignore call event for different recipient.");
}

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.ui.mentions;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -32,7 +33,7 @@ public class MentionsPickerFragment extends LoggingFragment {
private BottomSheetBehavior<View> behavior;
private MentionsPickerViewModel viewModel;
private final Runnable lockSheetAfterListUpdate = () -> behavior.setHideable(false);
private final Handler handler = new Handler();
private final Handler handler = new Handler(Looper.getMainLooper());
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {

View File

@@ -7,10 +7,13 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.paging.PagedListAdapter;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
@@ -28,7 +31,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerView.ViewHolder> {
class ConversationListAdapter extends ListAdapter<Conversation, RecyclerView.ViewHolder> {
private static final int TYPE_THREAD = 1;
private static final int TYPE_ACTION = 2;
@@ -46,6 +49,8 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
private boolean batchMode = false;
private final Set<Long> typingSet = new HashSet<>();
private PagingController pagingController;
protected ConversationListAdapter(@NonNull GlideRequests glideRequests,
@NonNull OnConversationClickListener onConversationClickListener)
{
@@ -156,6 +161,19 @@ class ConversationListAdapter extends PagedListAdapter<Conversation, RecyclerVie
}
}
@Override
protected Conversation getItem(int position) {
if (pagingController != null) {
pagingController.onDataNeededAroundIndex(position);
}
return super.getItem(position);
}
public void setPagingController(@Nullable PagingController pagingController) {
this.pagingController = pagingController;
}
void setTypingThreads(@NonNull Set<Long> typingThreadSet) {
this.typingSet.clear();
this.typingSet.addAll(typingThreadSet);

View File

@@ -37,19 +37,19 @@ import com.google.android.material.snackbar.Snackbar;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
import org.thoughtcrime.securesms.util.views.Stub;
import java.util.Set;
@Trace
public class ConversationListArchiveFragment extends ConversationListFragment implements ActionMode.Callback
{
private RecyclerView list;
private View emptyState;
private Stub<View> emptyState;
private PulsingFloatingActionButton fab;
private PulsingFloatingActionButton cameraFab;
private Stub<Toolbar> toolbar;
public static ConversationListArchiveFragment newInstance() {
return new ConversationListArchiveFragment();
@@ -63,26 +63,30 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
toolbar = new Stub<>(view.findViewById(R.id.toolbar_basic));
super.onViewCreated(view, savedInstanceState);
list = view.findViewById(R.id.list);
fab = view.findViewById(R.id.fab);
cameraFab = view.findViewById(R.id.camera_fab);
emptyState = view.findViewById(R.id.empty_state);
emptyState = new Stub<>(view.findViewById(R.id.empty_state));
((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
Toolbar toolbar = view.findViewById(R.id.toolbar_basic);
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
toolbar.setTitle(R.string.AndroidManifest_archived_conversations);
toolbar.get().setNavigationOnClickListener(v -> requireActivity().onBackPressed());
toolbar.get().setTitle(R.string.AndroidManifest_archived_conversations);
fab.hide();
cameraFab.hide();
}
@Override
protected void onPostSubmitList() {
protected void onPostSubmitList(int conversationCount) {
list.setVisibility(View.VISIBLE);
emptyState.setVisibility(View.GONE);
if (emptyState.resolved()) {
emptyState.get().setVisibility(View.GONE);
}
}
@Override
@@ -91,8 +95,8 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
}
@Override
protected int getToolbarRes() {
return R.id.toolbar_basic;
protected @NonNull Toolbar getToolbar(@NonNull View rootView) {
return toolbar.get();
}
@Override
@@ -144,6 +148,11 @@ public class ConversationListArchiveFragment extends ConversationListFragment im
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
}
@Override
void updateEmptyState(boolean isConversationEmpty) {
// Do nothing
}
}

View File

@@ -1,122 +1,76 @@
package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MergeCursor;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.paging.DataSource;
import androidx.paging.PositionalDataSource;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagedDataSource;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.ConversationReader;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
import org.thoughtcrime.securesms.util.Stopwatch;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.Executor;
@Trace
abstract class ConversationListDataSource extends PositionalDataSource<Conversation> {
public static final Executor EXECUTOR = SignalExecutors.newFixedLifoThreadExecutor("signal-conversation-list", 1, 1);
private static final ThrottledDebouncer THROTTLER = new ThrottledDebouncer(500);
abstract class ConversationListDataSource implements PagedDataSource<Conversation> {
private static final String TAG = Log.tag(ConversationListDataSource.class);
protected final ThreadDatabase threadDatabase;
protected ConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
protected ConversationListDataSource(@NonNull Context context) {
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
ContentObserver contentObserver = new ContentObserver(null) {
@Override
public void onChange(boolean selfChange) {
THROTTLER.publish(() -> {
invalidate();
context.getContentResolver().unregisterContentObserver(this);
});
}
};
invalidator.observe(() -> {
invalidate();
context.getContentResolver().unregisterContentObserver(contentObserver);
});
context.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, contentObserver);
}
private static ConversationListDataSource create(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
if (!isArchived) return new UnarchivedConversationListDataSource(context, invalidator);
else return new ArchivedConversationListDataSource(context, invalidator);
public static ConversationListDataSource create(@NonNull Context context, boolean isArchived) {
if (!isArchived) return new UnarchivedConversationListDataSource(context);
else return new ArchivedConversationListDataSource(context);
}
@Override
public final void loadInitial(@NonNull LoadInitialParams params, @NonNull LoadInitialCallback<Conversation> callback) {
long start = System.currentTimeMillis();
public int size() {
long startTime = System.currentTimeMillis();
int count = getTotalCount();
List<Conversation> conversations = new ArrayList<>(params.requestedLoadSize);
int totalCount = getTotalCount();
int effectiveCount = params.requestedStartPosition;
Log.d(TAG, "[size(), " + getClass().getSimpleName() + "] " + (System.currentTimeMillis() - startTime) + " ms");
return count;
}
@Override
public @NonNull List<Conversation> load(int start, int length, @NonNull CancellationSignal cancellationSignal) {
Stopwatch stopwatch = new Stopwatch("load(" + start + ", " + length + "), " + getClass().getSimpleName());
List<Conversation> conversations = new ArrayList<>(length);
List<Recipient> recipients = new LinkedList<>();
try (ConversationReader reader = new ConversationReader(getCursor(params.requestedStartPosition, params.requestedLoadSize))) {
try (ConversationReader reader = new ConversationReader(getCursor(start, length))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
conversations.add(new Conversation(record));
recipients.add(record.getRecipient());
effectiveCount++;
}
}
ApplicationDependencies.getRecipientCache().addToCache(recipients);
if (!isInvalid()) {
SizeFixResult<Conversation> result = SizeFixResult.ensureMultipleOfPageSize(conversations, params.requestedStartPosition, params.pageSize, totalCount);
callback.onResult(result.getItems(), params.requestedStartPosition, result.getTotal());
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", actualSize: " + result.getItems().size() + ", totalCount: " + result.getTotal() + ", class: " + getClass().getSimpleName());
} else {
Log.d(TAG, "[Initial Load] " + (System.currentTimeMillis() - start) + " ms | start: " + params.requestedStartPosition + ", requestedSize: " + params.requestedLoadSize + ", totalCount: " + totalCount + ", class: " + getClass().getSimpleName() + " -- invalidated");
}
}
@Override
public final void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<Conversation> callback) {
long start = System.currentTimeMillis();
List<Conversation> conversations = new ArrayList<>(params.loadSize);
List<Recipient> recipients = new LinkedList<>();
try (ConversationReader reader = new ConversationReader(getCursor(params.startPosition, params.loadSize))) {
ThreadRecord record;
while ((record = reader.getNext()) != null && !isInvalid()) {
while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) {
conversations.add(new Conversation(record));
recipients.add(record.getRecipient());
}
}
stopwatch.split("cursor");
ApplicationDependencies.getRecipientCache().addToCache(recipients);
callback.onResult(conversations);
stopwatch.split("cache-recipients");
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | start: " + params.startPosition + ", size: " + params.loadSize + ", class: " + getClass().getSimpleName() + (isInvalid() ? " -- invalidated" : ""));
stopwatch.stop(TAG);
return conversations;
}
protected abstract int getTotalCount();
@@ -124,8 +78,8 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
private static class ArchivedConversationListDataSource extends ConversationListDataSource {
ArchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
super(context, invalidator);
ArchivedConversationListDataSource(@NonNull Context context) {
super(context);
}
@Override
@@ -147,8 +101,8 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
private int archivedCount;
private int unpinnedCount;
UnarchivedConversationListDataSource(@NonNull Context context, @NonNull Invalidator invalidator) {
super(context, invalidator);
UnarchivedConversationListDataSource(@NonNull Context context) {
super(context);
}
@Override
@@ -158,14 +112,27 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
pinnedCount = threadDatabase.getPinnedConversationListCount();
archivedCount = threadDatabase.getArchivedConversationListCount();
unpinnedCount = unarchivedCount - pinnedCount;
totalCount = unarchivedCount + (archivedCount != 0 ? 1 : 0) + (pinnedCount != 0 ? (unpinnedCount != 0 ? 2 : 1) : 0);
totalCount = unarchivedCount;
if (archivedCount != 0) {
totalCount++;
}
if (pinnedCount != 0) {
if (unpinnedCount != 0) {
totalCount += 2;
} else {
totalCount += 1;
}
}
return totalCount;
}
@Override
protected Cursor getCursor(long offset, long limit) {
List<Cursor> cursors = new ArrayList<>(5);
List<Cursor> cursors = new ArrayList<>(5);
long originalLimit = limit;
if (offset == 0 && hasPinnedHeader()) {
MatrixCursor pinnedHeaderCursor = new MatrixCursor(ConversationReader.HEADER_COLUMN);
@@ -189,7 +156,7 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
Cursor unpinnedCursor = threadDatabase.getUnarchivedConversationList(false, unpinnedOffset, limit);
cursors.add(unpinnedCursor);
if (offset + limit >= totalCount && hasArchivedFooter()) {
if (offset + originalLimit >= totalCount && hasArchivedFooter()) {
MatrixCursor archivedFooterCursor = new MatrixCursor(ConversationReader.ARCHIVED_COLUMNS);
archivedFooterCursor.addRow(ConversationReader.createArchivedFooterRow(archivedCount));
cursors.add(archivedFooterCursor);
@@ -218,22 +185,4 @@ abstract class ConversationListDataSource extends PositionalDataSource<Conversat
return archivedCount != 0;
}
}
static class Factory extends DataSource.Factory<Integer, Conversation> {
private final Context context;
private final Invalidator invalidator;
private final boolean isArchived;
public Factory(@NonNull Context context, @NonNull Invalidator invalidator, boolean isArchived) {
this.context = context;
this.invalidator = invalidator;
this.isArchived = isArchived;
}
@Override
public @NonNull DataSource<Integer, Conversation> create() {
return ConversationListDataSource.create(context, invalidator, isArchived);
}
}
}

View File

@@ -80,7 +80,6 @@ import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SearchToolbar;
import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
import org.thoughtcrime.securesms.components.reminder.DefaultSmsReminder;
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
import org.thoughtcrime.securesms.components.reminder.OutdatedBuildReminder;
@@ -88,8 +87,6 @@ import org.thoughtcrime.securesms.components.reminder.PushRegistrationReminder;
import org.thoughtcrime.securesms.components.reminder.Reminder;
import org.thoughtcrime.securesms.components.reminder.ReminderView;
import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
import org.thoughtcrime.securesms.components.reminder.ShareReminder;
import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
import org.thoughtcrime.securesms.conversation.ConversationFragment;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
@@ -103,6 +100,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
import org.thoughtcrime.securesms.insights.InsightsLauncher;
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateKbsPinActivity;
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
import org.thoughtcrime.securesms.megaphone.Megaphone;
@@ -116,18 +114,19 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.PlayStoreUtil;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collections;
@@ -141,7 +140,6 @@ import java.util.Set;
import static android.app.Activity.RESULT_OK;
@Trace
public class ConversationListFragment extends MainFragment implements ActionMode.Callback,
ConversationListAdapter.OnConversationClickListener,
ConversationListSearchAdapter.EventListener,
@@ -155,21 +153,14 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private static final int MAXIMUM_PINNED_CONVERSATIONS = 4;
private static final int[] EMPTY_IMAGES = new int[] { R.drawable.empty_inbox_1,
R.drawable.empty_inbox_2,
R.drawable.empty_inbox_3,
R.drawable.empty_inbox_4,
R.drawable.empty_inbox_5 };
private ActionMode actionMode;
private RecyclerView list;
private ReminderView reminderView;
private View emptyState;
private ImageView emptyImage;
private Stub<ReminderView> reminderView;
private Stub<ViewGroup> emptyState;
private TextView searchEmptyState;
private PulsingFloatingActionButton fab;
private PulsingFloatingActionButton cameraFab;
private SearchToolbar searchToolbar;
private Stub<SearchToolbar> searchToolbar;
private ImageView searchAction;
private View toolbarShadow;
private ConversationListViewModel viewModel;
@@ -177,11 +168,13 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private ConversationListAdapter defaultAdapter;
private ConversationListSearchAdapter searchAdapter;
private StickyHeaderDecoration searchAdapterDecoration;
private ViewGroup megaphoneContainer;
private Stub<ViewGroup> megaphoneContainer;
private SnapToTopDataObserver snapToTopDataObserver;
private Drawable archiveDrawable;
private LifecycleObserver visibilityLifecycleObserver;
private Stopwatch startupStopwatch;
public static ConversationListFragment newInstance() {
return new ConversationListFragment();
}
@@ -190,6 +183,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
setHasOptionsMenu(true);
startupStopwatch = new Stopwatch("startup");
}
@Override
@@ -199,28 +193,24 @@ public class ConversationListFragment extends MainFragment implements ActionMode
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
reminderView = view.findViewById(R.id.reminder);
list = view.findViewById(R.id.list);
fab = view.findViewById(R.id.fab);
cameraFab = view.findViewById(R.id.camera_fab);
emptyState = view.findViewById(R.id.empty_state);
emptyImage = view.findViewById(R.id.empty);
searchEmptyState = view.findViewById(R.id.search_no_results);
searchToolbar = view.findViewById(R.id.search_toolbar);
searchAction = view.findViewById(R.id.search_action);
toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
megaphoneContainer = view.findViewById(R.id.megaphone_container);
reminderView = new Stub<>(view.findViewById(R.id.reminder));
emptyState = new Stub<>(view.findViewById(R.id.empty_state));
searchToolbar = new Stub<>(view.findViewById(R.id.search_toolbar));
megaphoneContainer = new Stub<>(view.findViewById(R.id.megaphone_container));
Toolbar toolbar = view.findViewById(getToolbarRes());
Toolbar toolbar = getToolbar(view);
toolbar.setVisibility(View.VISIBLE);
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
fab.show();
cameraFab.show();
reminderView.setOnDismissListener(this::updateReminders);
reminderView.setOnActionClickListener(this::onReminderAction);
list.setLayoutManager(new LinearLayoutManager(requireActivity()));
list.setItemAnimator(new DeleteItemAnimator());
list.addOnScrollListener(new ScrollListener());
@@ -241,8 +231,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
.execute();
});
initializeListAdapters();
initializeViewModel();
initializeListAdapters();
initializeTypingObserver();
initializeSearchListener();
@@ -264,7 +254,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
SimpleTask.run(getViewLifecycleOwner().getLifecycle(), Recipient::self, this::initializeProfileIcon);
if (!searchToolbar.isVisible() && list.getAdapter() != defaultAdapter) {
if ((!searchToolbar.resolved() || !searchToolbar.get().isVisible()) && list.getAdapter() != defaultAdapter) {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
@@ -330,10 +320,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
private boolean closeSearchIfOpen() {
if (searchToolbar.isVisible() || activeAdapter == searchAdapter) {
if ((searchToolbar.resolved() && searchToolbar.get().isVisible()) || activeAdapter == searchAdapter) {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
searchToolbar.collapse();
searchToolbar.get().collapse();
return true;
}
@@ -430,6 +420,11 @@ public class ConversationListFragment extends MainFragment implements ActionMode
dialogFragment.show(getChildFragmentManager(), "megaphone_dialog");
}
private void initializeReminderView() {
reminderView.get().setOnDismissListener(this::updateReminders);
reminderView.get().setOnActionClickListener(this::onReminderAction);
}
private void onReminderAction(@IdRes int reminderActionId) {
if (reminderActionId == R.id.reminder_action_update_now) {
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(requireContext());
@@ -450,36 +445,36 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private void initializeSearchListener() {
searchAction.setOnClickListener(v -> {
searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2.0f),
searchAction.getY() + (searchAction.getHeight() / 2.0f));
});
searchToolbar.get().display(searchAction.getX() + (searchAction.getWidth() / 2.0f),
searchAction.getY() + (searchAction.getHeight() / 2.0f));
searchToolbar.setListener(new SearchToolbar.SearchListener() {
@Override
public void onSearchTextChange(String text) {
String trimmed = text.trim();
searchToolbar.get().setListener(new SearchToolbar.SearchListener() {
@Override
public void onSearchTextChange(String text) {
String trimmed = text.trim();
viewModel.updateQuery(trimmed);
viewModel.updateQuery(trimmed);
if (trimmed.length() > 0) {
if (activeAdapter != searchAdapter) {
setAdapter(searchAdapter);
list.removeItemDecoration(searchAdapterDecoration);
list.addItemDecoration(searchAdapterDecoration);
}
} else {
if (activeAdapter != defaultAdapter) {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
if (trimmed.length() > 0) {
if (activeAdapter != searchAdapter) {
setAdapter(searchAdapter);
list.removeItemDecoration(searchAdapterDecoration);
list.addItemDecoration(searchAdapterDecoration);
}
} else {
if (activeAdapter != defaultAdapter) {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
}
}
}
@Override
public void onSearchClosed() {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
@Override
public void onSearchClosed() {
list.removeItemDecoration(searchAdapterDecoration);
setAdapter(defaultAdapter);
}
});
});
}
@@ -489,6 +484,19 @@ public class ConversationListFragment extends MainFragment implements ActionMode
searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
setAdapter(defaultAdapter);
defaultAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
startupStopwatch.split("data-set");
defaultAdapter.unregisterAdapterDataObserver(this);
list.post(() -> {
AppStartup.getInstance().onCriticalRenderEventEnd();
startupStopwatch.split("first-render");
startupStopwatch.stop(TAG);
});
}
});
}
@SuppressWarnings("rawtypes")
@@ -501,6 +509,10 @@ public class ConversationListFragment extends MainFragment implements ActionMode
return;
}
if (adapter instanceof ConversationListAdapter) {
((ConversationListAdapter) adapter).setPagingController(viewModel.getPagingController());
}
list.setAdapter(adapter);
if (adapter == defaultAdapter) {
@@ -554,20 +566,22 @@ public class ConversationListFragment extends MainFragment implements ActionMode
private void onMegaphoneChanged(@Nullable Megaphone megaphone) {
if (megaphone == null) {
megaphoneContainer.setVisibility(View.GONE);
megaphoneContainer.removeAllViews();
if (megaphoneContainer.resolved()) {
megaphoneContainer.get().setVisibility(View.GONE);
megaphoneContainer.get().removeAllViews();
}
return;
}
View view = MegaphoneViewBuilder.build(requireContext(), megaphone, this);
megaphoneContainer.removeAllViews();
megaphoneContainer.get().removeAllViews();
if (view != null) {
megaphoneContainer.addView(view);
megaphoneContainer.setVisibility(View.VISIBLE);
megaphoneContainer.get().addView(view);
megaphoneContainer.get().setVisibility(View.VISIBLE);
} else {
megaphoneContainer.setVisibility(View.GONE);
megaphoneContainer.get().setVisibility(View.GONE);
if (megaphone.getOnVisibleListener() != null) {
megaphone.getOnVisibleListener().onEvent(megaphone, this);
@@ -590,14 +604,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
return Optional.of(new ServiceOutageReminder(context));
} else if (OutdatedBuildReminder.isEligible()) {
return Optional.of(new OutdatedBuildReminder(context));
} else if (DefaultSmsReminder.isEligible(context)) {
return Optional.of(new DefaultSmsReminder(this, SMS_ROLE_REQUEST_CODE));
} else if (Util.isDefaultSmsProvider(context) && SystemSmsImportReminder.isEligible(context)) {
return Optional.of((new SystemSmsImportReminder(context)));
} else if (PushRegistrationReminder.isEligible(context)) {
return Optional.of((new PushRegistrationReminder(context)));
} else if (ShareReminder.isEligible(context)) {
return Optional.of(new ShareReminder(context));
} else if (DozeReminder.isEligible(context)) {
return Optional.of(new DozeReminder(context));
} else {
@@ -605,9 +613,12 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
}, reminder -> {
if (reminder.isPresent() && getActivity() != null && !isRemoving()) {
reminderView.showReminder(reminder.get());
} else if (!reminder.isPresent()) {
reminderView.hide();
if (!reminderView.resolved()) {
initializeReminderView();
}
reminderView.get().showReminder(reminder.get());
} else if (reminderView.resolved() && !reminder.isPresent()) {
reminderView.get().hide();
}
});
}
@@ -819,33 +830,37 @@ public class ConversationListFragment extends MainFragment implements ActionMode
getNavigator().goToConversation(recipient.getId(), threadId, distributionType, -1);
}
private void onSubmitList(@NonNull ConversationListViewModel.ConversationList conversationList) {
if (conversationList.getConversations().isDetached()) {
return;
}
defaultAdapter.submitList(conversationList.getConversations());
onPostSubmitList();
private void onSubmitList(@NonNull List<Conversation> conversationList) {
defaultAdapter.submitList(conversationList);
onPostSubmitList(conversationList.size());
}
private void updateEmptyState(boolean isConversationEmpty) {
void updateEmptyState(boolean isConversationEmpty) {
if (isConversationEmpty) {
Log.i(TAG, "Received an empty data set.");
list.setVisibility(View.INVISIBLE);
emptyState.setVisibility(View.VISIBLE);
emptyImage.setImageResource(EMPTY_IMAGES[(int) (Math.random() * EMPTY_IMAGES.length)]);
emptyState.get().setVisibility(View.VISIBLE);
fab.startPulse(3 * 1000);
cameraFab.startPulse(3 * 1000);
SignalStore.onboarding().setShowNewGroup(true);
SignalStore.onboarding().setShowInviteFriends(true);
} else {
list.setVisibility(View.VISIBLE);
emptyState.setVisibility(View.GONE);
fab.stopPulse();
cameraFab.stopPulse();
if (emptyState.resolved()) {
emptyState.get().setVisibility(View.GONE);
}
}
}
protected void onPostSubmitList() {
protected void onPostSubmitList(int conversationCount) {
if (conversationCount >= 6 && (SignalStore.onboarding().shouldShowInviteFriends() || SignalStore.onboarding().shouldShowNewGroup())) {
SignalStore.onboarding().clearAll();
ApplicationDependencies.getMegaphoneRepository().markFinished(Megaphones.Event.ONBOARDING);
}
}
@Override
@@ -975,8 +990,8 @@ public class ConversationListFragment extends MainFragment implements ActionMode
}
}
protected @IdRes int getToolbarRes() {
return R.id.toolbar;
protected Toolbar getToolbar(@NonNull View rootView) {
return rootView.findViewById(R.id.toolbar);
}
protected @PluralsRes int getArchivedSnackbarTitleRes() {

View File

@@ -20,18 +20,18 @@ import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Typeface;
import android.graphics.drawable.RippleDrawable;
import android.os.Build.VERSION;
import android.os.Build;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.util.AttributeSet;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
@@ -64,7 +64,6 @@ import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Collections;
@@ -73,7 +72,7 @@ import java.util.Set;
import static org.thoughtcrime.securesms.database.model.LiveUpdateMessage.recipientToStringAsync;
public final class ConversationListItem extends RelativeLayout
public final class ConversationListItem extends ConstraintLayout
implements RecipientForeverObserver,
BindableConversationListItem,
Unbindable,
@@ -90,7 +89,6 @@ public final class ConversationListItem extends RelativeLayout
private LiveRecipient recipient;
private long threadId;
private GlideRequests glideRequests;
private View subjectContainer;
private TextView subjectView;
private TypingIndicatorView typingView;
private FromTextView fromView;
@@ -122,21 +120,17 @@ public final class ConversationListItem extends RelativeLayout
@Override
protected void onFinishInflate() {
super.onFinishInflate();
this.subjectContainer = findViewById(R.id.subject_container);
this.subjectView = findViewById(R.id.subject);
this.typingView = findViewById(R.id.typing_indicator);
this.fromView = findViewById(R.id.from);
this.dateView = findViewById(R.id.date);
this.deliveryStatusIndicator = findViewById(R.id.delivery_status);
this.alertView = findViewById(R.id.indicators_parent);
this.contactPhotoImage = findViewById(R.id.contact_photo_image);
this.thumbnailView = findViewById(R.id.thumbnail);
this.archivedView = findViewById(R.id.archived);
this.unreadIndicator = findViewById(R.id.unread_indicator);
this.subjectView = findViewById(R.id.conversation_list_item_summary);
this.typingView = findViewById(R.id.conversation_list_item_typing_indicator);
this.fromView = findViewById(R.id.conversation_list_item_name);
this.dateView = findViewById(R.id.conversation_list_item_date);
this.deliveryStatusIndicator = findViewById(R.id.conversation_list_item_status);
this.alertView = findViewById(R.id.conversation_list_item_alert);
this.contactPhotoImage = findViewById(R.id.conversation_list_item_avatar);
this.thumbnailView = findViewById(R.id.conversation_list_item_thumbnail);
this.archivedView = findViewById(R.id.conversation_list_item_archived);
this.unreadIndicator = findViewById(R.id.conversation_list_item_unread_indicator);
thumbnailView.setClickable(false);
ViewUtil.setTextViewGravityStart(this.fromView, getContext());
ViewUtil.setTextViewGravityStart(this.subjectView, getContext());
}
@Override
@@ -158,19 +152,17 @@ public final class ConversationListItem extends RelativeLayout
boolean batchMode,
@Nullable String highlightSubstring)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
observeRecipient(thread.getRecipient().live());
observeDisplayBody(null);
setSubjectViewText(null);
this.selectedThreads = selectedThreads;
this.recipient = thread.getRecipient().live();
this.threadId = thread.getThreadId();
this.glideRequests = glideRequests;
this.unreadCount = thread.getUnreadCount();
this.lastSeen = thread.getLastSeen();
this.thread = thread;
this.recipient.observeForever(this);
if (highlightSubstring != null) {
String name = recipient.get().isSelf() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
@@ -215,15 +207,13 @@ public final class ConversationListItem extends RelativeLayout
@NonNull Locale locale,
@Nullable String highlightSubstring)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
observeRecipient(contact.live());
observeDisplayBody(null);
setSubjectViewText(null);
this.selectedThreads = Collections.emptySet();
this.recipient = contact.live();
this.glideRequests = glideRequests;
this.recipient.observeForever(this);
fromView.setText(contact);
fromView.setText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), new SpannableString(fromView.getText()), highlightSubstring));
@@ -245,16 +235,13 @@ public final class ConversationListItem extends RelativeLayout
@NonNull Locale locale,
@Nullable String highlightSubstring)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
observeRecipient(messageResult.conversationRecipient.live());
observeDisplayBody(null);
setSubjectViewText(null);
this.selectedThreads = Collections.emptySet();
this.recipient = messageResult.conversationRecipient.live();
this.glideRequests = glideRequests;
this.recipient.observeForever(this);
fromView.setText(recipient.get(), true);
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, () -> new StyleSpan(Typeface.BOLD), messageResult.bodySnippet, highlightSubstring));
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.receivedTimestampMs));
@@ -272,9 +259,7 @@ public final class ConversationListItem extends RelativeLayout
@Override
public void unbind() {
if (this.recipient != null) {
this.recipient.removeForeverObserver(this);
this.recipient = null;
observeRecipient(null);
setBatchMode(false);
contactPhotoImage.setAvatar(glideRequests, null, !batchMode);
}
@@ -323,6 +308,18 @@ public final class ConversationListItem extends RelativeLayout
return lastSeen;
}
private void observeRecipient(@Nullable LiveRecipient newRecipient) {
if (this.recipient != null) {
this.recipient.removeForeverObserver(this);
}
this.recipient = newRecipient;
if (this.recipient != null) {
this.recipient.observeForever(this);
}
}
private void observeDisplayBody(@Nullable LiveData<SpannableString> displayBody) {
if (this.displayBody != null) {
this.displayBody.removeObserver(this);
@@ -349,19 +346,8 @@ public final class ConversationListItem extends RelativeLayout
if (thread.getSnippetUri() != null) {
this.thumbnailView.setVisibility(View.VISIBLE);
this.thumbnailView.setImageResource(glideRequests, thread.getSnippetUri());
LayoutParams subjectParams = (RelativeLayout.LayoutParams)this.subjectContainer .getLayoutParams();
subjectParams.addRule(RelativeLayout.LEFT_OF, R.id.thumbnail);
subjectParams.addRule(RelativeLayout.START_OF, R.id.thumbnail);
this.subjectContainer.setLayoutParams(subjectParams);
this.post(new ThumbnailPositioner(thumbnailView, archivedView, deliveryStatusIndicator, dateView));
} else {
this.thumbnailView.setVisibility(View.GONE);
LayoutParams subjectParams = (RelativeLayout.LayoutParams)this.subjectContainer.getLayoutParams();
subjectParams.addRule(RelativeLayout.LEFT_OF, R.id.status);
subjectParams.addRule(RelativeLayout.START_OF, R.id.status);
this.subjectContainer.setLayoutParams(subjectParams);
}
}
@@ -403,7 +389,7 @@ public final class ConversationListItem extends RelativeLayout
}
private void setRippleColor(Recipient recipient) {
if (VERSION.SDK_INT >= 21) {
if (Build.VERSION.SDK_INT >= 21) {
((RippleDrawable)(getBackground()).mutate())
.setColor(ColorStateList.valueOf(recipient.getColor().toConversationColor(getContext())));
}
@@ -421,6 +407,11 @@ public final class ConversationListItem extends RelativeLayout
@Override
public void onRecipientChanged(@NonNull Recipient recipient) {
if (this.recipient == null || !this.recipient.getId().equals(recipient.getId())) {
Log.w(TAG, "Bad change! Local recipient doesn't match. Ignoring. Local: " + (this.recipient == null ? "null" : this.recipient.getId()) + ", Changed: " + recipient.getId());
return;
}
fromView.setText(recipient, unreadCount == 0);
contactPhotoImage.setAvatar(glideRequests, recipient, !batchMode);
setRippleColor(recipient);
@@ -443,7 +434,8 @@ public final class ConversationListItem extends RelativeLayout
} else if (SmsDatabase.Types.isKeyExchangeType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ConversationListItem_key_exchange_message), defaultTint);
} else if (SmsDatabase.Types.isFailedDecryptType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_bad_encrypted_message), defaultTint);
UpdateDescription description = UpdateDescription.staticDescription(context.getString(R.string.ThreadRecord_chat_session_refreshed), R.drawable.ic_refresh_16);
return emphasisAdded(context, description, defaultTint);
} else if (SmsDatabase.Types.isNoRemoteSessionType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.MessageDisplayHelper_message_encrypted_for_non_existing_session), defaultTint);
} else if (SmsDatabase.Types.isEndSessionType(thread.getType())) {
@@ -549,37 +541,4 @@ public final class ConversationListItem extends RelativeLayout
updateTypingIndicator(typingThreads);
}
}
private static class ThumbnailPositioner implements Runnable {
private final View thumbnailView;
private final View archivedView;
private final View deliveryStatusView;
private final View dateView;
ThumbnailPositioner(View thumbnailView, View archivedView, View deliveryStatusView, View dateView) {
this.thumbnailView = thumbnailView;
this.archivedView = archivedView;
this.deliveryStatusView = deliveryStatusView;
this.dateView = dateView;
}
@Override
public void run() {
LayoutParams thumbnailParams = (RelativeLayout.LayoutParams)thumbnailView.getLayoutParams();
if (archivedView.getVisibility() == View.VISIBLE &&
(archivedView.getWidth() + deliveryStatusView.getWidth()) > dateView.getWidth())
{
thumbnailParams.addRule(RelativeLayout.LEFT_OF, R.id.status);
thumbnailParams.addRule(RelativeLayout.START_OF, R.id.status);
} else {
thumbnailParams.addRule(RelativeLayout.LEFT_OF, R.id.date);
thumbnailParams.addRule(RelativeLayout.START_OF, R.id.date);
}
thumbnailView.setLayoutParams(thumbnailParams);
}
}
}

View File

@@ -4,6 +4,7 @@ import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -13,12 +14,11 @@ import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Locale;
import java.util.Set;
public class ConversationListItemAction extends LinearLayout implements BindableConversationListItem {
public class ConversationListItemAction extends FrameLayout implements BindableConversationListItem {
private TextView description;
@@ -38,7 +38,7 @@ public class ConversationListItemAction extends LinearLayout implements Bindable
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.description = ViewUtil.findById(this, R.id.description);
this.description = findViewById(R.id.description);
}
@Override

View File

@@ -1,26 +1,22 @@
package org.thoughtcrime.securesms.conversationlist;
import android.app.Application;
import android.database.ContentObserver;
import android.os.Handler;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import androidx.paging.DataSource;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.conversationlist.model.Conversation;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.megaphone.Megaphone;
import org.thoughtcrime.securesms.megaphone.MegaphoneRepository;
@@ -28,80 +24,64 @@ import org.thoughtcrime.securesms.megaphone.Megaphones;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.paging.Invalidator;
import java.util.Objects;
import java.util.List;
class ConversationListViewModel extends ViewModel {
private static final String TAG = Log.tag(ConversationListViewModel.class);
private final Application application;
private final MutableLiveData<Megaphone> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final LiveData<ConversationList> conversationList;
private final SearchRepository searchRepository;
private final MegaphoneRepository megaphoneRepository;
private final Debouncer debouncer;
private final ContentObserver observer;
private final Invalidator invalidator;
private static boolean coldStart = true;
private final MutableLiveData<Megaphone> megaphone;
private final MutableLiveData<SearchResult> searchResult;
private final PagedData<Conversation> pagedData;
private final LiveData<Boolean> hasNoConversations;
private final SearchRepository searchRepository;
private final MegaphoneRepository megaphoneRepository;
private final Debouncer debouncer;
private final DatabaseObserver.Observer observer;
private final Invalidator invalidator;
private String lastQuery;
private int pinnedCount;
private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository, boolean isArchived) {
this.application = application;
this.megaphone = new MutableLiveData<>();
this.searchResult = new MutableLiveData<>();
this.searchRepository = searchRepository;
this.megaphoneRepository = ApplicationDependencies.getMegaphoneRepository();
this.debouncer = new Debouncer(300);
this.invalidator = new Invalidator();
this.observer = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
if (!TextUtils.isEmpty(getLastQuery())) {
searchRepository.query(getLastQuery(), searchResult::postValue);
}
this.pagedData = PagedData.create(ConversationListDataSource.create(application, isArchived),
new PagingConfig.Builder()
.setPageSize(15)
.setBufferPages(2)
.build());
this.observer = () -> {
if (!TextUtils.isEmpty(getLastQuery())) {
searchRepository.query(getLastQuery(), searchResult::postValue);
}
pagedData.getController().onDataInvalidated();
};
DataSource.Factory<Integer, Conversation> factory = new ConversationListDataSource.Factory(application, invalidator, isArchived);
PagedList.Config config = new PagedList.Config.Builder()
.setPageSize(15)
.setInitialLoadSizeHint(30)
.setEnablePlaceholders(true)
.build();
this.hasNoConversations = LiveDataUtil.mapAsync(pagedData.getData(), conversations -> {
pinnedCount = DatabaseFactory.getThreadDatabase(application).getPinnedConversationListCount();
LiveData<PagedList<Conversation>> conversationList = new LivePagedListBuilder<>(factory, config).setFetchExecutor(ConversationListDataSource.EXECUTOR)
.setInitialLoadKey(0)
.build();
application.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, observer);
this.conversationList = Transformations.switchMap(conversationList, conversation -> {
if (conversation.getDataSource().isInvalid()) {
Log.w(TAG, "Received an invalid conversation list. Ignoring.");
return new MutableLiveData<>();
}
MutableLiveData<ConversationList> updated = new MutableLiveData<>();
if (isArchived) {
updated.postValue(new ConversationList(conversation, 0, 0));
if (conversations.size() > 0) {
return false;
} else {
SignalExecutors.BOUNDED.execute(() -> {
int archiveCount = DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount();
int pinnedCount = DatabaseFactory.getThreadDatabase(application).getPinnedConversationListCount();
updated.postValue(new ConversationList(conversation, archiveCount, pinnedCount));
});
return DatabaseFactory.getThreadDatabase(application).getArchivedConversationListCount() == 0;
}
return updated;
});
ApplicationDependencies.getDatabaseObserver().registerConversationListObserver(observer);
}
public LiveData<Boolean> hasNoConversations() {
return Transformations.map(getConversationList(), ConversationList::isEmpty);
return hasNoConversations;
}
@NonNull LiveData<SearchResult> getSearchResult() {
@@ -112,16 +92,26 @@ class ConversationListViewModel extends ViewModel {
return megaphone;
}
@NonNull LiveData<ConversationList> getConversationList() {
return conversationList;
@NonNull LiveData<List<Conversation>> getConversationList() {
return pagedData.getData();
}
@NonNull PagingController getPagingController() {
return pagedData.getController();
}
public int getPinnedCount() {
return Objects.requireNonNull(getConversationList().getValue()).pinnedCount;
return pinnedCount;
}
void onVisible() {
megaphoneRepository.getNextMegaphone(megaphone::postValue);
if (!coldStart) {
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
}
coldStart = false;
}
void onMegaphoneCompleted(@NonNull Megaphones.Event event) {
@@ -157,7 +147,7 @@ class ConversationListViewModel extends ViewModel {
protected void onCleared() {
invalidator.invalidate();
debouncer.clear();
application.getContentResolver().unregisterContentObserver(observer);
ApplicationDependencies.getDatabaseObserver().unregisterObserver(observer);
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
@@ -174,32 +164,4 @@ class ConversationListViewModel extends ViewModel {
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository(), isArchived));
}
}
final static class ConversationList {
private final PagedList<Conversation> conversations;
private final int archivedCount;
private final int pinnedCount;
ConversationList(PagedList<Conversation> conversations, int archivedCount, int pinnedCount) {
this.conversations = conversations;
this.archivedCount = archivedCount;
this.pinnedCount = pinnedCount;
}
PagedList<Conversation> getConversations() {
return conversations;
}
int getArchivedCount() {
return archivedCount;
}
public int getPinnedCount() {
return pinnedCount;
}
boolean isEmpty() {
return conversations.isEmpty() && archivedCount == 0;
}
}
}

View File

@@ -11,18 +11,30 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.io.IOException;
import java.security.SecureRandom;
public class DatabaseSecretProvider {
/**
* It can be rather expensive to read from the keystore, so this class caches the key in memory
* after it is created.
*/
public final class DatabaseSecretProvider {
@SuppressWarnings("unused")
private static final String TAG = DatabaseSecretProvider.class.getSimpleName();
private static volatile DatabaseSecret instance;
private final Context context;
public static DatabaseSecret getOrCreateDatabaseSecret(@NonNull Context context) {
if (instance == null) {
synchronized (DatabaseSecretProvider.class) {
if (instance == null) {
instance = getOrCreate(context);
}
}
}
public DatabaseSecretProvider(@NonNull Context context) {
this.context = context.getApplicationContext();
return instance;
}
public DatabaseSecret getOrCreateDatabaseSecret() {
private DatabaseSecretProvider() {
}
private static @NonNull DatabaseSecret getOrCreate(@NonNull Context context) {
String unencryptedSecret = TextSecurePreferences.getDatabaseUnencryptedSecret(context);
String encryptedSecret = TextSecurePreferences.getDatabaseEncryptedSecret(context);
@@ -31,12 +43,12 @@ public class DatabaseSecretProvider {
else return createAndStoreDatabaseSecret(context);
}
private DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret)
private static @NonNull DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret)
{
try {
DatabaseSecret databaseSecret = new DatabaseSecret(unencryptedSecret);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT < 23) {
return databaseSecret;
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
@@ -51,8 +63,8 @@ public class DatabaseSecretProvider {
}
}
private DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
private static @NonNull DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) {
if (Build.VERSION.SDK_INT < 23) {
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
@@ -60,14 +72,14 @@ public class DatabaseSecretProvider {
}
}
private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
private static @NonNull DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32];
random.nextBytes(secret);
DatabaseSecret databaseSecret = new DatabaseSecret(secret);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= 23) {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
} else {

View File

@@ -82,10 +82,6 @@ public final class ProfileKeyUtil {
return Optional.of(profileKeyOrThrow(profileKey));
}
public static @NonNull Optional<ProfileKeyCredential> profileKeyCredentialOptional(@Nullable byte[] profileKey) {
return Optional.fromNullable(profileKeyCredentialOrNull(profileKey));
}
public static @NonNull ProfileKey createNew() {
try {
return new ProfileKey(Util.getSecretBytes(32));

View File

@@ -29,4 +29,7 @@ public class SessionUtil {
new TextSecureSessionStore(context).archiveAllSessions();
}
public static void archiveSession(Context context, RecipientId recipientId, int deviceId) {
new TextSecureSessionStore(context).archiveSession(recipientId, deviceId);
}
}

View File

@@ -103,6 +103,16 @@ public class TextSecureSessionStore implements SessionStore {
}
}
public void archiveSession(@NonNull RecipientId recipientId, int deviceId) {
synchronized (FILE_LOCK) {
SessionRecord session = DatabaseFactory.getSessionDatabase(context).load(recipientId, deviceId);
if (session != null) {
session.archiveCurrentState();
DatabaseFactory.getSessionDatabase(context).store(recipientId, deviceId, session);
}
}
}
public void archiveSiblingSessions(@NonNull SignalProtocolAddress address) {
synchronized (FILE_LOCK) {
if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {

View File

@@ -53,7 +53,6 @@ import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.FileUtils;
@@ -82,7 +81,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
@Trace
public class AttachmentDatabase extends Database {
private static final String TAG = AttachmentDatabase.class.getSimpleName();
@@ -751,7 +749,7 @@ public class AttachmentDatabase extends Database {
}
/**
* @param onlyModifyThisAttachment If false and more than one attachment shares this file, they will all up updated.
* @param onlyModifyThisAttachment If false and more than one attachment shares this file, they will all be updated.
* If true, then guarantees not to affect other attachments.
*/
public void updateAttachmentData(@NonNull DatabaseAttachment databaseAttachment,
@@ -1032,7 +1030,7 @@ public class AttachmentDatabase extends Database {
}
}
private File newFile() throws IOException {
public File newFile() throws IOException {
File partsDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE);
return File.createTempFile("part", ".mms", partsDirectory);
}

View File

@@ -23,6 +23,7 @@ import android.database.Cursor;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import java.util.Set;
@@ -39,20 +40,27 @@ public abstract class Database {
}
protected void notifyConversationListeners(Set<Long> threadIds) {
for (long threadId : threadIds)
ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadIds);
for (long threadId : threadIds) {
notifyConversationListeners(threadId);
}
}
protected void notifyConversationListeners(long threadId) {
ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId);
context.getContentResolver().notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadId), null);
notifyVerboseConversationListeners(threadId);
}
protected void notifyVerboseConversationListeners(long threadId) {
ApplicationDependencies.getDatabaseObserver().notifyVerboseConversationListeners(threadId);
context.getContentResolver().notifyChange(DatabaseContentProviders.Conversation.getVerboseUriForThread(threadId), null);
}
protected void notifyConversationListListeners() {
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners();
context.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
}
@@ -64,26 +72,32 @@ public abstract class Database {
context.getContentResolver().notifyChange(DatabaseContentProviders.StickerPack.CONTENT_URI, null);
}
@Deprecated
protected void setNotifyConversationListeners(Cursor cursor, long threadId) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getUriForThread(threadId));
}
@Deprecated
protected void setNotifyConversationListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getUriForAllThreads());
}
@Deprecated
protected void setNotifyVerboseConversationListeners(Cursor cursor, long threadId) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Conversation.getVerboseUriForThread(threadId));
}
@Deprecated
protected void setNotifyConversationListListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.ConversationList.CONTENT_URI);
}
@Deprecated
protected void setNotifyStickerListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.Sticker.CONTENT_URI);
}
@Deprecated
protected void setNotifyStickerPackListeners(Cursor cursor) {
cursor.setNotificationUri(context.getContentResolver(), DatabaseContentProviders.StickerPack.CONTENT_URI);
}
@@ -101,5 +115,4 @@ public abstract class Database {
public void reset(SQLCipherOpenHelper databaseHelper) {
this.databaseHelper = databaseHelper;
}
}

View File

@@ -32,13 +32,14 @@ import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class DatabaseFactory {
private static final Object lock = new Object();
private static DatabaseFactory instance;
private static volatile DatabaseFactory instance;
private final SQLCipherOpenHelper databaseHelper;
private final SmsDatabase sms;
@@ -58,21 +59,20 @@ public class DatabaseFactory {
private final SignedPreKeyDatabase signedPreKeyDatabase;
private final SessionDatabase sessionDatabase;
private final SearchDatabase searchDatabase;
private final JobDatabase jobDatabase;
private final StickerDatabase stickerDatabase;
private final StorageKeyDatabase storageKeyDatabase;
private final KeyValueDatabase keyValueDatabase;
private final MegaphoneDatabase megaphoneDatabase;
private final RemappedRecordsDatabase remappedRecordsDatabase;
private final MentionDatabase mentionDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
if (instance == null)
instance = new DatabaseFactory(context.getApplicationContext());
return instance;
if (instance == null) {
synchronized (lock) {
if (instance == null) {
instance = new DatabaseFactory(context.getApplicationContext());
}
}
}
return instance;
}
public static MmsSmsDatabase getMmsSmsDatabase(Context context) {
@@ -143,10 +143,6 @@ public class DatabaseFactory {
return getInstance(context).searchDatabase;
}
public static JobDatabase getJobDatabase(Context context) {
return getInstance(context).jobDatabase;
}
public static StickerDatabase getStickerDatabase(Context context) {
return getInstance(context).stickerDatabase;
}
@@ -155,14 +151,6 @@ public class DatabaseFactory {
return getInstance(context).storageKeyDatabase;
}
public static KeyValueDatabase getKeyValueDatabase(Context context) {
return getInstance(context).keyValueDatabase;
}
public static MegaphoneDatabase getMegaphoneDatabase(Context context) {
return getInstance(context).megaphoneDatabase;
}
static RemappedRecordsDatabase getRemappedRecordsDatabase(Context context) {
return getInstance(context).remappedRecordsDatabase;
}
@@ -180,6 +168,11 @@ public class DatabaseFactory {
getInstance(context).databaseHelper.onUpgrade(database, database.getVersion(), -1);
getInstance(context).databaseHelper.markCurrent(database);
getInstance(context).mms.trimEntriesForExpiredMessages();
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS key_value");
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS megaphone");
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS job_spec");
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS constraint_spec");
getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS dependency_spec");
instance.databaseHelper.close();
instance = null;
@@ -193,8 +186,8 @@ public class DatabaseFactory {
private DatabaseFactory(@NonNull Context context) {
SQLiteDatabase.loadLibs(context);
DatabaseSecret databaseSecret = new DatabaseSecretProvider(context).getOrCreateDatabaseSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
DatabaseSecret databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context);
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret);
this.sms = new SmsDatabase(context, databaseHelper);
@@ -214,11 +207,8 @@ public class DatabaseFactory {
this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper);
this.sessionDatabase = new SessionDatabase(context, databaseHelper);
this.searchDatabase = new SearchDatabase(context, databaseHelper);
this.jobDatabase = new JobDatabase(context, databaseHelper);
this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret);
this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper);
this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper);
this.megaphoneDatabase = new MegaphoneDatabase(context, databaseHelper);
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
}
@@ -250,4 +240,12 @@ public class DatabaseFactory {
public void triggerDatabaseAccess() {
databaseHelper.getWritableDatabase();
}
public SQLiteDatabase getRawDatabase() {
return databaseHelper.getWritableDatabase().getSqlCipherDatabase();
}
public boolean hasTable(String table) {
return SqlUtil.tableExists(databaseHelper.getReadableDatabase().getSqlCipherDatabase(), table);
}
}

View File

@@ -0,0 +1,142 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.database.ContentObserver;
import android.database.Cursor;
import androidx.annotation.NonNull;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SerialExecutor;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
/**
* Allows listening to database changes to varying degrees of specificity.
*
* A replacement for the observer system in {@link Database}. We should move to this over time.
*/
public final class DatabaseObserver {
private final Application application;
private final Executor executor;
private final Set<Observer> conversationListObservers;
private final Map<Long, Set<Observer>> conversationObservers;
private final Map<Long, Set<Observer>> verboseConversationObservers;
public DatabaseObserver(Application application) {
this.application = application;
this.executor = new SerialExecutor(SignalExecutors.BOUNDED);
this.conversationListObservers = new HashSet<>();
this.conversationObservers = new HashMap<>();
this.verboseConversationObservers = new HashMap<>();
}
public void registerConversationListObserver(@NonNull Observer listener) {
executor.execute(() -> {
conversationListObservers.add(listener);
});
}
public void registerConversationObserver(long threadId, @NonNull Observer listener) {
executor.execute(() -> {
registerMapped(conversationObservers, threadId, listener);
});
}
public void registerVerboseConversationObserver(long threadId, @NonNull Observer listener) {
executor.execute(() -> {
registerMapped(verboseConversationObservers, threadId, listener);
});
}
public void unregisterObserver(@NonNull Observer listener) {
executor.execute(() -> {
conversationListObservers.remove(listener);
unregisterMapped(conversationObservers, listener);
unregisterMapped(verboseConversationObservers, listener);
});
}
public void notifyConversationListeners(Set<Long> threadIds) {
executor.execute(() -> {
for (long threadId : threadIds) {
notifyMapped(conversationObservers, threadId);
notifyMapped(verboseConversationObservers, threadId);
}
});
for (long threadId : threadIds) {
application.getContentResolver().notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadId), null);
application.getContentResolver().notifyChange(DatabaseContentProviders.Conversation.getVerboseUriForThread(threadId), null);
}
}
public void notifyConversationListeners(long threadId) {
executor.execute(() -> {
notifyMapped(conversationObservers, threadId);
notifyMapped(verboseConversationObservers, threadId);
});
application.getContentResolver().notifyChange(DatabaseContentProviders.Conversation.getUriForThread(threadId), null);
application.getContentResolver().notifyChange(DatabaseContentProviders.Conversation.getVerboseUriForThread(threadId), null);
}
public void notifyVerboseConversationListeners(long threadId) {
executor.execute(() -> {
notifyMapped(verboseConversationObservers, threadId);
});
application.getContentResolver().notifyChange(DatabaseContentProviders.Conversation.getVerboseUriForThread(threadId), null);
}
public void notifyConversationListListeners() {
executor.execute(() -> {
for (Observer listener : conversationListObservers) {
listener.onChanged();
}
});
application.getContentResolver().notifyChange(DatabaseContentProviders.ConversationList.CONTENT_URI, null);
}
private <K> void registerMapped(@NonNull Map<K, Set<Observer>> map, @NonNull K key, @NonNull Observer listener) {
Set<Observer> listeners = map.get(key);
if (listeners == null) {
listeners = new HashSet<>();
}
listeners.add(listener);
map.put(key, listeners);
}
private <K> void unregisterMapped(@NonNull Map<K, Set<Observer>> map, @NonNull Observer listener) {
for (Map.Entry<K, Set<Observer>> entry : map.entrySet()) {
entry.getValue().remove(listener);
}
}
private static <K> void notifyMapped(@NonNull Map<K, Set<Observer>> map, @NonNull K key) {
Set<Observer> listeners = map.get(key);
if (listeners != null) {
for (Observer listener : listeners) {
listener.onChanged();
}
}
}
public interface Observer {
/**
* Called when the relevant data changes. Executed on a serial executor, so don't do any
* long-running tasks!
*/
void onChanged();
}
}

View File

@@ -10,13 +10,11 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
@Trace
public class DraftDatabase extends Database {
static final String TABLE_NAME = "drafts";

View File

@@ -15,8 +15,10 @@ import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.core.util.logging.Log;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
@@ -26,13 +28,13 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -47,29 +49,29 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@Trace
public final class GroupDatabase extends Database {
private static final String TAG = Log.tag(GroupDatabase.class);
static final String TABLE_NAME = "groups";
private static final String ID = "_id";
static final String GROUP_ID = "group_id";
static final String RECIPIENT_ID = "recipient_id";
private static final String TITLE = "title";
static final String MEMBERS = "members";
private static final String AVATAR_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key";
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type";
private static final String AVATAR_RELAY = "avatar_relay";
private static final String AVATAR_DIGEST = "avatar_digest";
private static final String TIMESTAMP = "timestamp";
static final String ACTIVE = "active";
static final String MMS = "mms";
private static final String EXPECTED_V2_ID = "expected_v2_id";
private static final String FORMER_V1_MEMBERS = "former_v1_members";
static final String TABLE_NAME = "groups";
private static final String ID = "_id";
static final String GROUP_ID = "group_id";
static final String RECIPIENT_ID = "recipient_id";
private static final String TITLE = "title";
static final String MEMBERS = "members";
private static final String AVATAR_ID = "avatar_id";
private static final String AVATAR_KEY = "avatar_key";
private static final String AVATAR_CONTENT_TYPE = "avatar_content_type";
private static final String AVATAR_RELAY = "avatar_relay";
private static final String AVATAR_DIGEST = "avatar_digest";
private static final String TIMESTAMP = "timestamp";
static final String ACTIVE = "active";
static final String MMS = "mms";
private static final String EXPECTED_V2_ID = "expected_v2_id";
private static final String UNMIGRATED_V1_MEMBERS = "former_v1_members";
/* V2 Group columns */
@@ -80,24 +82,24 @@ public final class GroupDatabase extends Database {
/** Serialized {@link DecryptedGroup} protobuf */
private static final String V2_DECRYPTED_GROUP = "decrypted_group";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
GROUP_ID + " TEXT, " +
RECIPIENT_ID + " INTEGER, " +
TITLE + " TEXT, " +
MEMBERS + " TEXT, " +
AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " +
AVATAR_CONTENT_TYPE + " TEXT, " +
AVATAR_RELAY + " TEXT, " +
TIMESTAMP + " INTEGER, " +
ACTIVE + " INTEGER DEFAULT 1, " +
AVATAR_DIGEST + " BLOB, " +
MMS + " INTEGER DEFAULT 0, " +
V2_MASTER_KEY + " BLOB, " +
V2_REVISION + " BLOB, " +
V2_DECRYPTED_GROUP + " BLOB, " +
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
FORMER_V1_MEMBERS + " TEXT DEFAULT NULL);";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY, " +
GROUP_ID + " TEXT, " +
RECIPIENT_ID + " INTEGER, " +
TITLE + " TEXT, " +
MEMBERS + " TEXT, " +
AVATAR_ID + " INTEGER, " +
AVATAR_KEY + " BLOB, " +
AVATAR_CONTENT_TYPE + " TEXT, " +
AVATAR_RELAY + " TEXT, " +
TIMESTAMP + " INTEGER, " +
ACTIVE + " INTEGER DEFAULT 1, " +
AVATAR_DIGEST + " BLOB, " +
MMS + " INTEGER DEFAULT 0, " +
V2_MASTER_KEY + " BLOB, " +
V2_REVISION + " BLOB, " +
V2_DECRYPTED_GROUP + " BLOB, " +
EXPECTED_V2_ID + " TEXT DEFAULT NULL, " +
UNMIGRATED_V1_MEMBERS + " TEXT DEFAULT NULL);";
public static final String[] CREATE_INDEXS = {
"CREATE UNIQUE INDEX IF NOT EXISTS group_id_index ON " + TABLE_NAME + " (" + GROUP_ID + ");",
@@ -106,7 +108,7 @@ public final class GroupDatabase extends Database {
};
private static final String[] GROUP_PROJECTION = {
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, FORMER_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
GROUP_ID, RECIPIENT_ID, TITLE, MEMBERS, UNMIGRATED_V1_MEMBERS, AVATAR_ID, AVATAR_KEY, AVATAR_CONTENT_TYPE, AVATAR_RELAY, AVATAR_DIGEST,
TIMESTAMP, ACTIVE, MMS, V2_MASTER_KEY, V2_REVISION, V2_DECRYPTED_GROUP
};
@@ -162,9 +164,23 @@ public final class GroupDatabase extends Database {
}
}
public void clearFormerV1Members(@NonNull GroupId.V2 id) {
/**
* Removes the specified members from the list of 'unmigrated V1 members' -- the list of members
* that were either dropped or had to be invited when migrating the group from V1->V2.
*/
public void removeUnmigratedV1Members(@NonNull GroupId.V2 id, @NonNull List<RecipientId> toRemove) {
Optional<GroupRecord> group = getGroup(id);
if (!group.isPresent()) {
Log.w(TAG, "Couldn't find the group!", new Throwable());
return;
}
List<RecipientId> newUnmigrated = group.get().getUnmigratedV1Members();
newUnmigrated.removeAll(toRemove);
ContentValues values = new ContentValues();
values.putNull(FORMER_V1_MEMBERS);
values.put(UNMIGRATED_V1_MEMBERS, newUnmigrated.isEmpty() ? null : RecipientId.toSerializedList(newUnmigrated));
databaseHelper.getWritableDatabase().update(TABLE_NAME, values, GROUP_ID + " = ?", SqlUtil.buildArgs(id));
@@ -505,16 +521,15 @@ public final class GroupDatabase extends Database {
contentValues.put(V2_MASTER_KEY, groupMasterKey.serialize());
contentValues.putNull(EXPECTED_V2_ID);
List<RecipientId> newMembers = Stream.of(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList())).map(u -> RecipientId.from(u, null)).toList();
List<RecipientId> pendingMembers = Stream.of(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList())).map(u -> RecipientId.from(u, null)).toList();
List<RecipientId> newMembers = uuidsToRecipientIds(DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList()));
List<RecipientId> pendingMembers = uuidsToRecipientIds(DecryptedGroupUtil.pendingToUuidList(decryptedGroup.getPendingMembersList()));
newMembers.addAll(pendingMembers);
List<RecipientId> droppedMembers = new ArrayList<>(SetUtil.difference(record.getMembers(), newMembers));
List<RecipientId> droppedMembers = new ArrayList<>(SetUtil.difference(record.getMembers(), newMembers));
List<RecipientId> unmigratedMembers = Util.concatenatedList(pendingMembers, droppedMembers);
if (droppedMembers.size() > 0) {
contentValues.put(FORMER_V1_MEMBERS, RecipientId.toSerializedList(record.getMembers()));
}
contentValues.put(UNMIGRATED_V1_MEMBERS, unmigratedMembers.isEmpty() ? null : RecipientId.toSerializedList(unmigratedMembers));
int updated = db.update(TABLE_NAME, contentValues, GROUP_ID + " = ?", SqlUtil.buildArgs(groupIdV1.toString()));
@@ -543,10 +558,31 @@ public final class GroupDatabase extends Database {
}
public void update(@NonNull GroupId.V2 groupId, @NonNull DecryptedGroup decryptedGroup) {
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
RecipientId groupRecipientId = recipientDatabase.getOrInsertFromGroupId(groupId);
String title = decryptedGroup.getTitle();
ContentValues contentValues = new ContentValues();
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
RecipientId groupRecipientId = recipientDatabase.getOrInsertFromGroupId(groupId);
Optional<GroupRecord> existingGroup = getGroup(groupId);
String title = decryptedGroup.getTitle();
ContentValues contentValues = new ContentValues();
if (existingGroup.isPresent() && existingGroup.get().getUnmigratedV1Members().size() > 0 && existingGroup.get().isV2Group()) {
Set<RecipientId> unmigratedV1Members = new HashSet<>(existingGroup.get().getUnmigratedV1Members());
DecryptedGroupChange change = GroupChangeReconstruct.reconstructGroupChange(existingGroup.get().requireV2GroupProperties().getDecryptedGroup(), decryptedGroup);
List<RecipientId> addedMembers = uuidsToRecipientIds(DecryptedGroupUtil.membersToUuidList(change.getNewMembersList()));
List<RecipientId> removedMembers = uuidsToRecipientIds(DecryptedGroupUtil.removedMembersUuidList(change));
List<RecipientId> addedInvites = uuidsToRecipientIds(DecryptedGroupUtil.pendingToUuidList(change.getNewPendingMembersList()));
List<RecipientId> removedInvites = uuidsToRecipientIds(DecryptedGroupUtil.removedPendingMembersUuidList(change));
List<RecipientId> acceptedInvites = uuidsToRecipientIds(DecryptedGroupUtil.membersToUuidList(change.getPromotePendingMembersList()));
unmigratedV1Members.removeAll(addedMembers);
unmigratedV1Members.removeAll(removedMembers);
unmigratedV1Members.removeAll(addedInvites);
unmigratedV1Members.removeAll(removedInvites);
unmigratedV1Members.removeAll(acceptedInvites);
contentValues.put(UNMIGRATED_V1_MEMBERS, unmigratedV1Members.isEmpty() ? null : RecipientId.toSerializedList(unmigratedV1Members));
}
contentValues.put(TITLE, title);
contentValues.put(V2_REVISION, decryptedGroup.getRevision());
@@ -688,16 +724,11 @@ public final class GroupDatabase extends Database {
}
}
@WorkerThread
public boolean isPendingMember(@NonNull GroupId.Push groupId, @NonNull Recipient recipient) {
return getGroup(groupId).transform(g -> g.isPendingMember(recipient)).or(false);
}
private static String serializeV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) {
List<RecipientId> groupMembers = new ArrayList<>(decryptedGroup.getMembersCount());
private static List<RecipientId> uuidsToRecipientIds(@NonNull List<UUID> uuids) {
List<RecipientId> groupMembers = new ArrayList<>(uuids.size());
for (DecryptedMember member : decryptedGroup.getMembersList()) {
UUID uuid = UuidUtil.fromByteString(member.getUuid());
for (UUID uuid : uuids) {
if (UuidUtil.UNKNOWN_UUID.equals(uuid)) {
Log.w(TAG, "Seen unknown UUID in members list");
} else {
@@ -707,7 +738,14 @@ public final class GroupDatabase extends Database {
Collections.sort(groupMembers);
return RecipientId.toSerializedList(groupMembers);
return groupMembers;
}
private static String serializeV2GroupMembers(@NonNull DecryptedGroup decryptedGroup) {
List<UUID> uuids = DecryptedGroupUtil.membersToUuidList(decryptedGroup.getMembersList());
List<RecipientId> recipientIds = uuidsToRecipientIds(uuids);
return RecipientId.toSerializedList(recipientIds);
}
public @NonNull List<GroupId.V2> getAllGroupV2Ids() {
@@ -772,7 +810,7 @@ public final class GroupDatabase extends Database {
RecipientId.from(CursorUtil.requireString(cursor, RECIPIENT_ID)),
CursorUtil.requireString(cursor, TITLE),
CursorUtil.requireString(cursor, MEMBERS),
CursorUtil.requireString(cursor, FORMER_V1_MEMBERS),
CursorUtil.requireString(cursor, UNMIGRATED_V1_MEMBERS),
CursorUtil.requireLong(cursor, AVATAR_ID),
CursorUtil.requireBlob(cursor, AVATAR_KEY),
CursorUtil.requireString(cursor, AVATAR_CONTENT_TYPE),
@@ -798,7 +836,7 @@ public final class GroupDatabase extends Database {
private final RecipientId recipientId;
private final String title;
private final List<RecipientId> members;
private final List<RecipientId> formerV1Members;
private final List<RecipientId> unmigratedV1Members;
private final long avatarId;
private final byte[] avatarKey;
private final byte[] avatarDigest;
@@ -812,7 +850,7 @@ public final class GroupDatabase extends Database {
@NonNull RecipientId recipientId,
String title,
String members,
String formerV1Members,
@Nullable String unmigratedV1Members,
long avatarId,
byte[] avatarKey,
String avatarContentType,
@@ -853,10 +891,10 @@ public final class GroupDatabase extends Database {
this.members = Collections.emptyList();
}
if (!TextUtils.isEmpty(formerV1Members)) {
this.formerV1Members = RecipientId.fromSerializedList(formerV1Members);
if (!TextUtils.isEmpty(unmigratedV1Members)) {
this.unmigratedV1Members = RecipientId.fromSerializedList(unmigratedV1Members);
} else {
this.formerV1Members = Collections.emptyList();
this.unmigratedV1Members = Collections.emptyList();
}
}
@@ -876,8 +914,9 @@ public final class GroupDatabase extends Database {
return members;
}
public @NonNull List<RecipientId> getFormerV1Members() {
return formerV1Members;
/** V1 members that were lost during the V1->V2 migration */
public @NonNull List<RecipientId> getUnmigratedV1Members() {
return unmigratedV1Members;
}
public boolean hasAvatar() {

View File

@@ -9,14 +9,12 @@ import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.whispersystems.libsignal.util.Pair;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
@Trace
public class GroupReceiptDatabase extends Database {
public static final String TABLE_NAME = "group_receipts";

View File

@@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.whispersystems.libsignal.IdentityKey;
@@ -39,7 +38,6 @@ import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
@Trace
public class IdentityDatabase extends Database {
@SuppressWarnings("unused")

View File

@@ -1,36 +1,37 @@
package org.thoughtcrime.securesms.database;
import android.app.Application;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import net.sqlcipher.database.SQLiteOpenHelper;
import net.sqlcipher.database.SQLiteDatabase;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.DatabaseSecret;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.jobmanager.persistence.ConstraintSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.DependencySpec;
import org.thoughtcrime.securesms.jobmanager.persistence.FullSpec;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.CursorUtil;
import java.util.LinkedList;
import java.util.List;
@Trace
public class JobDatabase extends Database {
public class JobDatabase extends SQLiteOpenHelper implements SignalDatabase {
public static String JOBS_TABLE_NAME = "job_spec";
public static String CONSTRAINTS_TABLE_NAME = "constraint_spec";
public static String DEPENDENCIES_TABLE_NAME = "dependency_spec";
private static final String TAG = Log.tag(JobDatabase.class);
public static final String[] CREATE_TABLE = new String[] { Jobs.CREATE_TABLE,
Constraints.CREATE_TABLE,
Dependencies.CREATE_TABLE };
private static final int DATABASE_VERSION = 1;
private static final String DATABASE_NAME = "signal-jobmanager.db";
private static final class Jobs {
private static final String TABLE_NAME = JOBS_TABLE_NAME;
private static final String TABLE_NAME = "job_spec";
private static final String ID = "_id";
private static final String JOB_SPEC_ID = "job_spec_id";
private static final String FACTORY_KEY = "factory_key";
@@ -40,7 +41,6 @@ public class JobDatabase extends Database {
private static final String RUN_ATTEMPT = "run_attempt";
private static final String MAX_ATTEMPTS = "max_attempts";
private static final String MAX_BACKOFF = "max_backoff";
private static final String MAX_INSTANCES = "max_instances";
private static final String LIFESPAN = "lifespan";
private static final String SERIALIZED_DATA = "serialized_data";
private static final String SERIALIZED_INPUT_DATA = "serialized_input_data";
@@ -55,7 +55,6 @@ public class JobDatabase extends Database {
RUN_ATTEMPT + " INTEGER, " +
MAX_ATTEMPTS + " INTEGER, " +
MAX_BACKOFF + " INTEGER, " +
MAX_INSTANCES + " INTEGER, " +
LIFESPAN + " INTEGER, " +
SERIALIZED_DATA + " TEXT, " +
SERIALIZED_INPUT_DATA + " TEXT DEFAULT NULL, " +
@@ -63,7 +62,7 @@ public class JobDatabase extends Database {
}
private static final class Constraints {
private static final String TABLE_NAME = CONSTRAINTS_TABLE_NAME;
private static final String TABLE_NAME = "constraint_spec";
private static final String ID = "_id";
private static final String JOB_SPEC_ID = "job_spec_id";
private static final String FACTORY_KEY = "factory_key";
@@ -75,7 +74,7 @@ public class JobDatabase extends Database {
}
private static final class Dependencies {
private static final String TABLE_NAME = DEPENDENCIES_TABLE_NAME;
private static final String TABLE_NAME = "dependency_spec";
private static final String ID = "_id";
private static final String JOB_SPEC_ID = "job_spec_id";
private static final String DEPENDS_ON_JOB_SPEC_ID = "depends_on_job_spec_id";
@@ -87,8 +86,65 @@ public class JobDatabase extends Database {
}
public JobDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
private static volatile JobDatabase instance;
private final Application application;
private final DatabaseSecret databaseSecret;
public static @NonNull JobDatabase getInstance(@NonNull Application context) {
if (instance == null) {
synchronized (JobDatabase.class) {
if (instance == null) {
instance = new JobDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context));
}
}
}
return instance;
}
public JobDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
super(application, DATABASE_NAME, null, DATABASE_VERSION, new SqlCipherDatabaseHook());
this.application = application;
this.databaseSecret = databaseSecret;
}
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, "onCreate()");
db.execSQL(Jobs.CREATE_TABLE);
db.execSQL(Constraints.CREATE_TABLE);
db.execSQL(Dependencies.CREATE_TABLE);
if (DatabaseFactory.getInstance(application).hasTable("job_spec")) {
Log.i(TAG, "Found old job_spec table. Migrating data.");
migrateJobSpecsFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db);
}
if (DatabaseFactory.getInstance(application).hasTable("constraint_spec")) {
Log.i(TAG, "Found old constraint_spec table. Migrating data.");
migrateConstraintSpecsFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db);
}
if (DatabaseFactory.getInstance(application).hasTable("dependency_spec")) {
Log.i(TAG, "Found old dependency_spec table. Migrating data.");
migrateDependencySpecsFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db);
}
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(TAG, "onUpgrade(" + oldVersion + ", " + newVersion + ")");
}
@Override
public void onOpen(SQLiteDatabase db) {
Log.i(TAG, "onOpen()");
dropTableIfPresent("job_spec");
dropTableIfPresent("constraint_spec");
dropTableIfPresent("dependency_spec");
}
public synchronized void insertJobs(@NonNull List<FullSpec> fullSpecs) {
@@ -96,7 +152,7 @@ public class JobDatabase extends Database {
return;
}
SQLiteDatabase db = databaseHelper.getWritableDatabase();
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
@@ -116,7 +172,7 @@ public class JobDatabase extends Database {
public synchronized @NonNull List<JobSpec> getAllJobSpecs() {
List<JobSpec> jobs = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Jobs.TABLE_NAME, null, null, null, null, null, Jobs.CREATE_TIME + ", " + Jobs.ID + " ASC")) {
try (Cursor cursor = getReadableDatabase().query(Jobs.TABLE_NAME, null, null, null, null, null, Jobs.CREATE_TIME + ", " + Jobs.ID + " ASC")) {
while (cursor != null && cursor.moveToNext()) {
jobs.add(jobSpecFromCursor(cursor));
}
@@ -132,7 +188,7 @@ public class JobDatabase extends Database {
String query = Jobs.JOB_SPEC_ID + " = ?";
String[] args = new String[]{ id };
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
}
public synchronized void updateJobAfterRetry(@NonNull String id, boolean isRunning, int runAttempt, long nextRunAttemptTime, @NonNull String serializedData) {
@@ -145,14 +201,14 @@ public class JobDatabase extends Database {
String query = Jobs.JOB_SPEC_ID + " = ?";
String[] args = new String[]{ id };
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, query, args);
}
public synchronized void updateAllJobsToBePending() {
ContentValues contentValues = new ContentValues();
contentValues.put(Jobs.IS_RUNNING, 0);
databaseHelper.getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, null, null);
getWritableDatabase().update(Jobs.TABLE_NAME, contentValues, null, null);
}
public synchronized void updateJobs(@NonNull List<JobSpec> jobs) {
@@ -160,7 +216,7 @@ public class JobDatabase extends Database {
return;
}
SQLiteDatabase db = databaseHelper.getWritableDatabase();
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
@@ -177,7 +233,6 @@ public class JobDatabase extends Database {
values.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
values.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
values.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
values.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
values.put(Jobs.LIFESPAN, job.getLifespan());
values.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
values.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
@@ -196,7 +251,7 @@ public class JobDatabase extends Database {
}
public synchronized void deleteJobs(@NonNull List<String> jobIds) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
SQLiteDatabase db = getWritableDatabase();
db.beginTransaction();
@@ -219,7 +274,7 @@ public class JobDatabase extends Database {
public synchronized @NonNull List<ConstraintSpec> getAllConstraintSpecs() {
List<ConstraintSpec> constraints = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Constraints.TABLE_NAME, null, null, null, null, null, null)) {
try (Cursor cursor = getReadableDatabase().query(Constraints.TABLE_NAME, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
constraints.add(constraintSpecFromCursor(cursor));
}
@@ -231,7 +286,7 @@ public class JobDatabase extends Database {
public synchronized @NonNull List<DependencySpec> getAllDependencySpecs() {
List<DependencySpec> dependencies = new LinkedList<>();
try (Cursor cursor = databaseHelper.getReadableDatabase().query(Dependencies.TABLE_NAME, null, null, null, null, null, null)) {
try (Cursor cursor = getReadableDatabase().query(Dependencies.TABLE_NAME, null, null, null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
dependencies.add(dependencySpecFromCursor(cursor));
}
@@ -254,7 +309,6 @@ public class JobDatabase extends Database {
contentValues.put(Jobs.RUN_ATTEMPT, job.getRunAttempt());
contentValues.put(Jobs.MAX_ATTEMPTS, job.getMaxAttempts());
contentValues.put(Jobs.MAX_BACKOFF, job.getMaxBackoff());
contentValues.put(Jobs.MAX_INSTANCES, job.getMaxInstances());
contentValues.put(Jobs.LIFESPAN, job.getLifespan());
contentValues.put(Jobs.SERIALIZED_DATA, job.getSerializedData());
contentValues.put(Jobs.SERIALIZED_INPUT_DATA, job.getSerializedInputData());
@@ -295,7 +349,6 @@ public class JobDatabase extends Database {
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_ATTEMPTS)),
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.MAX_BACKOFF)),
cursor.getLong(cursor.getColumnIndexOrThrow(Jobs.LIFESPAN)),
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.MAX_INSTANCES)),
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_DATA)),
cursor.getString(cursor.getColumnIndexOrThrow(Jobs.SERIALIZED_INPUT_DATA)),
cursor.getInt(cursor.getColumnIndexOrThrow(Jobs.IS_RUNNING)) == 1,
@@ -313,4 +366,73 @@ public class JobDatabase extends Database {
cursor.getString(cursor.getColumnIndexOrThrow(Dependencies.DEPENDS_ON_JOB_SPEC_ID)),
false);
}
private @NonNull SQLiteDatabase getReadableDatabase() {
return getWritableDatabase(databaseSecret.asString());
}
private @NonNull SQLiteDatabase getWritableDatabase() {
return getWritableDatabase(databaseSecret.asString());
}
@Override
public @NonNull SQLiteDatabase getSqlCipherDatabase() {
return getWritableDatabase();
}
private void dropTableIfPresent(@NonNull String table) {
if (DatabaseFactory.getInstance(application).hasTable(table)) {
Log.i(TAG, "Dropping original " + table + " table from the main database.");
DatabaseFactory.getInstance(application).getRawDatabase().rawExecSQL("DROP TABLE " + table);
}
}
private static void migrateJobSpecsFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
try (Cursor cursor = oldDb.rawQuery("SELECT * FROM job_spec", null)) {
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(Jobs.JOB_SPEC_ID, CursorUtil.requireString(cursor, "job_spec_id"));
values.put(Jobs.FACTORY_KEY, CursorUtil.requireString(cursor, "factory_key"));
values.put(Jobs.QUEUE_KEY, CursorUtil.requireString(cursor, "queue_key"));
values.put(Jobs.CREATE_TIME, CursorUtil.requireLong(cursor, "create_time"));
values.put(Jobs.NEXT_RUN_ATTEMPT_TIME, CursorUtil.requireLong(cursor, "next_run_attempt_time"));
values.put(Jobs.RUN_ATTEMPT, CursorUtil.requireInt(cursor, "run_attempt"));
values.put(Jobs.MAX_ATTEMPTS, CursorUtil.requireInt(cursor, "max_attempts"));
values.put(Jobs.MAX_BACKOFF, CursorUtil.requireLong(cursor, "max_backoff"));
values.put(Jobs.LIFESPAN, CursorUtil.requireLong(cursor, "lifespan"));
values.put(Jobs.SERIALIZED_DATA, CursorUtil.requireString(cursor, "serialized_data"));
values.put(Jobs.SERIALIZED_INPUT_DATA, CursorUtil.requireString(cursor, "serialized_input_data"));
values.put(Jobs.IS_RUNNING, CursorUtil.requireInt(cursor, "is_running"));
newDb.insert(Jobs.TABLE_NAME, null, values);
}
}
}
private static void migrateConstraintSpecsFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
try (Cursor cursor = oldDb.rawQuery("SELECT * FROM constraint_spec", null)) {
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(Constraints.JOB_SPEC_ID, CursorUtil.requireString(cursor, "job_spec_id"));
values.put(Constraints.FACTORY_KEY, CursorUtil.requireString(cursor, "factory_key"));
newDb.insert(Constraints.TABLE_NAME, null, values);
}
}
}
private static void migrateDependencySpecsFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) {
try (Cursor cursor = oldDb.rawQuery("SELECT * FROM dependency_spec", null)) {
while (cursor.moveToNext()) {
ContentValues values = new ContentValues();
values.put(Dependencies.JOB_SPEC_ID, CursorUtil.requireString(cursor, "job_spec_id"));
values.put(Dependencies.DEPENDS_ON_JOB_SPEC_ID, CursorUtil.requireString(cursor, "depends_on_job_spec_id"));
newDb.insert(Dependencies.TABLE_NAME, null, values);
}
}
}
}

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