Compare commits

...

397 Commits

Author SHA1 Message Date
Cody Henthorne
c45002d5b6 Bump version to 5.4.2 2021-02-11 16:19:28 -05:00
Cody Henthorne
3cd9f0ffef Updated language translations. 2021-02-11 16:18:44 -05:00
Greyson Parrelli
d065a6f563 Make max image dimensions a round number. 2021-02-11 13:59:02 -05:00
Greyson Parrelli
49d7a032fb Improve network reliability. 2021-02-11 13:53:48 -05:00
Greyson Parrelli
80f3504098 Move BlobProvider storage out of cache and into internal storage. 2021-02-11 13:26:12 -05:00
Greyson Parrelli
37d971859b Bump Lottie version to 3.6.0
Should hopefully fix an NPE we're seeing.
2021-02-11 12:35:16 -05:00
Greyson Parrelli
2a5bed1d21 Update chat wallpaper preview toolbar colors. 2021-02-11 11:59:47 -05:00
Cody Henthorne
0927914c57 Fix RTL padding issue for conversation items. 2021-02-11 11:47:37 -05:00
Greyson Parrelli
dda0e0393e Ensure a job has a context before we fail it. 2021-02-11 11:43:21 -05:00
Greyson Parrelli
cc9be7b61e Update long message activity toolbar color. 2021-02-11 11:37:50 -05:00
Greyson Parrelli
2751076089 Archive sessions on 409/410 instead of deleting them. 2021-02-11 10:43:04 -05:00
Greyson Parrelli
2a3f85008b Do not use View.getLayoutDirection().
This value doesn't populate until after the first layout pass. Instead,
it appears to be safer to just read it from the Configuration.
2021-02-11 10:41:40 -05:00
Alan Evans
432a732e7c Shorten message processor foreground service delay to resolve ANR. 2021-02-11 11:36:32 -04:00
Cody Henthorne
dc6045ca8b Fix crash when running shortcut update job on older APIs. 2021-02-11 10:36:02 -05:00
Greyson Parrelli
b58b0fd7a8 Fix boundaries of conversation banners in landscape. 2021-02-11 09:53:05 -05:00
Greyson Parrelli
5e122353e1 Fix landscape boundaries of conversation activity. 2021-02-11 00:58:49 -05:00
Greyson Parrelli
fc41fb5014 Bump version to 5.4.1 2021-02-11 00:09:33 -05:00
Greyson Parrelli
99477c8eef Updated language translations. 2021-02-11 00:05:58 -05:00
Greyson Parrelli
f50466f779 Fix NPE in AppForegroundObserver. 2021-02-11 00:05:58 -05:00
Greyson Parrelli
eb79300fe2 Lighten and unify toolbar shadows. 2021-02-10 22:05:09 -05:00
Greyson Parrelli
b35c96b0b6 Fix banner position issues. 2021-02-10 18:25:35 -05:00
Cody Henthorne
2282cd12d7 Bump version to 5.4.0 2021-02-10 15:30:47 -05:00
Cody Henthorne
0cbe992912 Updated language translations. 2021-02-10 15:26:29 -05:00
Alan Evans
98cb6b457c Periodic alarm to check for messages. 2021-02-10 15:21:04 -05:00
Greyson Parrelli
29d66f2b92 Remove unused signaling key code. 2021-02-10 15:21:04 -05:00
Greyson Parrelli
763a12dbc6 Ensure we only have one IncomingMessageObserver.
We saw a worrisome log that implied there may be a situation where
there's two IncomingMessageObservers. I can't see how that would happen,
but this is a failsafe to prevent that from happening.
2021-02-10 15:21:04 -05:00
Greyson Parrelli
158f3d898f Fail an enqueued job if its dependencies already failed.
This was a bug that was most notable during the attachment pre-upload
process: if an attachment failed to upload, the subsequently-enqueued
PushMediaSendJob would still send. This is because the attachment jobs
were enqueued first and failed *before* we enqueued the PushMediaSendJob
as a dependency.

This will use the JobTracker to determine if a dependency already failed
at the time of enqueueing a job like this. This isn't perfect, because
the JobTracker is memory-only and has a limited buffer (currently 1000),
but in practice this should be sufficient for our use cases. I imagine
it'd only fall apart if we somehow  enqueued a dependent job *much*
later, or somehow enqueued it based on a job ID that we persisted on
disk through an app restart. We don't do any of these things, currently,
and probably never should.

Also took the opportunity to patch a case where we weren't failing
dependent jobs when canceling a job, since I was giving the failure
stuff a look-over.
2021-02-10 15:21:03 -05:00
Cody Henthorne
b935999548 Fix timestamp and size for attachment saves. 2021-02-10 15:21:03 -05:00
Fumiaki Yoshimatsu
2cca6a5afb Check if the content uri already exists and rename the file until it's valid to insert.
Fixes #10159
2021-02-10 15:21:03 -05:00
Greyson Parrelli
2954c31b5f Fix issue where group sent transcripts were not put in proper processing queue. 2021-02-10 15:21:03 -05:00
Alan Evans
6c2d21125e Fix missing UUID crash, get group recipients direct from V2 group record. 2021-02-10 15:21:03 -05:00
Greyson Parrelli
59d69192c6 Disable conversation shortcuts when screen lock is enabled. 2021-02-10 15:21:03 -05:00
Greyson Parrelli
937a288cee Switch to a standard toolbar color.
* Switch to a standard toolbar color.

* T r a n s p a r e n c y

* Add back a toolbar shadow.

* Some more theming

* Also fix verify identity screen.

* Hide shadow on empty conversations.

* Slightly less transparent.
2021-02-10 15:21:03 -05:00
Greyson Parrelli
236e1ba885 Updated image compression parameters. 2021-02-10 09:16:42 -05:00
Greyson Parrelli
3bdf2e7e2c Add a system to improve app foreground observation.
There was previously a crash that occurred when multiple threads tried to use ProcessLifecycleOwner, and this will hopefully resolve that.
2021-02-08 15:37:45 -05:00
Fumiaki Yoshimatsu
a160af2d11 Adds some breathing space in the button caption.
Fix to a bug reported in the [beta forum](https://community.signalusers.org/t/beta-feedback-for-the-upcoming-android-5-3-release/25088/353)
2021-02-08 14:28:37 -05:00
AsamK
341a31da00 Fix crash from incoming call without contacts permission.
When the device is in Do Not Disturb mode and a call comes in from a
system contact, the app crashes if the user has revoked the contacts
permission.
The crash occurs because in Do Not Disturb mode Signal tries to check if
the contact is starred.
2021-02-08 14:28:19 -05:00
AsamK
e0128e7e31 Fix crash when changing contact color without contacts permission.
On mulit-device app, after the contact color is changed, a multi-device
contact update job is triggered, which tries to access the system
avatar.
This causes a crash if the user has revoked the contacts permission.
2021-02-08 14:27:49 -05:00
AsamK
8f51bdcb78 Adapt maxInstancesForQueue to only consider instances of the same job.
Currently the maxInstancesForQueue limit checks the count of all jobs in a
given queue. If there are already too many jobs, the new job is discarded.

However this is not the expected behavior for the two jobs where it's used:
GroupCallPeekWorkerJob and AutomaticSessionResetJob
For both the expected behavior is that there aren't too many jobs of them
started, but that there will be at least one instance of them started.
Both of them use the same queue as the PushProcessMessageJob and the MarkerJob.
Those two jobs are often in the queue at the same time, effectively preventing
the GroupCallPeekWorkerJob and AutomaticSessionResetJob from being enqueued.
2021-02-08 14:26:59 -05:00
Greyson Parrelli
53dc5bab43 Fix text overlapping on edit proxy screen. 2021-02-08 11:34:35 -05:00
Greyson Parrelli
8f86de1764 Add transparency to the compose bar when wallpaper is present. 2021-02-05 16:18:21 -05:00
Alex Hart
133a7d2576 Dismiss reactions when read on linked devices.
Also sends out read receipts for read reactions.

At present, only iPad is sending these -- desktop still needs to add send support.
2021-02-05 16:15:05 -05:00
Jack Lloyd
8b7506ed2d Bump libsignal-client to 0.2.3 2021-02-05 15:18:56 -05:00
Greyson Parrelli
c378e4413e Include git hash in debuglog. 2021-02-05 15:18:11 -05:00
Greyson Parrelli
f3182ddbc6 Fix success dialog in proxy edit screen. 2021-02-05 11:51:17 -05:00
Greyson Parrelli
951d4ad06f Prevent narrow race condition when resetting network components. 2021-02-05 11:40:58 -05:00
Alex Hart
2678a00781 Apply proper rotation to buttons and video in landscape. 2021-02-05 11:40:03 -05:00
Greyson Parrelli
e6e8786d86 Bump version to 5.3.12 2021-02-03 17:57:26 -05:00
Greyson Parrelli
1ec3782f64 Updated language translations. 2021-02-03 17:57:03 -05:00
Greyson Parrelli
a4ec31eebe Increase thread string length to 5 in logs. 2021-02-03 17:17:14 -05:00
Greyson Parrelli
94b631ccfe Add some description to the proxy settings screen. 2021-02-03 16:40:21 -05:00
Greyson Parrelli
26d8df5ea9 Change migration prompt from 'update' to 'upgrade'. 2021-02-03 16:04:01 -05:00
Greyson Parrelli
0569d0555f Improve proxy link parsing. 2021-02-03 16:04:01 -05:00
Alan Evans
3e2349c4ff Use matching color for read conversation sender names. 2021-02-03 16:04:01 -05:00
Greyson Parrelli
af7e736de9 Use a simple check to verify proxies during registration. 2021-02-03 16:04:01 -05:00
Greyson Parrelli
51879a9c46 Allow proxy deep links during registration. 2021-02-03 16:04:01 -05:00
Greyson Parrelli
524f3d6d08 Add support for sgnl:// proxy deep links. 2021-02-03 16:04:01 -05:00
Greyson Parrelli
64fe78ff9a Clean up several UX interactions with proxy entry. 2021-02-03 16:04:01 -05:00
Greyson Parrelli
e798f3f276 Add additional debug info for internal users. 2021-02-03 13:06:11 -05:00
Alan Evans
ddb04c6ea3 Do not linkify message body if recipient is not message request accepted.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2021-02-03 11:45:44 -05:00
Greyson Parrelli
213ffdab62 Bump version to 5.3.11 2021-02-02 20:35:14 -05:00
Greyson Parrelli
60354b2f1f Updated language translations. 2021-02-02 20:35:14 -05:00
AsamK
4bb214cb2a Configure keep alive duration for okhttp connection pool to 1 minute.
The signal http server supports http keep alive, but closes idle
connections after 1 minute.
The default OkHttp connection pool will keep idle connections in the pool
for 5 minutes and doesn't notice it when the server closes connections.
As currently the automatic okhttp retries are disabled, reusing such a
stale connection will be fatal.

Issue is especially severe for incoming calls, which fail because the request
to retrieve the turn servers fails and isn't retried: #10787
2021-02-02 20:35:14 -05:00
Greyson Parrelli
cfd4399685 Remove conversation update min width. 2021-02-02 20:32:52 -05:00
Greyson Parrelli
30563ed3e5 Allow using a proxy during registration. 2021-02-02 20:32:52 -05:00
Greyson Parrelli
46344776a4 Add UI support for configuring a proxy. 2021-02-02 16:42:47 -05:00
Cody Henthorne
0d215d609b Fix empty conversation update item text.
For some reason, if an EmojiTextView has a wrap content width and some other set of conditions occur, the view will not request a relayout when text changes.
This change inelegantly calls request layout more often to prevent that from happening.
2021-02-02 14:50:08 -05:00
Alan Evans
c15ea8c0b4 Skip automigration of nameless groups. 2021-02-02 15:30:20 -04:00
Alan Evans
d6061fb699 Fix migration of null titled group. 2021-02-02 15:19:06 -04:00
Moxie Marlinspike
7f2b6178d5 Add support for configuring a signal proxy. 2021-02-01 21:52:01 -05:00
Greyson Parrelli
53177bf40e Clean up unnecessary GCM stuff, improve FCM logging. 2021-02-01 20:56:25 -05:00
Greyson Parrelli
857b945410 Fix storage sync issue related to duplicate remote contacts.
The theory is that if multiple remote keys map to the *same* local
entry, then when we go to update the local contact the second time, we
won't find the entry by StorageID, because we changed it during the
*first*  update, which will then lead to a crash.

This change makes it so dupes are considered invalid, so we'll delete
them and upload our own local copy.
2021-02-01 18:06:33 -05:00
Alan Evans
904593c103 Add additional logging for conflict resolution. 2021-02-01 13:01:34 -04:00
Cody Henthorne
dcfa7e3b36 Allow contact support from registration lock and screen lock screens. 2021-02-01 11:58:33 -05:00
Alan Evans
589f345825 Fix unnecessary zeros padding. 2021-02-01 12:47:31 -04:00
Greyson Parrelli
0b7c22886d Fix issue where reaction shade is offset in chat bubbles.
Fixes #10843
2021-02-01 10:23:41 -05:00
Greyson Parrelli
e9e2846532 Force custom emojis for about views. 2021-02-01 09:44:12 -05:00
Greyson Parrelli
e0fc191883 Bump version to 5.3.10 2021-01-30 11:29:44 -05:00
Greyson Parrelli
b2ecd89a71 Updated language translations. 2021-01-30 11:29:44 -05:00
Greyson Parrelli
9ed95a6081 Revert "Fix wallpaper in landscape mode with notched devices."
This reverts commit 0b62bb8168.
2021-01-30 11:29:44 -05:00
Greyson Parrelli
3f51f89d86 Update libphonenumber to v8.12.17 2021-01-30 11:04:54 -05:00
Greyson Parrelli
01778f718a Bump version to 5.3.9 2021-01-29 18:35:44 -05:00
Greyson Parrelli
7d5ddd8eac Updated language translations. 2021-01-29 18:35:44 -05:00
Alan Evans
2447601219 Use stable ids on conversation list.
Fixes #10853
2021-01-29 18:35:44 -05:00
Greyson Parrelli
701e43c13d Do a normal message fetch in FcmReceiveService#onDeleteMessages() 2021-01-29 18:35:44 -05:00
Alex Hart
bbbccccf47 Fix crash when READ_PHONE_STATE is denied. 2021-01-29 18:22:32 -05:00
Alex Hart
1e9ca0a9bf Fix bad navigation for icon in ConversationActivity toolbar. 2021-01-29 18:22:32 -05:00
Alex Hart
0b62bb8168 Fix wallpaper in landscape mode with notched devices. 2021-01-29 18:22:32 -05:00
Greyson Parrelli
4f9f62992f Bump version to 5.3.8 2021-01-28 18:58:45 -05:00
Greyson Parrelli
1938d6cae0 Updated language translations. 2021-01-28 18:57:17 -05:00
Greyson Parrelli
13e8c55781 Delete duplicated internal preference. 2021-01-28 18:51:42 -05:00
Greyson Parrelli
6264f9b585 Have a much longer backoff maximum for 5xx errors. 2021-01-28 18:51:42 -05:00
Greyson Parrelli
4482bfcabb Ensure NonSuccessfulReponseCodeException knows the response code. 2021-01-28 18:51:42 -05:00
Alan Evans
015088a53f Fix registration issue where pin box is left disabled. 2021-01-28 18:51:42 -05:00
Alan Evans
ef7d707432 Fix wallpaper preview layout for longer text. 2021-01-28 18:44:04 -05:00
Alan Evans
d1f6a924fb Allow block of any recipient except MMS groups still. 2021-01-28 18:44:04 -05:00
Alan Evans
f312757daf Fix potential Base64 < 4 characters crash on group invite. 2021-01-28 18:44:04 -05:00
Greyson Parrelli
1d83729e6c Move backoff calculation into jobs. 2021-01-28 18:44:04 -05:00
Alan Evans
6a45858b4a Replace Firebase ML vision with built in face detection. 2021-01-28 18:44:04 -05:00
Alex Hart
1b448c2bdf Move reaction overlay UI into a stub. 2021-01-27 16:34:59 -04:00
Alan Evans
f6cd190245 Prevent warnings about multiple substitutions in non-positional format. 2021-01-27 12:32:10 -04:00
Alan Evans
23303e5407 Show name of message sender for groups in conversation list. 2021-01-27 11:53:31 -04:00
Alan Evans
b5237848e9 Restore pinned chats on archive undo. 2021-01-27 11:52:32 -04:00
Alan Evans
7cac0c9a7c UUID is now returned always. 2021-01-27 11:52:32 -04:00
Jim Gustafson
9dbbe4675f Update to RingRTC v2.9.0
Co-authored-by: Alex Hart <alex@signal.org>
2021-01-27 11:52:32 -04:00
Greyson Parrelli
95978f16e9 Possible fix to getting thrown to the bottom while reading unreads.
Shoutout to @fumiakiy for the excellent research here!

Sometimes we get thrown to the bottom of the list (or other list
locations) when reading content in the middle of the list. Most often,
this happens when you have a lot of unread messages and you open the
conversation.

FixedSizePagingController#onDataNeededAroundIndex() can be called very
fast in rapid succession, and we use the DataStatus class for
bookkeeping to know which requests are in-flight. We then make those
requests in LIFO order in order to make sure that the data visible on
screen now gets the highest priority.

...But in practice, that LIFO ordering can make things a little screwy.
Imagine we called onDataNeedAroundIndex() 50 times in rapid succession
(1, 2..., 50). Each time it's called, we generate a range and mark that
range as being fetched in DataStatus. That could mean that the latest
request for index 50 might only have, like, 1 item in it, because a
previously-enqueued fetch already got assigned most of it's data.

BUT we execute the nearly-empty request for index 50 first because of the
LIFO ordering. We give that data to RecyclerView first, and it doesn't like
that at all, and it jumps to weird places because we gave it mostly
null values, which are rendered as placeholder values (which are smaller
than real cells). So then, when we give it the real data right after,
its position is all off.

I switched to a serial executor. That prevents us from giving back weird
lists. The consequence is that if you scroll super fast, you run the
risk of the executor getting 'backed up' fetching data that's offscreen.
However, in practice, I couldn't trigger this. We'll see how it goes. I
think the true solution is a smarter way of fetching and ordering
requests, but that gets to be really tricky from a threading
perspective, and I'd rather keep things simple.
2021-01-27 11:52:32 -04:00
Alan Evans
d055bba452 Lint to prevent glide log usage. 2021-01-27 11:52:32 -04:00
Greyson Parrelli
8ef809a02b Only cluster updates of the same type together. 2021-01-27 11:52:32 -04:00
Alex Hart
458941f952 Enable dither on the gradient painter. 2021-01-27 11:52:32 -04:00
Greyson Parrelli
5852a508aa Bump version to 5.3.7 2021-01-27 10:17:53 -05:00
Greyson Parrelli
e2b4995fbb Updated language translations. 2021-01-27 10:17:20 -05:00
Greyson Parrelli
a3556d9f68 Ensure passphrases are disabled for all but the oldest users. 2021-01-27 10:10:26 -05:00
Greyson Parrelli
fe890a1a41 Added some additional logging around About. 2021-01-27 10:09:07 -05:00
Greyson Parrelli
c06bb18249 Fix navigation bar theming issue.
Fixes #10772
2021-01-27 09:09:56 -05:00
Greyson Parrelli
9099969b41 Bump version to 5.3.6 2021-01-25 18:14:14 -05:00
Greyson Parrelli
6358f59f67 Updated language translations. 2021-01-25 18:13:49 -05:00
Greyson Parrelli
073034dd3c Update logic on deciding whether to bulk animate stickers. 2021-01-25 13:57:24 -05:00
Alan Evans
17fb815805 Prevent duplicate member UUIDs in groups.
Fixes #10702
2021-01-25 13:06:15 -04:00
Alan Evans
409e7c41b4 Restore group update message "Loading" text. 2021-01-25 12:58:24 -04:00
Alan Evans
b9a1a5027c Fix rotation locked after voice record cancel and allow rotation when recording locked. 2021-01-25 12:46:49 -04:00
Alan Evans
49535f6378 Fix initial LiveData value for recipients. 2021-01-25 12:30:21 -04:00
Greyson Parrelli
c058452605 Bump version to 5.3.5 2021-01-24 17:50:40 -05:00
Greyson Parrelli
b3511dba77 Updated language translations. 2021-01-24 17:40:44 -05:00
Greyson Parrelli
afbe27c55f Revert "Bump libsignal-client to 0.2.2"
This reverts commit ce156c3450.
2021-01-24 17:40:44 -05:00
Greyson Parrelli
41d227207d Fix author title on remote deleted group messages. 2021-01-24 17:40:44 -05:00
Greyson Parrelli
92b586c061 Disable mass APNG animation on low-memory devices. 2021-01-24 17:40:44 -05:00
Greyson Parrelli
acbc17c909 Bump version to 5.3.4 2021-01-24 03:35:56 -05:00
Greyson Parrelli
15f17747ee Updated language translations. 2021-01-24 03:35:56 -05:00
Greyson Parrelli
781054fc9d Switch dark theme bubbles with wallpaper to grey_95 instead of black. 2021-01-24 03:35:56 -05:00
Greyson Parrelli
b59769a30a Do not allow saving pending media. 2021-01-24 03:10:03 -05:00
Greyson Parrelli
26e0e09e24 Update padding and margins on conversation updates. 2021-01-24 03:07:49 -05:00
Greyson Parrelli
3a2990a911 Fix crash when sharing stickers you don't have installed. 2021-01-24 02:33:24 -05:00
Greyson Parrelli
d8060b3041 Fix inset issues in landscape. 2021-01-24 02:22:09 -05:00
Greyson Parrelli
f42ec5318f Bump version to 5.3.3 2021-01-23 18:58:48 -05:00
Greyson Parrelli
bd0d425cbf Updated language translations. 2021-01-23 18:58:48 -05:00
Greyson Parrelli
b3d5d7c33e Move cursor to end of text field after select About preset. 2021-01-23 18:58:48 -05:00
Greyson Parrelli
1746869dc3 Fix issue with rendering of group update timestamps.
TIL SimpleDateFormat is not thread safe.
Across instances.

God forgive them, for they know not what they did.
2021-01-23 18:48:14 -05:00
Greyson Parrelli
633f4cbbe5 Disable 'loading' update message. 2021-01-23 18:48:14 -05:00
Greyson Parrelli
0944e2f758 Apply contact list SMS filter to 'recents' section. 2021-01-23 18:48:14 -05:00
Alex Hart
b49e4004ab Restrict SMS in multishare. 2021-01-23 18:48:14 -05:00
Greyson Parrelli
68381f8b64 Fix text color of recent conversations in share activity. 2021-01-23 18:48:14 -05:00
Greyson Parrelli
f180066058 Disallow link previews in multi-forward when sending to SMS. 2021-01-23 15:31:48 -05:00
Greyson Parrelli
6b7de2e85e Make voice note play button visible in wallpaper mode. 2021-01-23 15:20:58 -05:00
Greyson Parrelli
c650a978e9 Update styling of last seen divider. 2021-01-23 15:02:11 -05:00
Greyson Parrelli
e05cadafe6 Collapse adjacent conversation updates. 2021-01-23 14:55:19 -05:00
Greyson Parrelli
c6008a4f90 Disable forwarding of pending media. 2021-01-23 13:39:13 -05:00
Greyson Parrelli
5624855eba Make ManageProfileActivity work with screen lock. 2021-01-23 13:27:37 -05:00
Greyson Parrelli
799ff86fc0 Fixed tinting of wallpaper bubble previews. 2021-01-23 13:18:53 -05:00
Greyson Parrelli
798fc84e82 Fix issue where empty about could be rendered in contact list. 2021-01-23 12:56:00 -05:00
Greyson Parrelli
cc363a3c88 Fix wallpaper sizing issues in landscape. 2021-01-23 12:41:29 -05:00
Greyson Parrelli
6fdaef1f58 Bump version to 5.3.2 2021-01-22 23:44:10 -05:00
Greyson Parrelli
6db6c93295 Updated language translations. 2021-01-22 23:43:45 -05:00
Greyson Parrelli
4fb0f30d32 Add bubbles around additional elements in wallpaper mode. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
7fa4eb079b Improve responsiveness of conversation update rendering. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
c5392b8844 Fix highlighted bounds of conversation items. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
e460973957 Various wallpaper UX fixes. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
e1c6311a18 Fix bug where dates weren't rendered properly after wallpaper change. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
ed11e2f05a Center-align About text in recipient settings. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
3182e5af88 Fix spacing bug when About is set to only text or only emoji. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
5cfdf626fe Reduce the font size of all update messages. 2021-01-22 23:36:42 -05:00
Alex Hart
e55834d523 Prevent NPE in PhoneNumberFormatter. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
9d5a52a980 Made setting a profile photo a synchronous operation. 2021-01-22 23:36:42 -05:00
Alan Evans
5649c906a5 Do not bump group threads on leave. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
ee548d27e5 Made setting profile name and About synchronous operations. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
1dc737b5e5 Increase max About glyphs to 140. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
427869d4ca Various improvements to About UI/UX. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
36395ced89 Add About presets. 2021-01-22 23:36:42 -05:00
Greyson Parrelli
52a9f2c893 Add the ability to clear about and emoji. 2021-01-22 10:29:52 -05:00
Greyson Parrelli
bd88be2513 Auto-open keyboard on About screen. 2021-01-22 10:17:06 -05:00
Alan Evans
7107c1d6b2 Add Taqbaylit language support. 2021-01-22 10:59:42 -04:00
Alan Evans
51f4a343c9 Add Odia ଓଡ଼ିଆ language support. 2021-01-22 10:54:21 -04:00
Alex Hart
a12ee1b78b Update copy for reset all wallpapers dialog. 2021-01-22 09:42:24 -04:00
Alex Hart
4bf59a55da Distinguish clear copy between global and single recipient wallpapers. 2021-01-22 09:40:05 -04:00
Alex Hart
dbac9bf9f6 Enable 'dim in dark theme' by default. 2021-01-22 09:13:12 -04:00
Alex Hart
b95083fe92 Fix sticker support in multishare. 2021-01-22 09:01:38 -04:00
Alan Evans
e2d297eb8a Skip native LibSignal tests on unsupported and non-unix OS. 2021-01-22 00:34:41 -04:00
Greyson Parrelli
a3176bbb67 Bump version to 5.3.1 2021-01-21 22:52:31 -05:00
Greyson Parrelli
4203dde151 Updated language translations. 2021-01-21 22:52:08 -05:00
Greyson Parrelli
880661710f Fix possible NPE in group list item. 2021-01-21 22:47:30 -05:00
Greyson Parrelli
d844fa0fb5 Fix possible NPE on wallpaper change. 2021-01-21 22:47:30 -05:00
Greyson Parrelli
18ede2e900 Bump version to 5.3.0 2021-01-21 18:29:56 -05:00
Greyson Parrelli
06cc96bee7 Updated language translations. 2021-01-21 18:29:15 -05:00
Greyson Parrelli
43a12d2a81 Refine incognito keyboard setting string. 2021-01-21 18:22:05 -05:00
Michael Crenshaw
0a29ffcf4c Update incognito keyboard copy. 2021-01-21 18:17:35 -05:00
Thore Goebel
9c88532c21 Add ripple to review storage button. 2021-01-21 18:04:56 -05:00
Martin d'Allens
f3450b8f10 Center the terms link on the welcome fragment for long translations. 2021-01-21 18:02:20 -05:00
Alan Evans
a4d56e376f Allow clicking on typer avatar to bring up their bottom sheet details. 2021-01-21 18:02:20 -05:00
ascendingSun
24b5bac589 Fix camera crash when mic permission is granted.
Fixes #10642
2021-01-21 18:02:19 -05:00
Greyson Parrelli
762f17f1c1 Install a new animated sticker pack. 2021-01-21 18:02:19 -05:00
Greyson Parrelli
105c8c9745 Fix issue where forwarded link previews weren't marked uploaded. 2021-01-21 18:02:19 -05:00
Alan Evans
93d99287eb Wallpaper preview size respects device aspect ratio. 2021-01-21 18:02:19 -05:00
Jack Lloyd
ce156c3450 Bump libsignal-client to 0.2.2 2021-01-21 18:02:19 -05:00
Greyson Parrelli
7db16e6156 Add support for an 'About' field on your profile. 2021-01-21 18:02:19 -05:00
Alex Hart
e80033c287 Fix several issues with local expandable pip. 2021-01-21 18:02:19 -05:00
Alex Hart
1553f9b75d Upgrade libphonenumber to v8.12.16 2021-01-21 18:02:19 -05:00
Alex Hart
c244a98962 Finalize wallpaper UX.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
Co-authored-by: Alan Evans <alan@signal.org>
2021-01-21 18:02:19 -05:00
Alan Evans
a8ad1e718e Wallpaper image selection and cropping. 2021-01-21 18:02:19 -05:00
Alex Hart
b5712f4bd1 Improve wallpaper settings screen, conversation rendering.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2021-01-21 18:02:19 -05:00
Greyson Parrelli
6bcb0de43d Add support for persisting wallpaper selection. 2021-01-21 18:02:19 -05:00
Alex Hart
80651d2425 Initial wallpaper settings screens. 2021-01-21 18:02:19 -05:00
Martin d'Allens
46492b8238 Remove ~200 unused English strings. 2021-01-21 18:02:19 -05:00
Alan Evans
1be561543c Fix API19 drawable. 2021-01-21 18:02:19 -05:00
PockelHockel
e430a46e20 Prevent screen rotation during voice record.
Fixes #8276
2021-01-21 18:02:19 -05:00
Alex Hart
8d187c8ba1 Add the ability to forward content to multiple chats at once. 2021-01-21 18:02:19 -05:00
Sicco van Sas
eacf03768f Create FUNDING.yml
This will add a 'sponsor' button at the top of the repository which links to Signals donate page https://signal.org/donate/
2021-01-19 17:09:42 -04:00
Alan Evans
b077c9b4f3 Only schedule one job per constraint set. 2021-01-19 17:07:32 -04:00
Alan Evans
893749fcab Prevent stripping of leading zeros in national numbers. 2021-01-19 17:06:49 -04:00
Alan Evans
848ead5e78 Include an English filter line in the support email body. 2021-01-19 17:06:23 -04:00
Jim Gustafson
9c47acb004 Update to RingRTC v2.8.10 2021-01-19 16:03:37 -05:00
Greyson Parrelli
8ca54bcc7b Create a new manage profile screen. 2021-01-19 10:39:49 -05:00
Greyson Parrelli
7e64d57ba8 Bump version to 5.2.3 2021-01-17 00:17:47 -05:00
Greyson Parrelli
a517fc4e15 Fix NPE in RecipientDatabase. 2021-01-17 00:17:26 -05:00
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
895 changed files with 57758 additions and 25750 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: https://signal.org/donate/

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 = 753
def canonicalVersionName = "5.0.2"
def canonicalVersionCode = 788
def canonicalVersionName = "5.4.2"
def postFixSize = 10
def postFixSize = 100
def abiPostFix = ['universal' : 0,
'armeabi-v7a' : 1,
'arm64-v8a' : 2,
@@ -108,6 +108,7 @@ android {
project.ext.set("archivesBaseName", "Signal");
buildConfigField "long", "BUILD_TIMESTAMP", getLastCommitTimestamp() + "L"
buildConfigField "String", "GIT_HASH", "\"${getGitHash()}\""
buildConfigField "String", "SIGNAL_URL", "\"https://textsecure-service.whispersystems.org\""
buildConfigField "String", "STORAGE_URL", "\"https://storage.signal.org\""
buildConfigField "String", "SIGNAL_CDN_URL", "\"https://cdn.signal.org\""
@@ -128,7 +129,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'
@@ -163,10 +163,6 @@ android {
exclude 'META-INF/proguard/androidx-annotations.pro'
}
aaptOptions {
ignoreAssetsPattern '!contours.tfl:!LMprec_600.emd:!blazeface.tfl'
}
buildTypes {
debug {
if (keystores['debug'] != null) {
@@ -206,6 +202,12 @@ android {
minifyEnabled true
proguardFiles = buildTypes.debug.proguardFiles
}
perf {
initWith debug
isDefault false
debuggable false
matchingFallbacks = ['debug']
}
}
productFlavors {
@@ -229,7 +231,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,10 +312,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'
implementation ('com.google.firebase:firebase-messaging:20.2.0') {
exclude group: 'com.google.firebase', module: 'firebase-core'
@@ -335,12 +332,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 'org.whispersystems:signal-client-android:0.2.3'
implementation 'com.google.protobuf:protobuf-javalite:3.10.0'
implementation 'org.signal:argon2:13.1@aar'
implementation 'org.signal:ringrtc-android:2.8.7'
implementation 'org.signal:ringrtc-android:2.9.2'
implementation "me.leolin:ShortcutBadger:1.1.16"
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
@@ -378,7 +377,7 @@ dependencies {
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
implementation 'com.airbnb.android:lottie:3.0.7'
implementation 'com.airbnb.android:lottie:3.6.0'
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
@@ -488,6 +487,15 @@ def getLastCommitTimestamp() {
}
}
def getGitHash() {
def stdout = new ByteArrayOutputStream()
exec {
commandLine 'git', 'rev-parse', '--short', 'HEAD'
standardOutput = stdout
}
return stdout.toString().trim()
}
tasks.withType(Test) {
testLogging {
events "failed"

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

@@ -64,7 +64,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<!-- So we can add a TextSecure 'Account' -->
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
@@ -159,12 +158,15 @@
<activity android:name=".preferences.MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".sharing.interstitial.ShareInterstitialActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".sharing.ShareActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:excludeFromRecents="true"
android:launchMode="singleTask"
android:taskAffinity=""
android:noHistory="true"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
@@ -225,6 +227,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" />
@@ -242,12 +255,16 @@
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>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https"
android:host="signal.tube" />
<data android:scheme="sgnl"
android:host="signal.tube" />
</intent-filter>
</activity>
<activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
@@ -342,13 +359,24 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".ApplicationPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.NOTIFICATION_PREFERENCES" />
</intent-filter>
</activity>
<activity android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".wallpaper.ChatWallpaperPreviewActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="stateAlwaysHidden">
</activity>
<activity android:name=".registration.RegistrationNavigationActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightRegistrationTheme"
@@ -455,6 +483,10 @@
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".profiles.manage.ManageProfileActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity android:name=".lock.v2.CreateKbsPinActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:windowSoftInputMode="adjustResize"
@@ -465,17 +497,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 +536,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"
@@ -537,6 +562,15 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask" />
<activity android:name=".wallpaper.crop.WallpaperImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.FullScreenMedia" />
<activity android:name=".wallpaper.crop.WallpaperCropActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:screenOrientation="portrait"
android:theme="@style/Theme.Signal.WallpaperCropper" />
<service android:enabled="true" android:name=".service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
@@ -737,6 +771,13 @@
</intent-filter>
</receiver>
<receiver android:name=".messageprocessingalarm.MessageProcessReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="org.thoughtcrime.securesms.action.PROCESS_MESSAGES" />
</intent-filter>
</receiver>
<receiver android:name=".service.LocalBackupListener">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />

View File

@@ -24,7 +24,7 @@ public final class Log {
}
public static void e(@NonNull String tag, @NonNull String message) {
e(tag, message, null);
SignalGlideCodecs.getLogProvider().e(tag, message, null);
}
public static void e(@NonNull String tag, @NonNull String message, @Nullable Throwable throwable) {

View File

@@ -12,7 +12,7 @@ import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import org.signal.glide.Log;
import org.signal.core.util.logging.Log;
import org.signal.glide.apng.io.APNGReader;
import org.signal.glide.apng.io.APNGWriter;
import org.signal.glide.common.decode.Frame;

View File

@@ -21,7 +21,7 @@ import android.os.Message;
import androidx.annotation.NonNull;
import androidx.vectordrawable.graphics.drawable.Animatable2Compat;
import org.signal.glide.Log;
import org.signal.core.util.logging.Log;
import org.signal.glide.common.decode.FrameSeqDecoder;
import org.signal.glide.common.loader.Loader;

View File

@@ -15,7 +15,7 @@ import android.os.Looper;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import org.signal.glide.Log;
import org.signal.core.util.logging.Log;
import org.signal.glide.common.executor.FrameDecoderExecutor;
import org.signal.glide.common.io.Reader;
import org.signal.glide.common.io.Writer;

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

@@ -43,6 +43,7 @@ public final class AppInitialization {
SignalStore.onFirstEverAppLaunch();
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
@@ -52,8 +53,32 @@ public final class AppInitialization {
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.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
/**
* 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));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}

View File

@@ -16,17 +16,13 @@
*/
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.AsyncTask;
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;
import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.multidex.MultiDexApplication;
import com.google.android.gms.security.ProviderInstaller;
@@ -37,6 +33,7 @@ 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;
@@ -54,6 +51,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.LogSecretProvider;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.providers.BlobProvider;
@@ -69,12 +67,14 @@ import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.tracing.Trace;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.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,8 +93,7 @@ import java.util.concurrent.TimeUnit;
*
* @author Moxie Marlinspike
*/
@Trace
public class ApplicationContext extends MultiDexApplication implements DefaultLifecycleObserver {
public class ApplicationContext extends MultiDexApplication implements AppForegroundObserver.Listener {
private static final String TAG = ApplicationContext.class.getSimpleName();
@@ -102,79 +101,105 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
private ViewOnceMessageManager viewOnceMessageManager;
private PersistentLogger persistentLogger;
private volatile boolean isAppVisible;
public static ApplicationContext getInstance(Context context) {
return (ApplicationContext)context.getApplicationContext();
}
@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", () -> ApplicationDependencies.getAppForegroundObserver().addListener(this))
.addBlocking("message-retriever", this::initializeMessageRetrieval)
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
.addBlocking("vector-compat", () -> {
if (Build.VERSION.SDK_INT < 21) {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
})
.addBlocking("proxy-init", () -> {
if (SignalStore.proxy().isProxyEnabled()) {
Log.w(TAG, "Proxy detected. Enabling Conscrypt.setUseEngineSocketByDefault()");
Conscrypt.setUseEngineSocketByDefault(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();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
Tracer.getInstance().end("Application#onCreate()");
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
isAppVisible = true;
public void onForeground() {
long startTime = System.currentTimeMillis();
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
public void onStop(@NonNull LifecycleOwner owner) {
isAppVisible = false;
public void onBackground() {
Log.i(TAG, "App is no longer visible.");
KeyCachingService.onAppBackgrounded(this);
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
ApplicationDependencies.getFrameRateTracker().end();
ApplicationDependencies.getShakeToReport().disable();
}
public ExpiringMessageManager getExpiringMessageManager() {
if (expiringMessageManager == null) {
initializeExpiringMessageManager();
}
return expiringMessageManager;
}
@@ -182,10 +207,6 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
return viewOnceMessageManager;
}
public boolean isAppVisible() {
return isAppVisible;
}
public PersistentLogger getPersistentLogger() {
return persistentLogger;
}
@@ -242,18 +263,24 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
}
private void initializeAppDependencies() {
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this, new SignalServiceNetworkAccess(this)));
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this));
}
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);
} else if (!TextSecurePreferences.isPasswordDisabled(this) && VersionTracker.getDaysSinceFirstInstalled(this) < 912) {
Log.i(TAG, "Detected a not-recent install that doesn't have passphrases disabled -- disabling now.");
TextSecurePreferences.setPasswordDisabled(this, true);
}
}
@@ -286,6 +313,7 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
DirectoryRefreshListener.schedule(this);
LocalBackupListener.schedule(this);
RotateSenderCertificateListener.schedule(this);
MessageProcessReceiver.startOrUpdateAlarm(this);
if (BuildConfig.PLAY_STORE_DISABLED) {
UpdateApkRefreshListener.schedule(this);
@@ -328,23 +356,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() {
@@ -359,23 +379,21 @@ public class ApplicationContext extends MultiDexApplication implements DefaultLi
if (Build.VERSION.SDK_INT >= 26) {
FcmJobService.schedule(this);
} else {
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob(this));
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
}
TextSecurePreferences.setNeedsMessagePull(this, false);
}
}
@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,12 +39,14 @@ import org.thoughtcrime.securesms.preferences.AppearancePreferenceFragment;
import org.thoughtcrime.securesms.preferences.BackupsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
import org.thoughtcrime.securesms.preferences.DataAndStoragePreferenceFragment;
import org.thoughtcrime.securesms.preferences.EditProxyFragment;
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
import org.thoughtcrime.securesms.preferences.widgets.UsernamePreference;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.profiles.manage.ManageProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.CachedInflater;
@@ -65,6 +67,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
implements SharedPreferences.OnSharedPreferenceChangeListener
{
public static final String LAUNCH_TO_BACKUPS_FRAGMENT = "launch.to.backups.fragment";
public static final String LAUNCH_TO_HELP_FRAGMENT = "launch.to.help.fragment";
public static final String LAUNCH_TO_PROXY_FRAGMENT = "launch.to.proxy.fragment";
@SuppressWarnings("unused")
private static final String TAG = ApplicationPreferencesActivity.class.getSimpleName();
@@ -104,6 +108,10 @@ 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 (getIntent() != null && getIntent().getBooleanExtra(LAUNCH_TO_PROXY_FRAGMENT, false)) {
initFragment(android.R.id.content, EditProxyFragment.newInstance());
} else if (icicle == null) {
initFragment(android.R.id.content, new ApplicationPreferenceFragment());
} else {
@@ -309,7 +317,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);
@@ -342,7 +350,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
private class ProfileClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
requireActivity().startActivity(EditProfileActivity.getIntentForUserProfileEdit(preference.getContext()));
requireActivity().startActivity(ManageProfileActivity.getIntent(requireActivity()));
return true;
}
}
@@ -350,7 +358,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
private class UsernameClickListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
requireActivity().startActivity(EditProfileActivity.getIntentForUsernameEdit(preference.getContext()));
requireActivity().startActivity(ManageProfileActivity.getIntentForUsernameEdit(preference.getContext()));
return true;
}
}

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

@@ -36,7 +36,9 @@ public interface BindableConversationItem extends Unbindable {
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient recipients,
@Nullable String searchQuery,
boolean pulseMention);
boolean pulseMention,
boolean hasWallpaper,
boolean isMessageRequestAccepted);
ConversationMessage getConversationMessage();
@@ -61,7 +63,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

@@ -99,7 +99,6 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
private void initializeResources() {
contactsFragment = (ContactSelectionListFragment) getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
contactsFragment.setOnContactSelectedListener(this);
contactsFragment.setOnRefreshListener(this);
}

View File

@@ -76,6 +76,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.adapter.FixedViewsAdapter;
import org.thoughtcrime.securesms.util.adapter.RecyclerViewConcatenateAdapterStickyHeader;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
@@ -110,22 +111,26 @@ public final class ContactSelectionListFragment extends LoggingFragment
public static final String SELECTION_LIMITS = "selection_limits";
public static final String CURRENT_SELECTION = "current_selection";
public static final String HIDE_COUNT = "hide_count";
public static final String CAN_SELECT_SELF = "can_select_self";
public static final String DISPLAY_CHIPS = "display_chips";
private ConstraintLayout constraintLayout;
private TextView emptyText;
private OnContactSelectedListener onContactSelectedListener;
private SwipeRefreshLayout swipeRefresh;
private View showContactsLayout;
private Button showContactsButton;
private TextView showContactsDescription;
private ProgressWheel showContactsProgress;
private String cursorFilter;
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private WarningTextView groupLimit;
private OnSelectionLimitReachedListener onSelectionLimitReachedListener;
private ConstraintLayout constraintLayout;
private TextView emptyText;
private OnContactSelectedListener onContactSelectedListener;
private SwipeRefreshLayout swipeRefresh;
private View showContactsLayout;
private Button showContactsButton;
private TextView showContactsDescription;
private ProgressWheel showContactsProgress;
private String cursorFilter;
private RecyclerView recyclerView;
private RecyclerViewFastScroller fastScroller;
private ContactSelectionListAdapter cursorRecyclerViewAdapter;
private ChipGroup chipGroup;
private HorizontalScrollView chipGroupScrollContainer;
private WarningTextView groupLimit;
@Nullable private FixedViewsAdapter headerAdapter;
@Nullable private FixedViewsAdapter footerAdapter;
@@ -136,6 +141,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
private Set<RecipientId> currentSelection;
private boolean isMulti;
private boolean hideCount;
private boolean canSelectSelf;
@Override
public void onAttach(@NonNull Context context) {
@@ -148,6 +154,14 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (context instanceof ScrollCallback) {
scrollCallback = (ScrollCallback) context;
}
if (context instanceof OnContactSelectedListener) {
onContactSelectedListener = (OnContactSelectedListener) context;
}
if (context instanceof OnSelectionLimitReachedListener) {
onSelectionLimitReachedListener = (OnSelectionLimitReachedListener) context;
}
}
@Override
@@ -217,6 +231,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
hideCount = intent.getBooleanExtra(HIDE_COUNT, false);
selectionLimit = intent.getParcelableExtra(SELECTION_LIMITS);
isMulti = selectionLimit != null;
canSelectSelf = intent.getBooleanExtra(CAN_SELECT_SELF, !isMulti);
if (!isMulti) {
selectionLimit = SelectionLimits.NO_LIMITS;
@@ -295,7 +310,7 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
recyclerView.setAdapter(concatenateAdapter);
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true));
recyclerView.addItemDecoration(new StickyHeaderDecoration(concatenateAdapter, true, true, 0));
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
@@ -448,8 +463,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();
@@ -461,14 +479,18 @@ public final class ContactSelectionListFragment extends LoggingFragment
SelectedContact selectedContact = contact.isUsernameType() ? SelectedContact.forUsername(contact.getRecipientId().orNull(), contact.getNumber())
: SelectedContact.forPhone(contact.getRecipientId().orNull(), contact.getNumber());
if (isMulti && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
if (!canSelectSelf && Recipient.self().getId().equals(selectedContact.getOrCreateRecipientId(requireContext()))) {
Toast.makeText(requireContext(), R.string.ContactSelectionListFragment_you_do_not_need_to_add_yourself_to_the_group, Toast.LENGTH_SHORT).show();
return;
}
if (!isMulti || !cursorRecyclerViewAdapter.isSelectedContact(selectedContact)) {
if (selectionHardLimitReached()) {
GroupLimitDialog.showHardLimitMessage(requireContext());
if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onHardLimitReached(selectionLimit.getHardLimit());
} else {
GroupLimitDialog.showHardLimitMessage(requireContext());
}
return;
}
@@ -486,11 +508,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(Optional.of(recipient.getId()), null)) {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
markContactSelected(selected);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
new AlertDialog.Builder(requireContext())
@@ -504,16 +526,16 @@ public final class ContactSelectionListFragment extends LoggingFragment
if (onContactSelectedListener != null) {
if (onContactSelectedListener.onBeforeContactSelected(contact.getRecipientId(), contact.getNumber())) {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
} else {
markContactSelected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
}
}
} else {
markContactUnselected(selectedContact);
cursorRecyclerViewAdapter.notifyItemChanged(recyclerView.getChildAdapterPosition(contact), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
cursorRecyclerViewAdapter.notifyItemRangeChanged(0, cursorRecyclerViewAdapter.getItemCount(), ContactSelectionListAdapter.PAYLOAD_SELECTION_CHANGE);
if (onContactSelectedListener != null) {
onContactSelectedListener.onContactDeselected(contact.getRecipientId(), contact.getNumber());
@@ -608,7 +630,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
chipGroup.addView(chip);
updateGroupLimit(getChipCount());
if (selectionWarningLimitReachedExactly()) {
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
if (onSelectionLimitReachedListener != null) {
onSelectionLimitReachedListener.onSuggestedLimitReached(selectionLimit.getRecommendedLimit());
} else {
GroupLimitDialog.showRecommendedLimitMessage(requireContext());
}
}
}
@@ -630,6 +656,10 @@ public final class ContactSelectionListFragment extends LoggingFragment
}
private void setChipGroupVisibility(int visibility) {
if (!requireActivity().getIntent().getBooleanExtra(DISPLAY_CHIPS, true)) {
return;
}
TransitionManager.beginDelayedTransition(constraintLayout, new AutoTransition().setDuration(CHIP_GROUP_REVEAL_DURATION_MS));
ConstraintSet constraintSet = new ConstraintSet();
@@ -638,16 +668,12 @@ public final class ContactSelectionListFragment extends LoggingFragment
constraintSet.applyTo(constraintLayout);
}
public void setOnContactSelectedListener(OnContactSelectedListener onContactSelectedListener) {
this.onContactSelectedListener = onContactSelectedListener;
}
public void setOnRefreshListener(SwipeRefreshLayout.OnRefreshListener onRefreshListener) {
this.swipeRefresh.setOnRefreshListener(onRefreshListener);
}
private void smoothScrollChipsToEnd() {
int x = chipGroupScrollContainer.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? chipGroup.getWidth() : 0;
int x = ViewUtil.isLtr(chipGroupScrollContainer) ? chipGroup.getWidth() : 0;
chipGroupScrollContainer.smoothScrollTo(x, 0);
}
@@ -657,6 +683,11 @@ public final class ContactSelectionListFragment extends LoggingFragment
void onContactDeselected(Optional<RecipientId> recipientId, String number);
}
public interface OnSelectionLimitReachedListener {
void onSuggestedLimitReached(int limit);
void onHardLimitReached(int limit);
}
public interface ListCallback {
void onInvite();
void onNewGroup(boolean forceV1);

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,32 @@ 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,22 +22,42 @@ 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);
navigator.onCreate(savedInstanceState);
handleGroupLinkInIntent(getIntent());
handleProxyInIntent(getIntent());
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);
handleGroupLinkInIntent(intent);
handleProxyInIntent(intent);
}
@Override
@@ -77,4 +97,11 @@ public class MainActivity extends PassphraseRequiredActivity {
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString());
}
}
private void handleProxyInIntent(Intent intent) {
Uri data = intent.getData();
if (data != null) {
CommunicationActions.handlePotentialProxyLinkUrl(this, data.toString());
}
}
}

View File

@@ -57,8 +57,10 @@ import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.DynamicIntroTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.SupportEmailUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
@@ -137,7 +139,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
MenuInflater inflater = this.getMenuInflater();
menu.clear();
inflater.inflate(R.menu.log_submit, menu);
inflater.inflate(R.menu.passphrase_prompt, menu);
super.onCreateOptionsMenu(menu);
return true;
@@ -146,8 +148,12 @@ public class PassphrasePromptActivity extends PassphraseActivity {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case R.id.menu_submit_debug_logs: handleLogSubmit(); return true;
if (item.getItemId() == R.id.menu_submit_debug_logs) {
handleLogSubmit();
return true;
} else if (item.getItemId() == R.id.menu_contact_support) {
sendEmailToSupport();
return true;
}
return false;
@@ -294,6 +300,17 @@ public class PassphrasePromptActivity extends PassphraseActivity {
}
}
private void sendEmailToSupport() {
String body = SupportEmailUtil.generateSupportEmailBody(this,
R.string.PassphrasePromptActivity_signal_android_lock_screen,
null,
null);
CommunicationActions.openEmail(this,
SupportEmailUtil.getSupportEmailAddress(this),
getString(R.string.PassphrasePromptActivity_signal_android_lock_screen),
body);
}
private class PassphraseActionListener implements TextView.OnEditorActionListener {
@Override
public boolean onEditorAction(TextView exampleView, int actionId, KeyEvent keyEvent) {

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() {}
@@ -71,7 +78,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
super.onResume();
if (networkAccess.isCensored(this)) {
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob(this));
ApplicationDependencies.getJobManager().add(new PushNotificationReceiveJob());
}
}
@@ -84,8 +91,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
@Override
public void onMasterSecretCleared() {
Log.d(TAG, "onMasterSecretCleared()");
if (ApplicationContext.getInstance(this).isAppVisible()) routeApplicationState(true);
else finish();
if (ApplicationDependencies.getAppForegroundObserver().isForegrounded()) routeApplicationState(true);
else finish();
}
protected <T extends Fragment> T initFragment(@IdRes int target,
@@ -187,7 +194,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private Intent getPushRegistrationIntent() {
return RegistrationNavigationActivity.newIntentForNewRegistration(this);
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
}
private Intent getEnterSignalPinIntent() {
@@ -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

@@ -113,7 +113,7 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
private static final String IDENTITY_EXTRA = "recipient_identity";
private static final String VERIFIED_EXTRA = "verified_state";
private final DynamicTheme dynamicTheme = new DynamicDarkActionBarTheme();
private final DynamicTheme dynamicTheme = new DynamicTheme();
private final VerifyDisplayFragment displayFragment = new VerifyDisplayFragment();
private final VerifyScanFragment scanFragment = new VerifyScanFragment();
@@ -161,11 +161,6 @@ public class VerifyIdentityActivity extends PassphraseRequiredActivity implement
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.AndroidManifest__verify_safety_number);
LiveRecipient recipient = Recipient.live(getIntent().getParcelableExtra(RECIPIENT_EXTRA));
recipient.observe(this, r -> setActionBarNotificationBarColor(r.getColor()));
setActionBarNotificationBarColor(recipient.get().getColor());
Bundle extras = new Bundle();
extras.putParcelable(VerifyDisplayFragment.RECIPIENT_ID, getIntent().getParcelableExtra(RECIPIENT_EXTRA));
extras.putParcelable(VerifyDisplayFragment.REMOTE_IDENTITY, getIntent().getParcelableExtra(IDENTITY_EXTRA));
@@ -258,24 +253,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 +659,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

@@ -32,7 +32,6 @@ 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;
@@ -44,6 +43,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
@@ -59,6 +59,7 @@ 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.whispersystems.libsignal.IdentityKey;
@@ -67,10 +68,9 @@ 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;
@@ -81,7 +81,9 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
private DeviceOrientationMonitor deviceOrientationMonitor;
private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen;
private TooltipPopup videoTooltip;
private WebRtcCallViewModel viewModel;
@@ -102,8 +104,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 +130,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 +145,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 +160,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);
@@ -235,7 +237,12 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
private void initializeViewModel() {
viewModel = ViewModelProviders.of(this).get(WebRtcCallViewModel.class);
deviceOrientationMonitor = new DeviceOrientationMonitor(this);
getLifecycle().addObserver(deviceOrientationMonitor);
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor);
viewModel = ViewModelProviders.of(this, factory).get(WebRtcCallViewModel.class);
viewModel.setIsInPipMode(isInPipMode());
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
@@ -257,6 +264,25 @@ public class WebRtcCallActivity extends AppCompatActivity implements SafetyNumbe
}
}
});
viewModel.getOrientation().observe(this, orientation -> {
Intent intent = new Intent(this, WebRtcCallService.class);
intent.setAction(WebRtcCallService.ACTION_ORIENTATION_CHANGED)
.putExtra(WebRtcCallService.EXTRA_ORIENTATION_DEGREES, orientation.getDegrees());
startService(intent);
switch (orientation) {
case LANDSCAPE_LEFT_EDGE:
callScreen.rotateControls(90);
break;
case LANDSCAPE_RIGHT_EDGE:
callScreen.rotateControls(-90);
break;
case PORTRAIT_BOTTOM_EDGE:
callScreen.rotateControls(0);
}
});
}
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
@@ -473,7 +499,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) {
@@ -531,7 +556,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());
}
}
@@ -542,7 +567,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);
@@ -639,6 +664,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) {
@@ -704,5 +739,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

@@ -124,8 +124,6 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
ContactSelectionListFragment fragment = new ContactSelectionListFragment();
Intent intent = getIntent();
fragment.setOnContactSelectedListener(this);
intent.putExtra(ContactSelectionListFragment.REFRESHABLE, false);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, 1);
intent.putExtra(ContactSelectionListFragment.HIDE_COUNT, true);

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,13 +6,13 @@ 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;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -20,6 +20,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;
@@ -130,6 +131,20 @@ public class ConversationItemFooter extends LinearLayout {
presentDeliveryStatus(messageRecord);
}
public void enableBubbleBackground(@DrawableRes int drawableRes, @Nullable Integer tint) {
setBackgroundResource(drawableRes);
if (tint != null) {
getBackground().setColorFilter(tint, PorterDuff.Mode.MULTIPLY);
} else {
getBackground().clearColorFilter();
}
}
public void disableBubbleBackground() {
setBackground(null);
}
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
dateView.forceLayout();
if (messageRecord.isFailed()) {
@@ -185,20 +200,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);
@@ -264,8 +275,8 @@ public class ConversationItemFooter extends LinearLayout {
addView(audioDuration, 0);
int padStart = ViewUtil.dpToPx(60);
int padLeft = getLayoutDirection() == LAYOUT_DIRECTION_LTR ? padStart : 0;
int padRight = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? padStart : 0;
int padLeft = ViewUtil.isLtr(this) ? padStart : 0;
int padRight = ViewUtil.isRtl(this) ? padStart : 0;
audioDuration.setPadding(padLeft, 0, padRight, 0);
}

View File

@@ -44,7 +44,7 @@ public class ConversationTypingView extends LinearLayout {
bubble.getBackground().setColorFilter(typist.getColor().toConversationColor(getContext()), PorterDuff.Mode.MULTIPLY);
if (isGroupThread) {
avatar.setAvatar(glideRequests, typist, false);
avatar.setAvatar(glideRequests, typist, true);
avatar.setVisibility(VISIBLE);
} else {
avatar.setVisibility(GONE);

View File

@@ -74,6 +74,7 @@ public class InputPanel extends LinearLayout
private View buttonToggle;
private View recordingContainer;
private View recordLockCancel;
private View composeContainer;
private MicrophoneRecorderView microphoneRecorderView;
private SlideToCancel slideToCancel;
@@ -103,6 +104,7 @@ public class InputPanel extends LinearLayout
View quoteDismiss = findViewById(R.id.quote_dismiss);
this.composeContainer = findViewById(R.id.compose_bubble);
this.stickerSuggestion = findViewById(R.id.input_panel_sticker_suggestion);
this.quoteView = findViewById(R.id.quote_view);
this.linkPreview = findViewById(R.id.link_preview);
@@ -286,6 +288,16 @@ public class InputPanel extends LinearLayout
return mediaKeyboard;
}
public void setWallpaperEnabled(boolean enabled) {
if (enabled) {
setBackgroundColor(getContext().getResources().getColor(R.color.wallpaper_compose_background));
composeContainer.setBackgroundResource(R.drawable.compose_background_wallpaper);
} else {
setBackgroundColor(getResources().getColor(R.color.signal_background_primary));
composeContainer.setBackgroundResource(R.drawable.compose_background);
}
}
@Override
public void onRecordPermissionRequired() {
if (listener != null) listener.onRecorderPermissionRequired();
@@ -323,11 +335,10 @@ public class InputPanel extends LinearLayout
public void onRecordMoved(float offsetX, float absoluteX) {
slideToCancel.moveTo(offsetX);
int direction = ViewCompat.getLayoutDirection(this);
float position = absoluteX / recordingContainer.getWidth();
if (direction == ViewCompat.LAYOUT_DIRECTION_LTR && position <= 0.5 ||
direction == ViewCompat.LAYOUT_DIRECTION_RTL && position >= 0.6)
if (ViewUtil.isLtr(this) && position <= 0.5 ||
ViewUtil.isRtl(this) && position >= 0.6)
{
this.microphoneRecorderView.cancelAction();
}

View File

@@ -0,0 +1,66 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Rect;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.Guideline;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class InsetAwareConstraintLayout extends ConstraintLayout {
public InsetAwareConstraintLayout(@NonNull Context context) {
super(context);
}
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public InsetAwareConstraintLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected boolean fitSystemWindows(Rect insets) {
Guideline statusBarGuideline = findViewById(R.id.status_bar_guideline);
Guideline navigationBarGuideline = findViewById(R.id.navigation_bar_guideline);
Guideline parentStartGuideline = findViewById(R.id.parent_start_guideline);
Guideline parentEndGuideline = findViewById(R.id.parent_end_guideline);
if (statusBarGuideline != null) {
statusBarGuideline.setGuidelineBegin(insets.top);
}
if (navigationBarGuideline != null) {
navigationBarGuideline.setGuidelineEnd(insets.bottom);
}
if (parentStartGuideline != null) {
if (ViewUtil.isLtr(this)) {
parentStartGuideline.setGuidelineBegin(insets.left);
} else {
parentStartGuideline.setGuidelineBegin(insets.right);
}
}
if (parentEndGuideline != null) {
if (ViewUtil.isLtr(this)) {
parentEndGuideline.setGuidelineEnd(insets.right);
} else {
parentEndGuideline.setGuidelineEnd(insets.left);
}
}
return true;
}
}

View File

@@ -16,6 +16,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class LabeledEditText extends FrameLayout implements View.OnFocusChangeListener {
@@ -91,4 +92,8 @@ public class LabeledEditText extends FrameLayout implements View.OnFocusChangeLi
super.setEnabled(enabled);
input.setEnabled(enabled);
}
public void focusAndMoveCursorToEndAndOpenKeyboard() {
ViewUtil.focusAndMoveCursorToEndAndOpenKeyboard(input);
}
}

View File

@@ -55,7 +55,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);
}
@@ -225,8 +225,8 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
}
private float getXOffset(float x) {
return ViewCompat.getLayoutDirection(recordButtonFab) == ViewCompat.LAYOUT_DIRECTION_LTR ?
-Math.max(0, this.startPositionX - x) : Math.max(0, x - this.startPositionX);
return ViewUtil.isLtr(recordButtonFab) ? -Math.max(0, this.startPositionX - x)
: Math.max(0, x - this.startPositionX);
}
private float getYOffset(float y) {

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

@@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.util.AttributeSet;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
/**
* Selection aware {@link EmojiEditText}. This view allows the developer to provide an
* {@link OnSelectionChangedListener} that will be notified when the selection is changed.
*/
public class SelectionAwareEmojiEditText extends EmojiEditText {
private OnSelectionChangedListener onSelectionChangedListener;
public SelectionAwareEmojiEditText(Context context) {
super(context);
}
public SelectionAwareEmojiEditText(Context context, AttributeSet attrs) {
super(context, attrs);
}
public SelectionAwareEmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void setOnSelectionChangedListener(@Nullable OnSelectionChangedListener onSelectionChangedListener) {
this.onSelectionChangedListener = onSelectionChangedListener;
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
if (onSelectionChangedListener != null) {
onSelectionChangedListener.onSelectionChanged(selStart, selEnd);
}
super.onSelectionChanged(selStart, selEnd);
}
public interface OnSelectionChangedListener {
void onSelectionChanged(int selStart, int selEnd);
}
}

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

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.text.InputFilter;
import android.util.AttributeSet;
@@ -27,7 +28,12 @@ public class EmojiEditText extends AppCompatEditText {
public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (!TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
a.recycle();
if (forceCustom || !TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
setFilters(appendEmojiFilter(this.getFilters()));
}
}

View File

@@ -30,7 +30,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
{
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
private static final String RECENT_STORAGE_KEY = "pref_recent_emoji2";
public static final String RECENT_STORAGE_KEY = "pref_recent_emoji2";
private final Context context;
private final List<EmojiPageModel> models;
@@ -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

@@ -10,6 +10,7 @@ import android.text.Spanned;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.ViewGroup;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
@@ -137,6 +138,10 @@ public class EmojiTextView extends AppCompatTextView {
}
}
}
if (getLayoutParams() != null && getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
requestLayout();
}
}
public void setOverflowText(@Nullable CharSequence overflowText) {

View File

@@ -1,7 +1,14 @@
package org.thoughtcrime.securesms.components.emoji;
import androidx.annotation.NonNull;
import android.content.Context;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
import java.util.HashMap;
@@ -72,4 +79,15 @@ public final class EmojiUtil {
String canonical = VARIATION_MAP.get(emoji);
return canonical != null ? canonical : emoji;
}
/**
* Converts the provided emoji string into a single drawable, if possible.
*/
public static @Nullable Drawable convertToDrawable(@NonNull Context context, @Nullable String emoji) {
if (Util.isEmpty(emoji)) {
return null;
} else {
return EmojiProvider.getInstance(context).getEmojiDrawable(emoji);
}
}
}

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

@@ -13,7 +13,7 @@ public class GroupsV1MigrationInitiationReminder extends Reminder {
public GroupsV1MigrationInitiationReminder(@NonNull Context context) {
super(null, context.getString(R.string.GroupsV1MigrationInitiationReminder_to_access_new_features_like_mentions));
addAction(new Action(context.getString(R.string.GroupsV1MigrationInitiationReminder_update_group), R.id.reminder_action_gv1_initiation_update_group));
addAction(new Action(context.getString(R.string.GroupsV1MigrationInitiationReminder_upgrade_group), R.id.reminder_action_gv1_initiation_update_group));
addAction(new Action(context.getResources().getString(R.string.GroupsV1MigrationInitiationReminder_not_now), R.id.reminder_action_gv1_initiation_not_now));
}

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

@@ -0,0 +1,104 @@
package org.thoughtcrime.securesms.components.sensors;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import androidx.annotation.NonNull;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import org.thoughtcrime.securesms.util.ServiceUtil;
public final class DeviceOrientationMonitor implements DefaultLifecycleObserver {
private static final float MAGNITUDE_MAXIMUM = 1.5f;
private static final float MAGNITUDE_MINIMUM = 0.75f;
private static final float LANDSCAPE_PITCH_MINIMUM = -0.5f;
private static final float LANDSCAPE_PITCH_MAXIMUM = 0.5f;
private final SensorManager sensorManager;
private final EventListener eventListener = new EventListener();
private final float[] accelerometerReading = new float[3];
private final float[] magnetometerReading = new float[3];
private final float[] rotationMatrix = new float[9];
private final float[] orientationAngles = new float[3];
private final MutableLiveData<Orientation> orientation = new MutableLiveData<>();
public DeviceOrientationMonitor(@NonNull Context context) {
this.sensorManager = ServiceUtil.getSensorManager(context);
}
@Override
public void onStart(@NonNull LifecycleOwner owner) {
Sensor accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
if (accelerometer != null) {
sensorManager.registerListener(eventListener,
accelerometer,
SensorManager.SENSOR_DELAY_NORMAL,
SensorManager.SENSOR_DELAY_UI);
}
Sensor magneticField = sensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);
if (magneticField != null) {
sensorManager.registerListener(eventListener,
magneticField,
SensorManager.SENSOR_DELAY_NORMAL,
SensorManager.SENSOR_DELAY_UI);
}
}
@Override
public void onStop(@NonNull LifecycleOwner owner) {
sensorManager.unregisterListener(eventListener);
}
public LiveData<Orientation> getOrientation() {
return Transformations.distinctUntilChanged(orientation);
}
private void updateOrientationAngles() {
SensorManager.getRotationMatrix(rotationMatrix, null, accelerometerReading, magnetometerReading);
SensorManager.getOrientation(rotationMatrix, orientationAngles);
float pitch = orientationAngles[1];
float roll = orientationAngles[2];
float mag = (float) Math.sqrt(Math.pow(pitch, 2) + Math.pow(roll, 2));
if (mag > MAGNITUDE_MAXIMUM || mag < MAGNITUDE_MINIMUM) {
return;
}
if (pitch > LANDSCAPE_PITCH_MINIMUM && pitch < LANDSCAPE_PITCH_MAXIMUM) {
orientation.setValue(roll > 0 ? Orientation.LANDSCAPE_RIGHT_EDGE : Orientation.LANDSCAPE_LEFT_EDGE);
} else {
orientation.setValue(Orientation.PORTRAIT_BOTTOM_EDGE);
}
}
private final class EventListener implements SensorEventListener {
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
System.arraycopy(event.values, 0, accelerometerReading, 0, accelerometerReading.length);
} else if (event.sensor.getType() == Sensor.TYPE_MAGNETIC_FIELD) {
System.arraycopy(event.values, 0, magnetometerReading, 0, magnetometerReading.length);
}
updateOrientationAngles();
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}
}

View File

@@ -0,0 +1,29 @@
package org.thoughtcrime.securesms.components.sensors;
import androidx.annotation.NonNull;
public enum Orientation {
PORTRAIT_BOTTOM_EDGE(0),
LANDSCAPE_LEFT_EDGE(90),
LANDSCAPE_RIGHT_EDGE(270);
private final int degrees;
Orientation(int degrees) {
this.degrees = degrees;
}
public int getDegrees() {
return degrees;
}
public static @NonNull Orientation fromDegrees(int degrees) {
for (Orientation orientation : Orientation.values()) {
if (orientation.degrees == degrees) {
return orientation;
}
}
return PORTRAIT_BOTTOM_EDGE;
}
}

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,6 +29,7 @@ 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;
@@ -49,6 +50,7 @@ public class CallParticipantView extends ConstraintLayout {
private RecipientId recipientId;
private boolean infoMode;
private Runnable missingMediaKeysUpdater;
private AppCompatImageView backgroundAvatar;
private AvatarImageView avatar;
@@ -77,6 +79,7 @@ public class CallParticipantView extends ConstraintLayout {
@Override
protected void onFinishInflate() {
super.onFinishInflate();
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);
@@ -102,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() && (System.currentTimeMillis() - participant.getAddedToCallTime()) > DELAY_SHOWING_MISSING_MEDIA_KEYS);
infoMode = participant.getRecipient().isBlocked() || isMissingMediaKeys(participant);
if (infoMode) {
renderer.setVisibility(View.GONE);
@@ -149,6 +152,28 @@ public class CallParticipantView extends ConstraintLayout {
}
}
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

@@ -31,8 +31,8 @@ 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;
@@ -112,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:
@@ -144,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,36 +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,
0);
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 long remoteDevicesCount;
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,
long remoteDevicesCount)
OptionalLong remoteDevicesCount)
{
this.callState = callState;
this.groupCallState = groupCallState;
@@ -80,11 +82,7 @@ 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() {
@@ -93,14 +91,11 @@ public final class CallParticipantsState {
if (isViewingFocusedParticipant && getAllRemoteParticipants().size() > 1) {
listParticipants.addAll(getAllRemoteParticipants());
listParticipants.remove(focusedParticipant);
} else if (getAllRemoteParticipants().size() > SMALL_GROUP_MAX) {
listParticipants.addAll(getAllRemoteParticipants().subList(SMALL_GROUP_MAX, getAllRemoteParticipants().size()));
} else {
return Collections.emptyList();
listParticipants.addAll(remoteParticipants.getListParticipants());
}
listParticipants.add(CallParticipant.EMPTY);
Collections.reverse(listParticipants);
return listParticipants;
@@ -112,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() {
@@ -158,10 +153,17 @@ public final class CallParticipantsState {
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
}
public long getRemoteDevicesCount() {
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)
@@ -179,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())));
@@ -188,7 +191,7 @@ public final class CallParticipantsState {
return new CallParticipantsState(webRtcViewModel.getState(),
webRtcViewModel.getGroupState(),
webRtcViewModel.getRemoteParticipants(),
oldState.remoteParticipants.getNext(webRtcViewModel.getRemoteParticipants()),
webRtcViewModel.getLocalParticipant(),
focused,
localRenderState,
@@ -205,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);
@@ -221,6 +225,28 @@ public final class CallParticipantsState {
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) {
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
@@ -230,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,
@@ -250,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,24 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
public final class OrientationAwareVideoSink implements VideoSink {
private final VideoSink delegate;
public OrientationAwareVideoSink(@NonNull VideoSink delegate) {
this.delegate = delegate;
}
@Override
public void onFrame(VideoFrame videoFrame) {
if (videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth()) {
delegate.onFrame(new VideoFrame(videoFrame.getBuffer(), 270, videoFrame.getTimestampNs()));
} else {
delegate.onFrame(videoFrame);
}
}
}

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

@@ -17,6 +17,7 @@ import androidx.core.view.GestureDetectorCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.TouchInterceptingFrameLayout;
import java.util.Arrays;
@@ -222,8 +223,9 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
@Override
public boolean onSingleTapUp(MotionEvent e) {
child.performClick();
isDragging = false;
child.performClick();
return true;
}
@@ -258,8 +260,8 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
private Point findNearestCornerPosition(Point projection) {
if (isLockedToBottomEnd) {
return parent.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? calculateBottomRightCoordinates(parent)
: calculateBottomLeftCoordinates(parent);
return ViewUtil.isLtr(parent) ? calculateBottomRightCoordinates(parent)
: calculateBottomLeftCoordinates(parent);
}
Point maxPoint = null;

View File

@@ -1,15 +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.ViewStub;
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;
@@ -34,6 +43,7 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.google.android.material.button.MaterialButton;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
@@ -97,16 +107,20 @@ public class WebRtcCallView extends FrameLayout {
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<>();
private final Set<View> visibleViewSet = new HashSet<>();
private final Set<View> adjustableMarginsSet = new HashSet<>();
private final Set<View> rotatableControls = new HashSet<>();
private WebRtcControls controls = WebRtcControls.NONE;
private final Runnable fadeOutRunnable = () -> {
@@ -120,7 +134,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")
@@ -152,6 +166,7 @@ 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));
@@ -159,7 +174,6 @@ public class WebRtcCallView extends FrameLayout {
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)));
@@ -211,7 +225,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) {
@@ -226,8 +247,21 @@ 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();
}
});
rotatableControls.add(hangup);
rotatableControls.add(answer);
rotatableControls.add(answerWithAudio);
rotatableControls.add(audioToggle);
rotatableControls.add(micToggle);
rotatableControls.add(videoToggle);
rotatableControls.add(cameraDirectionToggle);
rotatableControls.add(decline);
rotatableControls.add(smallLocalRender.findViewById(R.id.call_participant_mic_muted));
}
@Override
@@ -239,12 +273,38 @@ 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();
cancelFadeOut();
}
public void rotateControls(int degrees) {
for (View view : rotatableControls) {
view.animate().rotation(degrees);
}
}
public void setControlsListener(@Nullable ControlsListener controlsListener) {
this.controlsListener = controlsListener;
}
@@ -264,19 +324,21 @@ public class WebRtcCallView extends FrameLayout {
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.getRemoteDevicesCount() + (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();
@@ -285,8 +347,7 @@ public class WebRtcCallView extends FrameLayout {
}
}
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant) {
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
public void updateLocalCallParticipant(@NonNull WebRtcLocalRenderState state, @NonNull CallParticipant localCallParticipant, @NonNull CallParticipant focusedParticipant) {
largeLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
smallLocalRender.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
@@ -296,9 +357,19 @@ 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 || state == WebRtcLocalRenderState.GONE) && pictureInPictureExpansionHelper.isExpandedOrExpanding()) {
shrinkPip(localCallParticipant);
return;
} else {
smallLocalRender.setCallParticipant(localCallParticipant);
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
}
switch (state) {
case GONE:
@@ -356,7 +427,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);
@@ -426,6 +496,11 @@ public class WebRtcCallView extends FrameLayout {
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()));
@@ -502,6 +577,10 @@ public class WebRtcCallView extends FrameLayout {
}
} else {
cancelFadeOut();
if (controlsListener != null) {
controlsListener.showSystemUI();
}
}
controls = webRtcControls;
@@ -526,6 +605,62 @@ public class WebRtcCallView extends FrameLayout {
}
}
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);
largeLocalRenderNoVideo.setVisibility(View.GONE);
largeLocalRenderNoVideoAvatar.setVisibility(View.GONE);
}
@Override
public void onPictureInPictureNotVisible() {
smallLocalRender.setCallParticipant(focusedParticipant);
smallLocalRender.setMirror(false);
}
@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);
smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT);
if (!localCallParticipant.isVideoEnabled()) {
smallLocalRenderFrame.setVisibility(View.GONE);
}
}
@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);
@@ -568,12 +703,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();
}
@@ -617,6 +750,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();
@@ -701,6 +843,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);
@@ -711,5 +855,6 @@ public class WebRtcCallView extends FrameLayout {
void onAcceptCallPressed();
void onShowParticipantsList();
void onPageChanged(@NonNull CallParticipantsState.SelectedPage page);
void onLocalPictureInPictureClicked();
}
}

View File

@@ -10,9 +10,12 @@ import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.database.IdentityDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.events.CallParticipant;
@@ -31,23 +34,25 @@ import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
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 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 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 final LiveData<Orientation> orientation;
private boolean canDisplayTooltipIfNeeded = true;
private boolean hasEnabledLocalVideo = false;
@@ -56,11 +61,25 @@ 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());
private WebRtcCallViewModel(@NonNull DeviceOrientationMonitor deviceOrientationMonitor) {
orientation = LiveDataUtil.combineLatest(deviceOrientationMonitor.getOrientation(), webRtcControls, (deviceOrientation, controls) -> {
if (controls.canRotateControls()) {
return deviceOrientation;
} else {
return Orientation.PORTRAIT_BOTTOM_EDGE;
}
});
}
public LiveData<Orientation> getOrientation() {
return Transformations.distinctUntilChanged(orientation);
}
public LiveData<Boolean> getMicrophoneEnabled() {
return Transformations.distinctUntilChanged(microphoneEnabled);
}
@@ -113,8 +132,8 @@ public class WebRtcCallViewModel extends ViewModel {
return answerWithVideoAvailable;
}
public boolean isCallingStarted() {
return callingStarted;
public boolean isCallStarting() {
return callStarting;
}
@MainThread
@@ -135,13 +154,26 @@ public class WebRtcCallViewModel extends ViewModel {
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();
@@ -170,7 +202,7 @@ public class WebRtcCallViewModel extends ViewModel {
webRtcViewModel.isBluetoothAvailable(),
Util.hasItems(webRtcViewModel.getRemoteParticipants()),
repository.getAudioOutput(),
webRtcViewModel.getRemoteDevicesCount(),
webRtcViewModel.getRemoteDevicesCount().orElse(0),
webRtcViewModel.getParticipantLimit());
if (webRtcViewModel.getState() == WebRtcViewModel.State.CALL_CONNECTED && callConnectedTime == -1) {
@@ -232,6 +264,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;
}
@@ -274,9 +309,9 @@ public class WebRtcCallViewModel extends ViewModel {
}
private boolean shouldShowSpeakerHint(@NonNull CallParticipantsState state) {
return !state.isInPipMode() &&
state.getRemoteDevicesCount() > 1 &&
state.getGroupCallState().isConnected() &&
return !state.isInPipMode() &&
state.getRemoteDevicesCount().orElse(0) > 1 &&
state.getGroupCallState().isConnected() &&
!SignalStore.tooltips().hasSeenGroupCallSpeakerView();
}
@@ -309,7 +344,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 -> {
@@ -378,4 +413,18 @@ public class WebRtcCallViewModel extends ViewModel {
return recipientIds;
}
}
public static class Factory implements ViewModelProvider.Factory {
private final DeviceOrientationMonitor deviceOrientationMonitor;
public Factory(@NonNull DeviceOrientationMonitor deviceOrientationMonitor) {
this.deviceOrientationMonitor = deviceOrientationMonitor;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
return Objects.requireNonNull(modelClass.cast(new WebRtcCallViewModel(deviceOrientationMonitor)));
}
}
}

View File

@@ -51,6 +51,14 @@ public final class WebRtcControls {
this.participantLimit = participantLimit;
}
boolean canRotateControls() {
return !isGroupCall();
}
boolean displayErrorControls() {
return isError();
}
boolean displayStartCallControls() {
return isPreJoin();
}
@@ -145,6 +153,10 @@ public final class WebRtcControls {
return audioOutput;
}
private boolean isError() {
return callState == CallState.ERROR;
}
private boolean isPreJoin() {
return callState == CallState.PRE_JOIN;
}
@@ -167,6 +179,7 @@ public final class WebRtcControls {
public enum CallState {
NONE,
ERROR,
PRE_JOIN,
INCOMING,
OUTGOING,

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

@@ -15,6 +15,7 @@ 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;
@@ -88,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((int) callParticipantsState.getRemoteDevicesCount() + (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

@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.Pair;
@@ -42,28 +43,29 @@ public class ContactRepository {
static final String NUMBER_TYPE_COLUMN = "number_type";
static final String LABEL_COLUMN = "label";
static final String CONTACT_TYPE_COLUMN = "contact_type";
static final String ABOUT_COLUMN = "about";
static final int NORMAL_TYPE = 0;
static final int PUSH_TYPE = 1;
static final int NEW_PHONE_TYPE = 2;
static final int NEW_USERNAME_TYPE = 3;
static final int RECENT_TYPE = 4;
static final int DIVIDER_TYPE = 5;
static final int PUSH_TYPE = 1 << 0;
static final int NEW_PHONE_TYPE = 1 << 2;
static final int NEW_USERNAME_TYPE = 1 << 3;
static final int RECENT_TYPE = 1 << 4;
static final int DIVIDER_TYPE = 1 << 5;
/** Maps the recipient results to the legacy contact column names */
private static final List<Pair<String, ValueMapper>> SEARCH_CURSOR_MAPPERS = new ArrayList<Pair<String, ValueMapper>>() {{
add(new Pair<>(ID_COLUMN, cursor -> cursor.getLong(cursor.getColumnIndexOrThrow(RecipientDatabase.ID))));
add(new Pair<>(ID_COLUMN, cursor -> CursorUtil.requireLong(cursor, RecipientDatabase.ID)));
add(new Pair<>(NAME_COLUMN, cursor -> {
String system = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_DISPLAY_NAME));
String profile = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SEARCH_PROFILE_NAME));
String system = CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_DISPLAY_NAME);
String profile = CursorUtil.requireString(cursor, RecipientDatabase.SEARCH_PROFILE_NAME);
return Util.getFirstNonEmpty(system, profile);
}));
add(new Pair<>(NUMBER_COLUMN, cursor -> {
String phone = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.PHONE));
String email = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.EMAIL));
String phone = CursorUtil.requireString(cursor, RecipientDatabase.PHONE);
String email = CursorUtil.requireString(cursor, RecipientDatabase.EMAIL);
if (phone != null) {
phone = PhoneNumberFormatter.prettyPrint(phone);
@@ -72,14 +74,31 @@ public class ContactRepository {
return Util.getFirstNonEmpty(phone, email);
}));
add(new Pair<>(NUMBER_TYPE_COLUMN, cursor -> cursor.getInt(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_PHONE_TYPE))));
add(new Pair<>(NUMBER_TYPE_COLUMN, cursor -> CursorUtil.requireInt(cursor, RecipientDatabase.SYSTEM_PHONE_TYPE)));
add(new Pair<>(LABEL_COLUMN, cursor -> cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.SYSTEM_PHONE_LABEL))));
add(new Pair<>(LABEL_COLUMN, cursor -> CursorUtil.requireString(cursor, RecipientDatabase.SYSTEM_PHONE_LABEL)));
add(new Pair<>(CONTACT_TYPE_COLUMN, cursor -> {
int registered = cursor.getInt(cursor.getColumnIndexOrThrow(RecipientDatabase.REGISTERED));
int registered = CursorUtil.requireInt(cursor, RecipientDatabase.REGISTERED);
return registered == RecipientDatabase.RegisteredState.REGISTERED.getId() ? PUSH_TYPE : NORMAL_TYPE;
}));
add(new Pair<>(ABOUT_COLUMN, cursor -> {
String aboutEmoji = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT_EMOJI);
String about = CursorUtil.requireString(cursor, RecipientDatabase.ABOUT);
if (!Util.isEmpty(aboutEmoji)) {
if (!Util.isEmpty(about)) {
return aboutEmoji + " " + about;
} else {
return aboutEmoji;
}
} else if (!Util.isEmpty(about)) {
return about;
} else {
return "";
}
}));
}};
public ContactRepository(@NonNull Context context) {
@@ -106,7 +125,7 @@ public class ContactRepository {
if (shouldAdd) {
MatrixCursor selfCursor = new MatrixCursor(RecipientDatabase.SEARCH_PROJECTION_NAMES);
selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, null, self.getE164().or(""), self.getEmail().orNull(), null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), noteToSelfTitle });
selfCursor.addRow(new Object[]{ self.getId().serialize(), noteToSelfTitle, self.getE164().or(""), self.getEmail().orNull(), null, -1, RecipientDatabase.RegisteredState.REGISTERED.getId(), self.getAbout(), self.getAboutEmoji(), noteToSelfTitle, noteToSelfTitle });
cursor = cursor == null ? selfCursor : new MergeCursor(new Cursor[]{ cursor, selfCursor });
}

View File

@@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.contacts.ContactSelectionListAdapter.ViewHolde
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration.StickyHeaderAdapter;
import org.thoughtcrime.securesms.util.Util;
@@ -97,7 +98,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
super(itemView);
}
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean checkboxVisible);
public abstract void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, int color, boolean checkboxVisible);
public abstract void unbind(@NonNull GlideRequests glideRequests);
public abstract void setChecked(boolean checked);
public abstract void setEnabled(boolean enabled);
@@ -117,8 +118,8 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
return (ContactSelectionListItem) itemView;
}
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean checkBoxVisible) {
getView().set(glideRequests, recipientId, type, name, number, label, color, checkBoxVisible);
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, int color, boolean checkBoxVisible) {
getView().set(glideRequests, recipientId, type, name, number, label, about, color, checkBoxVisible);
}
@Override
@@ -147,7 +148,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
}
@Override
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, int color, boolean checkboxVisible) {
public void bind(@NonNull GlideRequests glideRequests, @Nullable RecipientId recipientId, int type, String name, String number, String label, String about, int color, boolean checkboxVisible) {
this.label.setText(name);
}
@@ -204,23 +205,25 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
@Override
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
String rawId = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN));
String rawId = CursorUtil.requireString(cursor, ContactRepository.ID_COLUMN);
RecipientId id = rawId != null ? RecipientId.from(rawId) : null;
int contactType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.CONTACT_TYPE_COLUMN));
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN ));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN));
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN ));
String label = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.LABEL_COLUMN ));
int contactType = CursorUtil.requireInt(cursor, ContactRepository.CONTACT_TYPE_COLUMN);
String name = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN);
String number = CursorUtil.requireString(cursor, ContactRepository.NUMBER_COLUMN);
int numberType = CursorUtil.requireInt(cursor, ContactRepository.NUMBER_TYPE_COLUMN);
String about = CursorUtil.requireString(cursor, ContactRepository.ABOUT_COLUMN);
String label = CursorUtil.requireString(cursor, ContactRepository.LABEL_COLUMN);
String labelText = ContactsContract.CommonDataKinds.Phone.getTypeLabel(getContext().getResources(),
numberType, label).toString();
boolean isPush = (contactType & ContactRepository.PUSH_TYPE) > 0;
int color = (contactType == ContactRepository.PUSH_TYPE) ? ContextCompat.getColor(getContext(), R.color.signal_text_primary)
: ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_60);
int color = isPush ? ContextCompat.getColor(getContext(), R.color.signal_text_primary)
: ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_60);
boolean currentContact = currentContacts.contains(id);
viewHolder.unbind(glideRequests);
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, color, multiSelect || currentContact);
viewHolder.bind(glideRequests, id, contactType, name, number, labelText, about, color, multiSelect || currentContact);
viewHolder.setEnabled(true);
if (currentContact) {
@@ -239,10 +242,10 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
throw new AssertionError();
}
String rawId = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.ID_COLUMN));
String rawId = CursorUtil.requireString(cursor, ContactRepository.ID_COLUMN);
RecipientId id = rawId != null ? RecipientId.from(rawId) : null;
int numberType = cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN));
String number = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN));
int numberType = CursorUtil.requireInt(cursor, ContactRepository.NUMBER_TYPE_COLUMN);
String number = CursorUtil.requireString(cursor, ContactRepository.NUMBER_COLUMN);
viewHolder.setEnabled(true);
@@ -258,7 +261,7 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
@Override
public int getItemViewType(@NonNull Cursor cursor) {
if (cursor.getInt(cursor.getColumnIndexOrThrow(ContactRepository.CONTACT_TYPE_COLUMN)) == ContactRepository.DIVIDER_TYPE) {
if (CursorUtil.requireInt(cursor, ContactRepository.CONTACT_TYPE_COLUMN) == ContactRepository.DIVIDER_TYPE) {
return VIEW_TYPE_DIVIDER;
} else {
return VIEW_TYPE_CONTACT;
@@ -266,12 +269,12 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
}
@Override
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position, int type) {
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.contact_selection_recyclerview_header, parent, false));
}
@Override
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position, int type) {
((TextView)viewHolder.itemView).setText(getSpannedHeaderString(position));
}
@@ -312,12 +315,12 @@ public class ContactSelectionListAdapter extends CursorRecyclerViewAdapter<ViewH
private @NonNull String getHeaderString(int position) {
int contactType = getContactType(position);
if (contactType == ContactRepository.RECENT_TYPE || contactType == ContactRepository.DIVIDER_TYPE) {
if ((contactType & ContactRepository.RECENT_TYPE) > 0 || contactType == ContactRepository.DIVIDER_TYPE) {
return " ";
}
Cursor cursor = getCursorAtPositionOrThrow(position);
String letter = cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NAME_COLUMN));
String letter = CursorUtil.requireString(cursor, ContactRepository.NAME_COLUMN);
if (letter != null) {
letter = letter.trim();

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -65,6 +66,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
String name,
String number,
String label,
String about,
int color,
boolean checkboxVisible)
{
@@ -87,7 +89,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
this.numberView.setTextColor(color);
this.contactPhotoImage.setAvatar(glideRequests, recipientSnapshot, false);
setText(recipientSnapshot, type, name, number, label);
setText(recipientSnapshot, type, name, number, label, about);
this.checkBox.setVisibility(checkboxVisible ? View.VISIBLE : View.GONE);
}
@@ -110,7 +112,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
}
@SuppressLint("SetTextI18n")
private void setText(@Nullable Recipient recipient, int type, String name, String number, String label) {
private void setText(@Nullable Recipient recipient, int type, String name, String number, String label, @Nullable String about) {
if (number == null || number.isEmpty()) {
this.nameView.setEnabled(false);
this.numberView.setText("");
@@ -120,7 +122,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
this.numberView.setText(getGroupMemberCount(recipient));
this.labelView.setVisibility(View.GONE);
} else if (type == ContactRepository.PUSH_TYPE) {
this.numberView.setText(number);
this.numberView.setText(!Util.isEmpty(about) ? about : number);
this.nameView.setEnabled(true);
this.labelView.setVisibility(View.GONE);
} else if (type == ContactRepository.NEW_USERNAME_TYPE) {
@@ -129,7 +131,7 @@ public class ContactSelectionListItem extends LinearLayout implements RecipientF
this.labelView.setText(label);
this.labelView.setVisibility(View.VISIBLE);
} else {
this.numberView.setText(number);
this.numberView.setText(!Util.isEmpty(about) ? about : number);
this.nameView.setEnabled(true);
this.labelView.setText(label != null && !label.equals("null") ? label : "");
this.labelView.setVisibility(View.VISIBLE);

View File

@@ -63,6 +63,7 @@ public class ContactsCursorLoader extends CursorLoader {
public static final int FLAG_SELF = 1 << 4;
public static final int FLAG_BLOCK = 1 << 5;
public static final int FLAG_HIDE_GROUPS_V1 = 1 << 5;
public static final int FLAG_HIDE_NEW = 1 << 6;
public static final int FLAG_ALL = FLAG_PUSH | FLAG_SMS | FLAG_ACTIVE_GROUPS | FLAG_INACTIVE_GROUPS | FLAG_SELF;
}
@@ -71,7 +72,8 @@ public class ContactsCursorLoader extends CursorLoader {
ContactRepository.NUMBER_COLUMN,
ContactRepository.NUMBER_TYPE_COLUMN,
ContactRepository.LABEL_COLUMN,
ContactRepository.CONTACT_TYPE_COLUMN};
ContactRepository.CONTACT_TYPE_COLUMN,
ContactRepository.ABOUT_COLUMN};
private static final int RECENT_CONVERSATION_MAX = 25;
@@ -134,8 +136,11 @@ public class ContactsCursorLoader extends CursorLoader {
addContactsSection(cursorList);
addGroupsSection(cursorList);
addNewNumberSection(cursorList);
addUsernameSearchSection(cursorList);
if (!hideNewNumberOrUsername(mode)) {
addNewNumberSection(cursorList);
addUsernameSearchSection(cursorList);
}
return cursorList;
}
@@ -212,7 +217,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return recentsHeader;
}
@@ -223,7 +229,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return contactsHeader;
}
@@ -234,7 +241,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return groupHeader;
}
@@ -245,7 +253,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return contactsHeader;
}
@@ -256,7 +265,8 @@ public class ContactsCursorLoader extends CursorLoader {
"",
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.DIVIDER_TYPE });
ContactRepository.DIVIDER_TYPE,
"" });
return contactsHeader;
}
@@ -269,7 +279,7 @@ public class ContactsCursorLoader extends CursorLoader {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(getContext());
MatrixCursor recentConversations = new MatrixCursor(CONTACT_PROJECTION, RECENT_CONVERSATION_MAX);
try (Cursor rawConversations = threadDatabase.getRecentConversationList(RECENT_CONVERSATION_MAX, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), groupsOnly, hideGroupsV1(mode))) {
try (Cursor rawConversations = threadDatabase.getRecentConversationList(RECENT_CONVERSATION_MAX, flagSet(mode, DisplayMode.FLAG_INACTIVE_GROUPS), groupsOnly, hideGroupsV1(mode), !smsEnabled(mode))) {
ThreadDatabase.Reader reader = threadDatabase.readerFor(rawConversations);
ThreadRecord threadRecord;
while ((threadRecord = reader.getNext()) != null) {
@@ -281,7 +291,8 @@ public class ContactsCursorLoader extends CursorLoader {
stringId,
ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE,
"",
ContactRepository.RECENT_TYPE });
ContactRepository.RECENT_TYPE | (recipient.isRegistered() && !recipient.isForceSmsSelection() ? ContactRepository.PUSH_TYPE : 0),
recipient.getCombinedAboutAndEmoji() });
}
}
return recentConversations;
@@ -316,7 +327,8 @@ public class ContactsCursorLoader extends CursorLoader {
groupRecord.getId(),
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"",
ContactRepository.NORMAL_TYPE });
ContactRepository.NORMAL_TYPE,
"" });
}
}
return groupContacts;
@@ -329,7 +341,8 @@ public class ContactsCursorLoader extends CursorLoader {
filter,
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"\u21e2",
ContactRepository.NEW_PHONE_TYPE});
ContactRepository.NEW_PHONE_TYPE,
"" });
return newNumberCursor;
}
@@ -340,7 +353,8 @@ public class ContactsCursorLoader extends CursorLoader {
filter,
ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM,
"\u21e2",
ContactRepository.NEW_USERNAME_TYPE});
ContactRepository.NEW_USERNAME_TYPE,
"" });
return cursor;
}
@@ -368,7 +382,8 @@ public class ContactsCursorLoader extends CursorLoader {
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.NUMBER_TYPE_COLUMN)),
cursor.getString(cursor.getColumnIndexOrThrow(ContactRepository.LABEL_COLUMN)),
ContactRepository.NORMAL_TYPE});
ContactRepository.NORMAL_TYPE,
"" });
}
}
Log.i(TAG, "filterNonPushContacts() -> " + (System.currentTimeMillis() - startMillis) + "ms");
@@ -418,6 +433,10 @@ public class ContactsCursorLoader extends CursorLoader {
return flagSet(mode, DisplayMode.FLAG_HIDE_GROUPS_V1);
}
private static boolean hideNewNumberOrUsername(int mode) {
return flagSet(mode, DisplayMode.FLAG_HIDE_NEW);
}
private static boolean flagSet(int mode, int flag) {
return (mode & flag) > 0;
}

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

@@ -6,6 +6,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
@@ -23,6 +24,7 @@ import java.util.List;
public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.InputView {
private View container;
private AttachmentKeyboardMediaAdapter mediaAdapter;
private AttachmentKeyboardButtonAdapter buttonAdapter;
private Callback callback;
@@ -44,8 +46,9 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
private void init(@NonNull Context context) {
inflate(context, R.layout.attachment_keyboard, this);
this.mediaList = findViewById(R.id.attachment_keyboard_media_list );
this.permissionText = findViewById(R.id.attachment_keyboard_permission_text );
this.container = findViewById(R.id.attachment_keyboard_container);
this.mediaList = findViewById(R.id.attachment_keyboard_media_list);
this.permissionText = findViewById(R.id.attachment_keyboard_permission_text);
this.permissionButton = findViewById(R.id.attachment_keyboard_permission_button);
RecyclerView buttonList = findViewById(R.id.attachment_keyboard_button_list);
@@ -98,6 +101,16 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
}
}
public void setWallpaperEnabled(boolean wallpaperEnabled) {
if (wallpaperEnabled) {
container.setBackgroundColor(getContext().getResources().getColor(R.color.wallpaper_compose_background));
} else {
container.setBackgroundColor(getContext().getResources().getColor(R.color.signal_background_primary));
}
buttonAdapter.setWallpaperEnabled(wallpaperEnabled);
}
@Override
public void show(int height, boolean immediate) {
ViewGroup.LayoutParams params = getLayoutParams();

View File

@@ -19,6 +19,8 @@ class AttachmentKeyboardButtonAdapter extends RecyclerView.Adapter<AttachmentKey
private final List<AttachmentKeyboardButton> buttons;
private final Listener listener;
private boolean wallpaperEnabled;
AttachmentKeyboardButtonAdapter(@NonNull Listener listener) {
this.buttons = new ArrayList<>();
this.listener = listener;
@@ -39,7 +41,7 @@ class AttachmentKeyboardButtonAdapter extends RecyclerView.Adapter<AttachmentKey
@Override
public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) {
holder.bind(buttons.get(position), listener);
holder.bind(buttons.get(position), wallpaperEnabled, listener);
}
@Override
@@ -52,13 +54,19 @@ class AttachmentKeyboardButtonAdapter extends RecyclerView.Adapter<AttachmentKey
return buttons.size();
}
public void setButtons(@NonNull List<AttachmentKeyboardButton> buttons) {
this.buttons.clear();
this.buttons.addAll(buttons);
notifyDataSetChanged();
}
public void setWallpaperEnabled(boolean enabled) {
if (wallpaperEnabled != enabled) {
wallpaperEnabled = enabled;
notifyDataSetChanged();
}
}
interface Listener {
void onClick(@NonNull AttachmentKeyboardButton button);
}
@@ -75,11 +83,17 @@ class AttachmentKeyboardButtonAdapter extends RecyclerView.Adapter<AttachmentKey
this.title = itemView.findViewById(R.id.attachment_button_title);
}
void bind(@NonNull AttachmentKeyboardButton button, @NonNull Listener listener) {
void bind(@NonNull AttachmentKeyboardButton button,boolean wallpaperEnabled, @NonNull Listener listener) {
image.setImageResource(button.getIconRes());
title.setText(button.getTitleRes());
itemView.setOnClickListener(v -> listener.onClick(button));
if (wallpaperEnabled) {
itemView.setBackgroundResource(R.drawable.attachment_keyboard_button_wallpaper_background);
} else {
itemView.setBackgroundResource(R.drawable.attachment_keyboard_button_background);
}
}
void recycle() {

View File

@@ -24,13 +24,12 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ShortcutManager;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.hardware.Camera;
import android.net.Uri;
@@ -60,6 +59,7 @@ import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
@@ -104,7 +104,6 @@ import org.thoughtcrime.securesms.VerifyIdentityActivity;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.components.ConversationSearchBottomBar;
@@ -200,7 +199,7 @@ import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView;
import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.SlideFactory.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GifSlide;
@@ -216,6 +215,7 @@ import org.thoughtcrime.securesms.mms.QuoteId;
import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.mms.SlideFactory;
import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
@@ -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;
@@ -253,10 +252,11 @@ import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.ConversationUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.MessageUtil;
@@ -274,6 +274,8 @@ import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.views.Stub;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperDimLevelUtil;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.util.Pair;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -302,7 +304,6 @@ import static org.whispersystems.libsignal.SessionCipher.SESSION_LOCK;
* @author Moxie Marlinspike
*
*/
@Trace
@SuppressLint("StaticFieldLeak")
public class ConversationActivity extends PassphraseRequiredActivity
implements ConversationFragment.ConversationFragmentListener,
@@ -339,26 +340,26 @@ public class ConversationActivity extends PassphraseRequiredActivity
private static final int SMS_DEFAULT = 11;
private static final int MEDIA_SENDER = 12;
private GlideRequests glideRequests;
protected ComposeText composeText;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
protected ConversationTitleView titleView;
private TextView charactersLeft;
private ConversationFragment fragment;
private Button unblockButton;
private Button makeDefaultSmsButton;
private Button registerButton;
private InputAwareLayout container;
protected Stub<ReminderView> reminderView;
private Stub<UnverifiedBannerView> unverifiedBannerView;
private Stub<ReviewBannerView> reviewBanner;
private TypingStatusTextWatcher typingTextWatcher;
private ConversationSearchBottomBar searchNav;
private MenuItem searchViewItem;
private MessageRequestsBottomView messageRequestBottomView;
private ConversationReactionOverlay reactionOverlay;
private GlideRequests glideRequests;
protected ComposeText composeText;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
protected ConversationTitleView titleView;
private TextView charactersLeft;
private ConversationFragment fragment;
private Button unblockButton;
private Button makeDefaultSmsButton;
private Button registerButton;
private InputAwareLayout container;
protected Stub<ReminderView> reminderView;
private Stub<UnverifiedBannerView> unverifiedBannerView;
private Stub<ReviewBannerView> reviewBanner;
private TypingStatusTextWatcher typingTextWatcher;
private ConversationSearchBottomBar searchNav;
private MenuItem searchViewItem;
private MessageRequestsBottomView messageRequestBottomView;
private ConversationReactionDelegate reactionDelegate;
private AttachmentManager attachmentManager;
private AudioRecorder audioRecorder;
@@ -374,6 +375,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
private View cancelJoinRequest;
private Stub<View> mentionsSuggestions;
private MaterialButton joinGroupCallButton;
private boolean callingTooltipShown;
private ImageView wallpaper;
private View wallpaperDim;
private Toolbar toolbar;
private LinkPreviewViewModel linkPreviewViewModel;
private ConversationSearchViewModel searchViewModel;
@@ -387,14 +392,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
private LiveRecipient recipient;
private long threadId;
private int distributionType;
private int reactWithAnyEmojiStartPage;
private boolean isSecureText;
private int reactWithAnyEmojiStartPage = -1;
private boolean isDefaultSms = true;
private boolean isMmsEnabled = true;
private boolean isSecurityInitialized = false;
private IdentityRecordList identityRecords = new IdentityRecordList(Collections.emptyList());
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@Override
@@ -408,11 +413,13 @@ 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;
}
new FullscreenHelper(this).showSystemUI();
ConversationIntents.Args args = ConversationIntents.Args.from(getIntent());
reportShortcutLaunch(args.getRecipientId());
@@ -425,6 +432,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
initializeReceivers();
initializeActionBar();
initializeViews();
updateWallpaper(args.getWallpaper());
initializeResources(args);
initializeLinkPreviewObserver();
initializeSearchObserver();
@@ -476,7 +484,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
reactWithAnyEmojiStartPage = 0;
reactWithAnyEmojiStartPage = -1;
if (!Util.isEmpty(composeText) || attachmentManager.isAttachmentPresent() || inputPanel.getQuote().isPresent()) {
saveDraft();
attachmentManager.clear(glideRequests, false);
@@ -487,7 +495,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;
}
@@ -518,6 +526,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
WindowUtil.setLightNavigationBarFromTheme(this);
WindowUtil.setLightStatusBarFromTheme(this);
EventBus.getDefault().register(this);
initializeMmsEnabledCheck();
initializeIdentityRecords();
@@ -526,7 +537,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
Recipient recipientSnapshot = recipient.get();
titleView.setTitle(glideRequests, recipientSnapshot);
setActionBarColor(recipientSnapshot.getColor());
setBlockedUserState(recipientSnapshot, isSecureText, isDefaultSms);
calculateCharactersRemaining();
@@ -537,6 +547,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) {
@@ -578,8 +592,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
container.hideAttachedInput(true);
}
if (reactionOverlay != null && reactionOverlay.isShowing()) {
reactionOverlay.hide();
if (reactionDelegate.isShowing()) {
reactionDelegate.hide();
}
}
@@ -592,7 +606,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
return reactionOverlay.applyTouchEvent(ev) || super.dispatchTouchEvent(ev);
return reactionDelegate.applyTouchEvent(ev) || super.dispatchTouchEvent(ev);
}
@Override
@@ -609,10 +623,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
switch (reqCode) {
case PICK_DOCUMENT:
setMedia(data.getData(), MediaType.DOCUMENT);
setMedia(data.getData(), SlideFactory.MediaType.DOCUMENT);
break;
case PICK_AUDIO:
setMedia(data.getData(), MediaType.AUDIO);
setMedia(data.getData(), SlideFactory.MediaType.AUDIO);
break;
case PICK_CONTACT:
if (isSecureText && !isSmsForced()) {
@@ -652,7 +666,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
break;
case PICK_GIF:
setMedia(data.getData(),
MediaType.GIF,
SlideFactory.MediaType.GIF,
data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0),
data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0),
data.getBooleanExtra(GiphyActivity.EXTRA_BORDERLESS, false));
@@ -729,7 +743,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, 0);
reactWithAnyEmojiStartPage = savedInstanceState.getInt(STATE_REACT_WITH_ANY_PAGE, -1);
}
private void setVisibleThread(long threadId) {
@@ -763,7 +777,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
getContentResolver().delete(attachmentManager.getCaptureUri(), null, null);
setMedia(mediaUri, MediaType.IMAGE);
setMedia(mediaUri, SlideFactory.MediaType.IMAGE);
} catch (IOException ioe) {
Log.w(TAG, "Could not handle public image", ioe);
}
@@ -830,6 +844,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);
@@ -982,7 +997,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
case R.id.menu_expiring_messages_off:
case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true;
case R.id.menu_create_bubble: handleCreateBubble(); return true;
case android.R.id.home: onNavigateUp(); return true;
case android.R.id.home: super.onBackPressed(); return true;
}
return false;
@@ -1013,8 +1028,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public void onBackPressed() {
Log.d(TAG, "onBackPressed()");
if (reactionOverlay.isShowing()) {
reactionOverlay.hide();
if (reactionDelegate.isShowing()) {
reactionDelegate.hide();
} else if (container.isInputOpen()) {
container.hideCurrentInput(composeText);
} else {
@@ -1085,6 +1100,8 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
final long thread = this.threadId;
ExpirationDialog.show(this, recipient.get().getExpireMessages(),
expirationTime ->
SimpleTask.run(
@@ -1100,7 +1117,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;
},
@@ -1385,6 +1402,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
} else {
viewModel.getRecentMedia().observe(this, media -> attachmentKeyboardStub.get().onMediaChanged(media));
attachmentKeyboardStub.get().setCallback(this);
attachmentKeyboardStub.get().setWallpaperEnabled(recipient.get().hasWallpaper());
container.show(composeText, attachmentKeyboardStub.get());
viewModel.onAttachmentKeyboardOpen();
@@ -1464,7 +1482,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
final CharSequence draftText = args.getDraftText();
final Uri draftMedia = getIntent().getData();
final String draftContentType = getIntent().getType();
final MediaType draftMediaType = MediaType.from(draftContentType);
final MediaType draftMediaType = SlideFactory.MediaType.from(draftContentType);
final List<Media> mediaList = args.getMedia();
final StickerLocator stickerLocator = args.getStickerLocator();
final boolean borderless = args.isBorderless();
@@ -1646,13 +1664,13 @@ public class ConversationActivity extends PassphraseRequiredActivity
attachmentManager.setLocation(SignalPlace.deserialize(draft.getValue()), getCurrentMediaConstraints()).addListener(listener);
break;
case Draft.IMAGE:
setMedia(Uri.parse(draft.getValue()), MediaType.IMAGE).addListener(listener);
setMedia(Uri.parse(draft.getValue()), SlideFactory.MediaType.IMAGE).addListener(listener);
break;
case Draft.AUDIO:
setMedia(Uri.parse(draft.getValue()), MediaType.AUDIO).addListener(listener);
setMedia(Uri.parse(draft.getValue()), SlideFactory.MediaType.AUDIO).addListener(listener);
break;
case Draft.VIDEO:
setMedia(Uri.parse(draft.getValue()), MediaType.VIDEO).addListener(listener);
setMedia(Uri.parse(draft.getValue()), SlideFactory.MediaType.VIDEO).addListener(listener);
break;
case Draft.QUOTE:
SettableFuture<Boolean> quoteResult = new SettableFuture<>();
@@ -1780,8 +1798,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,36 +1898,41 @@ 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);
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
wallpaper = findViewById(R.id.conversation_wallpaper);
wallpaperDim = findViewById(R.id.conversation_wallpaper_dim);
ImageButton quickCameraToggle = findViewById(R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = findViewById(R.id.inline_attachment_button);
Stub<ConversationReactionOverlay> reactionOverlayStub = ViewUtil.findStubById(this, R.id.conversation_reaction_scrubber_stub);
reactionDelegate = new ConversationReactionDelegate(reactionOverlayStub);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
noLongerMemberBanner = findViewById(R.id.conversation_no_longer_member_banner);
requestingMemberBanner = findViewById(R.id.conversation_requesting_banner);
cancelJoinRequest = findViewById(R.id.conversation_cancel_request);
joinGroupCallButton = findViewById(R.id.conversation_group_cal_join);
joinGroupCallButton = findViewById(R.id.conversation_group_call_join);
container.addOnKeyboardShownListener(this);
inputPanel.setListener(this);
@@ -1963,13 +1986,41 @@ public class ConversationActivity extends PassphraseRequiredActivity
inlineAttachmentButton.setOnClickListener(v -> handleAddAttachment());
reactionOverlay.setOnReactionSelectedListener(this);
reactionDelegate.setOnReactionSelectedListener(this);
joinGroupCallButton.setOnClickListener(v -> handleVideo(getRecipient()));
}
private void updateWallpaper(@Nullable ChatWallpaper chatWallpaper) {
Log.d(TAG, "Setting wallpaper.");
if (chatWallpaper != null) {
chatWallpaper.loadInto(wallpaper);
ChatWallpaperDimLevelUtil.applyDimLevelForNightMode(wallpaperDim, chatWallpaper);
inputPanel.setWallpaperEnabled(true);
if (attachmentKeyboardStub.resolved()) {
attachmentKeyboardStub.get().setWallpaperEnabled(true);
}
int toolbarColor = getResources().getColor(R.color.conversation_toolbar_color_wallpaper);
toolbar.setBackgroundColor(toolbarColor);
WindowUtil.setStatusBarColor(getWindow(), toolbarColor);
} else {
wallpaper.setImageDrawable(null);
wallpaperDim.setVisibility(View.GONE);
inputPanel.setWallpaperEnabled(false);
if (attachmentKeyboardStub.resolved()) {
attachmentKeyboardStub.get().setWallpaperEnabled(false);
}
int toolbarColor = getResources().getColor(R.color.conversation_toolbar_color);
toolbar.setBackgroundColor(toolbarColor);
WindowUtil.setStatusBarColor(getWindow(), toolbarColor);
}
fragment.onWallpaperChanged(chatWallpaper);
}
protected void initializeActionBar() {
Toolbar toolbar = findViewById(R.id.toolbar);
toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
ActionBar supportActionBar = getSupportActionBar();
@@ -1980,7 +2031,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)));
}
}
@@ -2075,6 +2126,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
this.viewModel = ViewModelProviders.of(this, new ConversationViewModel.Factory()).get(ConversationViewModel.class);
this.viewModel.setArgs(args);
this.viewModel.getWallpaper().observe(this, this::updateWallpaper);
}
private void initializeGroupViewModel() {
@@ -2137,6 +2189,28 @@ public class ConversationActivity extends PassphraseRequiredActivity
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() {
TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER);
inputPanel.setMediaKeyboardToggleMode(true);
@@ -2156,7 +2230,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
public void onReactionSelected(MessageRecord messageRecord, String emoji) {
final Context context = getApplicationContext();
reactionOverlay.hide();
reactionDelegate.hide();
SignalExecutors.BOUNDED.execute(() -> {
ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
@@ -2182,14 +2256,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (oldRecord != null && hasAddedCustomEmoji) {
final Context context = getApplicationContext();
reactionOverlay.hide();
reactionDelegate.hide();
SignalExecutors.BOUNDED.execute(() -> MessageSender.sendReactionRemoval(context,
messageRecord.getId(),
messageRecord.isMms(),
oldRecord));
} else {
reactionOverlay.hideAllButMask();
reactionDelegate.hideAllButMask();
ReactWithAnyEmojiBottomSheetDialogFragment.createForMessageRecord(messageRecord, reactWithAnyEmojiStartPage)
.show(getSupportFragmentManager(), "BOTTOM");
@@ -2198,7 +2272,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public void onReactWithAnyEmojiDialogDismissed() {
reactionOverlay.hideMask();
reactionDelegate.hideMask();
}
@Override
@@ -2206,6 +2280,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
reactWithAnyEmojiStartPage = page;
}
@Override
public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
}
@Override
public void onSearchMoveUpPressed() {
searchViewModel.onMoveUp();
@@ -2234,7 +2312,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
titleView.setTitle(glideRequests, recipient);
titleView.setVerified(identityRecords.isVerified());
setBlockedUserState(recipient, isSecureText, isDefaultSms);
setActionBarColor(recipient.getColor());
updateReminders();
updateDefaultSubscriptionId(recipient.getDefaultSubscriptionId());
initializeSecurity(isSecureText, isDefaultSms);
@@ -2307,10 +2384,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
return new SettableFuture<>(false);
}
if (MediaType.VCARD.equals(mediaType) && isSecureText) {
if (SlideFactory.MediaType.VCARD.equals(mediaType) && isSecureText) {
openContactShareEditor(uri);
return new SettableFuture<>(false);
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
} else if (SlideFactory.MediaType.IMAGE.equals(mediaType) || SlideFactory.MediaType.GIF.equals(mediaType) || SlideFactory.MediaType.VIDEO.equals(mediaType)) {
String mimeType = MediaUtil.getMimeType(this, uri);
if (mimeType == null) {
mimeType = mediaType.toFallbackMimeType();
@@ -2436,18 +2513,6 @@ public class ConversationActivity extends PassphraseRequiredActivity
return future;
}
private void setActionBarColor(MaterialColor color) {
ActionBar supportActionBar = getSupportActionBar();
if (supportActionBar == null) throw new AssertionError();
int actionBarColor = color.toActionBarColor(this);
supportActionBar.setBackgroundDrawable(new ColorDrawable(actionBarColor));
WindowUtil.setStatusBarColor(getWindow(), color.toStatusBarColor(this));
joinGroupCallButton.setTextColor(actionBarColor);
joinGroupCallButton.setIconTint(ColorStateList.valueOf(actionBarColor));
joinGroupCallButton.setRippleColor(ColorStateList.valueOf(actionBarColor));
}
private void setBlockedUserState(Recipient recipient, boolean isSecureText, boolean isDefaultSms) {
if (!isSecureText && isPushGroupConversation()) {
unblockButton.setVisibility(View.GONE);
@@ -2651,13 +2716,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 +2732,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 +2777,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 +2797,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 +2816,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 +2836,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
return;
}
final long thread = this.threadId;
final Context context = getApplicationContext();
final String messageBody = getMessage();
@@ -2775,7 +2844,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 +2860,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
@@ -2878,6 +2947,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
vibrator.vibrate(20);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LOCKED);
audioRecorder.startRecording();
}
@@ -2885,6 +2955,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public void onRecorderLocked() {
updateToggleButtonState();
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
@Override
@@ -2894,6 +2965,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
vibrator.vibrate(20);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
@@ -2948,6 +3020,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
vibrator.vibrate(50);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
@@ -3001,9 +3074,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
() -> getKeyboardImageDetails(uri),
details -> sendKeyboardImage(uri, contentType, details));
} else if (MediaUtil.isVideoType(contentType)) {
setMedia(uri, MediaType.VIDEO);
setMedia(uri, SlideFactory.MediaType.VIDEO);
} else if (MediaUtil.isAudioType(contentType)) {
setMedia(uri, MediaType.AUDIO);
setMedia(uri, SlideFactory.MediaType.AUDIO);
}
}
@@ -3060,7 +3133,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public void onReactionsDialogDismissed() {
reactionOverlay.hideMask();
reactionDelegate.hideMask();
}
// Listeners
@@ -3291,14 +3364,14 @@ public class ConversationActivity extends PassphraseRequiredActivity
@NonNull Toolbar.OnMenuItemClickListener toolbarListener,
@NonNull ConversationReactionOverlay.OnHideListener onHideListener)
{
reactionOverlay.setOnToolbarItemClickedListener(toolbarListener);
reactionOverlay.setOnHideListener(onHideListener);
reactionOverlay.show(this, maskTarget, recipient.get(), messageRecord, inputAreaHeight());
reactionDelegate.setOnToolbarItemClickedListener(toolbarListener);
reactionDelegate.setOnHideListener(onHideListener);
reactionDelegate.show(this, maskTarget, recipient.get(), messageRecord, inputAreaHeight());
}
@Override
public void onListVerticalTranslationChanged(float translationY) {
reactionOverlay.setListVerticalTranslation(translationY);
reactionDelegate.setListVerticalTranslation(translationY);
}
@Override
@@ -3318,22 +3391,22 @@ public class ConversationActivity extends PassphraseRequiredActivity
@Override
public void handleReactionDetails(@NonNull View maskTarget) {
reactionOverlay.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
reactionDelegate.showMask(maskTarget, titleView.getMeasuredHeight(), inputAreaHeight());
}
@Override
public void onCursorChanged() {
if (!reactionOverlay.isShowing()) {
if (!reactionDelegate.isShowing()) {
return;
}
SimpleTask.run(() -> {
//noinspection CodeBlock2Expr
return DatabaseFactory.getMmsSmsDatabase(this)
.checkMessageExists(reactionOverlay.getMessageRecord());
.checkMessageExists(reactionDelegate.getMessageRecord());
}, messageExists -> {
if (!messageExists) {
reactionOverlay.hide();
reactionDelegate.hide();
}
});
}
@@ -3504,7 +3577,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void sendKeyboardImage(@NonNull Uri uri, @NonNull String contentType, @Nullable KeyboardImageDetails details) {
if (details == null || !details.hasTransparency) {
setMedia(uri, Objects.requireNonNull(MediaType.from(contentType)));
setMedia(uri, Objects.requireNonNull(SlideFactory.MediaType.from(contentType)));
return;
}

View File

@@ -16,6 +16,7 @@
*/
package org.thoughtcrime.securesms.conversation;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -23,10 +24,13 @@ import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.LifecycleOwner;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.ListAdapter;
@@ -39,9 +43,11 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -72,6 +78,10 @@ public class ConversationAdapter
private static final String TAG = Log.tag(ConversationAdapter.class);
public static final int HEADER_TYPE_POPOVER_DATE = 1;
public static final int HEADER_TYPE_INLINE_DATE = 2;
public static final int HEADER_TYPE_LAST_SEEN = 3;
private static final int MESSAGE_TYPE_OUTGOING_MULTIMEDIA = 0;
private static final int MESSAGE_TYPE_OUTGOING_TEXT = 1;
private static final int MESSAGE_TYPE_INCOMING_MULTIMEDIA = 2;
@@ -101,6 +111,8 @@ public class ConversationAdapter
private View headerView;
private View footerView;
private PagingController pagingController;
private boolean hasWallpaper;
private boolean isMessageRequestAccepted;
ConversationAdapter(@NonNull LifecycleOwner lifecycleOwner,
@NonNull GlideRequests glideRequests,
@@ -122,15 +134,17 @@ public class ConversationAdapter
this.lifecycleOwner = lifecycleOwner;
this.glideRequests = glideRequests;
this.locale = locale;
this.clickListener = clickListener;
this.recipient = recipient;
this.selected = new HashSet<>();
this.fastRecords = new ArrayList<>();
this.releasedFastRecords = new HashSet<>();
this.calendar = Calendar.getInstance();
this.digest = getMessageDigestOrThrow();
this.glideRequests = glideRequests;
this.locale = locale;
this.clickListener = clickListener;
this.recipient = recipient;
this.selected = new HashSet<>();
this.fastRecords = new ArrayList<>();
this.releasedFastRecords = new HashSet<>();
this.calendar = Calendar.getInstance();
this.digest = getMessageDigestOrThrow();
this.hasWallpaper = recipient.hasWallpaper();
this.isMessageRequestAccepted = true;
setHasStableIds(true);
}
@@ -241,7 +255,9 @@ public class ConversationAdapter
selected,
recipient,
searchQuery,
conversationMessage == recordToPulse);
conversationMessage == recordToPulse,
hasWallpaper,
isMessageRequestAccepted);
if (conversationMessage == recordToPulse) {
recordToPulse = null;
@@ -288,14 +304,36 @@ public class ConversationAdapter
}
@Override
public StickyHeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
public StickyHeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position, int type) {
return new StickyHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.conversation_item_header, parent, false));
}
@Override
public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position) {
public void onBindHeaderViewHolder(StickyHeaderViewHolder viewHolder, int position, int type) {
Context context = viewHolder.itemView.getContext();
ConversationMessage conversationMessage = Objects.requireNonNull(getItem(position));
viewHolder.setText(DateUtils.getRelativeDate(viewHolder.itemView.getContext(), locale, conversationMessage.getMessageRecord().getDateReceived()));
if (type == HEADER_TYPE_POPOVER_DATE) {
if (hasWallpaper) {
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
} else {
viewHolder.setBackgroundRes(R.drawable.sticky_date_header_background);
}
} else if (type == HEADER_TYPE_INLINE_DATE) {
if (hasWallpaper) {
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
} else {
viewHolder.clearBackground();
}
}
if (hasWallpaper && ThemeUtil.isDarkTheme(context)) {
viewHolder.setTextColor(ContextCompat.getColor(context, R.color.core_grey_15));
} else {
viewHolder.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
}
}
public @Nullable ConversationMessage getItem(int position) {
@@ -325,6 +363,14 @@ public class ConversationAdapter
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));
if (hasWallpaper) {
viewHolder.setBackgroundRes(R.drawable.wallpaper_bubble_background_8);
viewHolder.setDividerColor(viewHolder.itemView.getResources().getColor(R.color.transparent_black_80));
} else {
viewHolder.clearBackground();
viewHolder.setDividerColor(viewHolder.itemView.getResources().getColor(R.color.core_grey_45));
}
}
boolean hasNoConversationMessages() {
@@ -421,6 +467,21 @@ public class ConversationAdapter
notifyDataSetChanged();
}
/**
* Lets the adapter know that the wallpaper state has changed.
* @return True if the internal wallpaper state changed, otherwise false.
*/
boolean onHasWallpaperChanged(boolean hasWallpaper) {
if (this.hasWallpaper != hasWallpaper) {
Log.d(TAG, "Resetting adapter due to wallpaper change.");
this.hasWallpaper = hasWallpaper;
notifyDataSetChanged();
return true;
} else {
return false;
}
}
/**
* Adds a record to a memory cache to allow it to be rendered immediately, as opposed to waiting
* for a database change.
@@ -537,6 +598,13 @@ public class ConversationAdapter
return getItem(position - ((hasFooter() && position == getItemCount() - 1) ? 1 : 0));
}
public void setMessageRequestAccepted(boolean messageRequestAccepted) {
if (this.isMessageRequestAccepted != messageRequestAccepted) {
this.isMessageRequestAccepted = messageRequestAccepted;
notifyDataSetChanged();
}
}
static class ConversationViewHolder extends RecyclerView.ViewHolder {
public ConversationViewHolder(final @NonNull View itemView) {
super(itemView);
@@ -549,10 +617,12 @@ public class ConversationAdapter
static class StickyHeaderViewHolder extends RecyclerView.ViewHolder {
TextView textView;
View divider;
StickyHeaderViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.text);
divider = itemView.findViewById(R.id.last_seen_divider);
}
StickyHeaderViewHolder(TextView textView) {
@@ -563,6 +633,24 @@ public class ConversationAdapter
public void setText(CharSequence text) {
textView.setText(text);
}
public void setTextColor(@ColorInt int color) {
textView.setTextColor(color);
}
public void setBackgroundRes(@DrawableRes int resId) {
textView.setBackgroundResource(resId);
}
public void setDividerColor(@ColorInt int color) {
if (divider != null) {
divider.setBackgroundColor(color);
}
}
public void clearBackground() {
textView.setBackground(null);
}
}
private static class HeaderFooterViewHolder extends RecyclerView.ViewHolder {

View File

@@ -21,6 +21,7 @@ public class ConversationBannerView extends ConstraintLayout {
private AvatarImageView contactAvatar;
private TextView contactTitle;
private TextView contactAbout;
private TextView contactSubtitle;
private TextView contactDescription;
@@ -39,6 +40,7 @@ public class ConversationBannerView extends ConstraintLayout {
contactAvatar = findViewById(R.id.message_request_avatar);
contactTitle = findViewById(R.id.message_request_title);
contactAbout = findViewById(R.id.message_request_about);
contactSubtitle = findViewById(R.id.message_request_subtitle);
contactDescription = findViewById(R.id.message_request_description);
@@ -53,6 +55,11 @@ public class ConversationBannerView extends ConstraintLayout {
contactTitle.setText(title);
}
public void setAbout(@Nullable String about) {
contactAbout.setText(about);
contactAbout.setVisibility(TextUtils.isEmpty(about) ? GONE : VISIBLE);
}
public void setSubtitle(@Nullable CharSequence subtitle) {
contactSubtitle.setText(subtitle);
contactSubtitle.setVisibility(TextUtils.isEmpty(subtitle) ? GONE : VISIBLE);
@@ -62,6 +69,14 @@ public class ConversationBannerView extends ConstraintLayout {
contactDescription.setText(description);
}
public void showBackgroundBubble(boolean enabled) {
if (enabled) {
setBackgroundResource(R.drawable.wallpaper_bubble_background_12);
} else {
setBackground(null);
}
}
public void hideSubtitle() {
contactSubtitle.setVisibility(View.GONE);
}

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

@@ -23,6 +23,8 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@@ -35,6 +37,7 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
@@ -66,9 +69,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 +98,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;
@@ -101,6 +107,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.longmessage.LongMessageActivity;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.messagedetails.MessageDetailsActivity;
import org.thoughtcrime.securesms.messagerequests.MessageRequestState;
import org.thoughtcrime.securesms.messagerequests.MessageRequestViewModel;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
@@ -121,13 +128,13 @@ 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;
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SetUtil;
import org.thoughtcrime.securesms.util.SignalProxyUtil;
import org.thoughtcrime.securesms.util.SnapToTopDataObserver;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.StorageUtil;
@@ -139,6 +146,7 @@ import org.thoughtcrime.securesms.util.WindowUtil;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.views.AdaptiveActionsToolbar;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
@@ -147,9 +155,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();
@@ -169,7 +177,7 @@ public class ConversationFragment extends LoggingFragment {
private Locale locale;
private RecyclerView list;
private RecyclerView.ItemDecoration lastSeenDecoration;
private RecyclerView.ItemDecoration stickyHeaderDecoration;
private RecyclerView.ItemDecoration inlineDateDecoration;
private ViewSwitcher topLoadMoreView;
private ViewSwitcher bottomLoadMoreView;
private ConversationTypingView typingView;
@@ -191,6 +199,7 @@ public class ConversationFragment extends LoggingFragment {
private OnScrollListener conversationScrollListener;
private int pulsePosition = -1;
private VoiceNoteMediaController voiceNoteMediaController;
private View toolbarShadow;
public static void prepare(@NonNull Context context) {
FrameLayout parent = new FrameLayout(context);
@@ -220,6 +229,7 @@ public class ConversationFragment extends LoggingFragment {
scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
scrollDateHeader = view.findViewById(R.id.scroll_date_header);
emptyConversationBanner = view.findViewById(R.id.empty_conversation_banner);
toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow);
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
list.setHasFixedSize(false);
@@ -250,8 +260,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);
@@ -275,6 +288,8 @@ public class ConversationFragment extends LoggingFragment {
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
scrollToMentionButton.setOnClickListener(v -> scrollToNextMention());
updateToolbarDependentMargins();
return view;
}
@@ -303,7 +318,9 @@ public class ConversationFragment extends LoggingFragment {
list.setTranslationY(Math.min(0, -chTop));
list.setOverScrollMode(RecyclerView.OVER_SCROLL_NEVER);
}
listener.onListVerticalTranslationChanged(list.getTranslationY());
int offset = WindowUtil.isStatusBarPresent(requireActivity().getWindow()) ? ViewUtil.getStatusBarHeight(list) : 0;
listener.onListVerticalTranslationChanged(list.getTranslationY() - offset);
}
@Override
@@ -327,6 +344,7 @@ public class ConversationFragment extends LoggingFragment {
public void onStart() {
super.onStart();
initializeTypingObserver();
SignalProxyUtil.startListeningToWebsocket();
}
@Override
@@ -352,6 +370,12 @@ public class ConversationFragment extends LoggingFragment {
ApplicationDependencies.getTypingStatusRepository().getTypists(threadId).removeObservers(this);
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
updateToolbarDependentMargins();
}
public void onNewIntent() {
if (actionMode != null) {
actionMode.finish();
@@ -385,6 +409,20 @@ public class ConversationFragment extends LoggingFragment {
snapToTopDataObserver.requestScrollPosition(position);
}
public void onWallpaperChanged(@Nullable ChatWallpaper wallpaper) {
if (list != null) {
ConversationAdapter adapter = getListAdapter();
if (adapter != null) {
Log.d(TAG, "Notifying adapter that wallpaper state has changed.");
if (adapter.onHasWallpaperChanged(wallpaper != null)) {
setInlineDateDecoration(adapter);
}
}
}
}
private int getStartPosition() {
return conversationViewModel.getArgs().getStartingPosition();
}
@@ -401,10 +439,16 @@ public class ConversationFragment extends LoggingFragment {
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationBanner);
presentMessageRequestProfileView(requireContext(), recipientInfo, emptyConversationBanner);
});
messageRequestViewModel.getMessageData().observe(getViewLifecycleOwner(), data -> {
ConversationAdapter adapter = getListAdapter();
if (adapter != null) {
adapter.setMessageRequestAccepted(data.getMessageState() == MessageRequestState.NONE);
}
});
}
private static void presentMessageRequestProfileView(@NonNull Context context, @NonNull MessageRequestViewModel.RecipientInfo recipientInfo, @Nullable ConversationBannerView conversationBanner) {
if (conversationBanner == null) {
return;
}
@@ -417,9 +461,11 @@ public class ConversationFragment extends LoggingFragment {
if (recipient != null) {
conversationBanner.setAvatar(GlideApp.with(context), recipient);
conversationBanner.showBackgroundBubble(recipient.hasWallpaper());
String title = isSelf ? context.getString(R.string.note_to_self) : recipient.getDisplayNameOrUsername(context);
conversationBanner.setTitle(title);
conversationBanner.setAbout(recipient.getCombinedAboutAndEmoji());
if (recipient.isGroup()) {
if (pendingMemberCount > 0) {
@@ -482,7 +528,7 @@ public class ConversationFragment extends LoggingFragment {
this.threadId = conversationViewModel.getArgs().getThreadId();
this.markReadHelper = new MarkReadHelper(threadId, requireContext());
conversationViewModel.onConversationDataAvailable(threadId, startingPosition);
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, startingPosition);
messageCountsViewModel.setThreadId(threadId);
messageCountsViewModel.getUnreadMessagesCount().observe(getViewLifecycleOwner(), scrollToBottomButton::setUnreadCount);
@@ -493,6 +539,7 @@ public class ConversationFragment extends LoggingFragment {
conversationScrollListener = new ConversationScrollListener(requireContext());
list.addOnScrollListener(conversationScrollListener);
list.addOnScrollListener(new ShadowScrollListener());
if (oldThreadId != threadId) {
ApplicationDependencies.getTypingStatusRepository().getTypists(oldThreadId).removeObservers(this);
@@ -505,7 +552,7 @@ public class ConversationFragment extends LoggingFragment {
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get());
adapter.setPagingController(conversationViewModel.getPagingController());
list.setAdapter(adapter);
setStickyHeaderDecoration(adapter);
setInlineDateDecoration(adapter);
ConversationAdapter.initializePool(list.getRecycledViewPool());
adapter.registerAdapterDataObserver(snapToTopDataObserver);
@@ -515,6 +562,7 @@ public class ConversationFragment extends LoggingFragment {
emptyConversationBanner.setVisibility(View.GONE);
} else if (threadId == -1) {
emptyConversationBanner.setVisibility(View.VISIBLE);
toolbarShadow.setVisibility(View.GONE);
}
}
@@ -609,7 +657,7 @@ public class ConversationFragment extends LoggingFragment {
menu.findItem(R.id.menu_context_copy).setVisible(menuState.shouldShowCopyAction());
}
private ConversationAdapter getListAdapter() {
private @Nullable ConversationAdapter getListAdapter() {
return (ConversationAdapter) list.getAdapter();
}
@@ -632,7 +680,7 @@ public class ConversationFragment extends LoggingFragment {
messageRequestViewModel.setConversationInfo(recipient.getId(), threadId);
snapToTopDataObserver.requestScrollPosition(0);
conversationViewModel.onConversationDataAvailable(threadId, -1);
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, -1);
messageCountsViewModel.setThreadId(threadId);
initializeListAdapter();
}
@@ -646,13 +694,13 @@ public class ConversationFragment extends LoggingFragment {
}
}
public void setStickyHeaderDecoration(@NonNull ConversationAdapter adapter) {
if (stickyHeaderDecoration != null) {
list.removeItemDecoration(stickyHeaderDecoration);
public void setInlineDateDecoration(@NonNull ConversationAdapter adapter) {
if (inlineDateDecoration != null) {
list.removeItemDecoration(inlineDateDecoration);
}
stickyHeaderDecoration = new StickyHeaderDecoration(adapter, false, false);
list.addItemDecoration(stickyHeaderDecoration);
inlineDateDecoration = new StickyHeaderDecoration(adapter, false, false, ConversationAdapter.HEADER_TYPE_INLINE_DATE);
list.addItemDecoration(inlineDateDecoration);
}
public void setLastSeen(long lastSeen) {
@@ -1036,8 +1084,8 @@ public class ConversationFragment extends LoggingFragment {
private void maybeShowSwipeToReplyTooltip() {
if (!TextSecurePreferences.hasSeenSwipeToReplyTooltip(requireContext())) {
int text = getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR ? R.string.ConversationFragment_you_can_swipe_to_the_right_reply
: R.string.ConversationFragment_you_can_swipe_to_the_left_reply;
int text = ViewUtil.isLtr(requireContext()) ? R.string.ConversationFragment_you_can_swipe_to_the_right_reply
: R.string.ConversationFragment_you_can_swipe_to_the_left_reply;
TooltipPopup.forTarget(requireActivity().findViewById(R.id.menu_context_reply))
.setText(text)
.setTextColor(getResources().getColor(R.color.core_white))
@@ -1094,6 +1142,22 @@ public class ConversationFragment extends LoggingFragment {
}
}
private void updateToolbarDependentMargins() {
Toolbar toolbar = requireActivity().findViewById(R.id.toolbar);
toolbar.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
Rect rect = new Rect();
toolbar.getGlobalVisibleRect(rect);
ViewUtil.setTopMargin(scrollDateHeader, rect.bottom + ViewUtil.dpToPx(8));
ViewUtil.setTopMargin(conversationBanner, rect.bottom + ViewUtil.dpToPx(16));
ViewUtil.setTopMargin(emptyConversationBanner, rect.bottom + ViewUtil.dpToPx(16));
toolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
public interface ConversationFragmentListener {
void setThreadId(long threadId);
void handleReplyMessage(ConversationMessage conversationMessage);
@@ -1174,7 +1238,7 @@ public class ConversationFragment extends LoggingFragment {
private void bindScrollHeader(StickyHeaderViewHolder headerViewHolder, int positionId) {
if (((ConversationAdapter)list.getAdapter()).getHeaderId(positionId) != -1) {
((ConversationAdapter) list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId);
((ConversationAdapter) list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId, ConversationAdapter.HEADER_TYPE_POPOVER_DATE);
}
}
}
@@ -1216,6 +1280,7 @@ public class ConversationFragment extends LoggingFragment {
listener.handleReaction(maskTarget, messageRecord, new ReactionsToolbarListener(conversationMessage), () -> {
isReacting = false;
list.setLayoutFrozen(false);
WindowUtil.setLightStatusBarFromTheme(requireActivity());
});
} else {
((ConversationAdapter) list.getAdapter()).toggleSelection(conversationMessage);
@@ -1405,7 +1470,8 @@ public class ConversationFragment extends LoggingFragment {
@Override
public boolean onUrlClicked(@NonNull String url) {
return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url);
return CommunicationActions.handlePotentialGroupLinkUrl(requireActivity(), url) ||
CommunicationActions.handlePotentialProxyLinkUrl(requireActivity(), url);
}
@Override
@@ -1413,10 +1479,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
@@ -1518,7 +1637,7 @@ public class ConversationFragment extends LoggingFragment {
mode.setTitle("1");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= 21) {
Window window = getActivity().getWindow();
statusBarColor = window.getStatusBarColor();
WindowUtil.setStatusBarColor(window, getResources().getColor(R.color.action_mode_status_bar));
@@ -1544,11 +1663,11 @@ public class ConversationFragment extends LoggingFragment {
((ConversationAdapter)list.getAdapter()).clearSelection();
list.getAdapter().notifyDataSetChanged();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (Build.VERSION.SDK_INT >= 21) {
WindowUtil.setStatusBarColor(requireActivity().getWindow(), statusBarColor);
}
WindowUtil.clearLightStatusBar(getActivity().getWindow());
WindowUtil.setLightStatusBarFromTheme(requireActivity());
actionMode = null;
}
@@ -1635,4 +1754,18 @@ public class ConversationFragment extends LoggingFragment {
}
}
private class ShadowScrollListener extends RecyclerView.OnScrollListener {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (recyclerView.canScrollVertically(-1)) {
if (toolbarShadow.getVisibility() != View.VISIBLE) {
ViewUtil.fadeIn(toolbarShadow, 250);
}
} else {
if (toolbarShadow.getVisibility() != View.GONE) {
ViewUtil.fadeOut(toolbarShadow, 250);
}
}
}
}
}

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

@@ -9,8 +9,10 @@ import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mediasend.Media;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper;
import java.util.ArrayList;
import java.util.Collection;
@@ -18,15 +20,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 +66,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 +78,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 +89,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 +100,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 +145,15 @@ public class ConversationIntents {
public boolean isBorderless() {
return isBorderless;
}
public boolean isFirstTimeInSelfCreatedGroup() {
return firstTimeInSelfCreatedGroup;
}
public @Nullable ChatWallpaper getWallpaper() {
// TODO [greyson][wallpaper] Is it worth it to do this beforehand?
return Recipient.resolved(recipientId).getWallpaper();
}
}
public final static class Builder {
@@ -153,6 +170,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 +230,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 +258,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

@@ -48,6 +48,7 @@ import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.ColorInt;
import androidx.annotation.DimenRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -155,17 +156,17 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private boolean groupThread;
private LiveRecipient recipient;
private GlideRequests glideRequests;
private ValueAnimator pulseOutlinerAlphaAnimator;
private ValueAnimator pulseOutlinerAlphaAnimator;
protected ConversationItemBodyBubble bodyBubble;
protected View reply;
protected View replyIcon;
@Nullable protected ViewGroup contactPhotoHolder;
@Nullable private QuoteView quoteView;
private EmojiTextView bodyText;
private ConversationItemFooter footer;
private ConversationItemFooter stickerFooter;
@Nullable private TextView groupSender;
@Nullable private TextView groupSenderProfileName;
@Nullable private View groupSenderHolder;
private AvatarImageView contactPhoto;
private AlertView alertView;
@@ -186,6 +187,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private @Nullable EventListener eventListener;
private int defaultBubbleColor;
private int defaultBubbleColorForWallpaper;
private int measureCalls;
private final PassthroughClickListener passthroughClickListener = new PassthroughClickListener();
@@ -223,7 +225,6 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.footer = findViewById(R.id.conversation_item_footer);
this.stickerFooter = findViewById(R.id.conversation_item_sticker_footer);
this.groupSender = findViewById(R.id.group_message_sender);
this.groupSenderProfileName = findViewById(R.id.group_message_sender_profile);
this.alertView = findViewById(R.id.indicators_parent);
this.contactPhoto = findViewById(R.id.contact_photo);
this.contactPhotoHolder = findViewById(R.id.contact_photo_container);
@@ -237,7 +238,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub));
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
this.quoteView = findViewById(R.id.quote_view);
this.reply = findViewById(R.id.reply_icon);
this.reply = findViewById(R.id.reply_icon_wrapper);
this.replyIcon = findViewById(R.id.reply_icon);
this.reactionsView = findViewById(R.id.reactions_view);
setOnClickListener(new ClickListener(null));
@@ -256,7 +258,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@NonNull Set<ConversationMessage> batchSelected,
@NonNull Recipient conversationRecipient,
@Nullable String searchQuery,
boolean pulse)
boolean pulse,
boolean hasWallpaper,
boolean isMessageRequestAccepted)
{
if (this.recipient != null) this.recipient.removeForeverObserver(this);
if (this.conversationRecipient != null) this.conversationRecipient.removeForeverObserver(this);
@@ -277,19 +281,19 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
setGutterSizes(messageRecord, groupThread);
setMessageShape(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, conversationRecipient, groupThread);
setBodyText(messageRecord, searchQuery);
setBubbleState(messageRecord);
setMediaAttributes(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper, isMessageRequestAccepted);
setBodyText(messageRecord, searchQuery, isMessageRequestAccepted);
setBubbleState(messageRecord, hasWallpaper);
setInteractionState(conversationMessage, pulse);
setStatusIcons(messageRecord);
setStatusIcons(messageRecord, hasWallpaper);
setContactPhoto(recipient.get());
setGroupMessageStatus(messageRecord, recipient.get());
setGroupAuthorColor(messageRecord);
setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setGroupAuthorColor(messageRecord, hasWallpaper);
setAuthor(messageRecord, previousMessageRecord, nextMessageRecord, groupThread, hasWallpaper);
setQuote(messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setMessageSpacing(context, messageRecord, previousMessageRecord, nextMessageRecord, groupThread);
setReactions(messageRecord);
setFooter(messageRecord, nextMessageRecord, locale, groupThread);
setFooter(messageRecord, nextMessageRecord, locale, groupThread, hasWallpaper);
}
@Override
@@ -344,12 +348,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
}
ConversationItemFooter activeFooter = getActiveFooter(messageRecord);
int availableWidth = getAvailableMessageBubbleWidth(footer);
if (!hasNoBubble(messageRecord)) {
ConversationItemFooter activeFooter = getActiveFooter(messageRecord);
int availableWidth = getAvailableMessageBubbleWidth(footer);
if (activeFooter.getVisibility() != GONE && activeFooter.getMeasuredWidth() != availableWidth) {
activeFooter.getLayoutParams().width = availableWidth;
needsMeasure = true;
if (activeFooter.getVisibility() != GONE && activeFooter.getMeasuredWidth() != availableWidth) {
activeFooter.getLayoutParams().width = availableWidth;
needsMeasure = true;
}
}
if (needsMeasure) {
@@ -366,7 +372,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@Override
public void onRecipientChanged(@NonNull Recipient modified) {
setBubbleState(messageRecord);
setBubbleState(messageRecord, modified.hasWallpaper());
if (recipient.getId().equals(modified.getId())) {
setContactPhoto(modified);
setGroupMessageStatus(messageRecord, modified);
@@ -389,7 +395,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
private void initializeAttributes() {
defaultBubbleColor = ContextCompat.getColor(context, R.color.signal_background_secondary);
defaultBubbleColor = ContextCompat.getColor(context, R.color.signal_background_secondary);
defaultBubbleColorForWallpaper = ContextCompat.getColor(context, R.color.conversation_item_wallpaper_bubble_color);
}
private @ColorInt int getDefaultBubbleColor(boolean hasWallpaper) {
return hasWallpaper ? defaultBubbleColorForWallpaper : defaultBubbleColor;
}
@Override
@@ -409,16 +420,20 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
/// MessageRecord Attribute Parsers
private void setBubbleState(MessageRecord messageRecord) {
private void setBubbleState(MessageRecord messageRecord, boolean hasWallpaper) {
if (messageRecord.isOutgoing() && !messageRecord.isRemoteDelete()) {
bodyBubble.getBackground().setColorFilter(defaultBubbleColor, PorterDuff.Mode.MULTIPLY);
bodyBubble.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.MULTIPLY);
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
footer.setOnlyShowSendingStatus(false, messageRecord);
} else if (messageRecord.isRemoteDelete() || (isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord))) {
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.signal_background_primary), PorterDuff.Mode.MULTIPLY);
if (hasWallpaper) {
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.wallpaper_bubble_color), PorterDuff.Mode.SRC_IN);
} else {
bodyBubble.getBackground().setColorFilter(ContextCompat.getColor(context, R.color.signal_background_primary), PorterDuff.Mode.MULTIPLY);
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
}
footer.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
footer.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord);
} else {
bodyBubble.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
@@ -433,7 +448,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
pulseOutliner.setStrokeWidth(ViewUtil.dpToPx(4));
outliners.clear();
if (shouldDrawBodyBubbleOutline(messageRecord)) {
if (shouldDrawBodyBubbleOutline(messageRecord, hasWallpaper)) {
outliners.add(outliner);
}
outliners.add(pulseOutliner);
@@ -447,6 +462,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
if (audioViewStub.resolved()) {
setAudioViewTint(messageRecord);
}
if (hasWallpaper) {
replyIcon.setBackgroundResource(R.drawable.wallpaper_message_decoration_background);
} else {
replyIcon.setBackground(null);
}
}
private void setAudioViewTint(MessageRecord messageRecord) {
@@ -515,9 +536,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
pulseOutliner.setAlpha(0);
}
private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord) {
boolean isIncomingViewedOnce = !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord);
return isIncomingViewedOnce || messageRecord.isRemoteDelete();
private boolean shouldDrawBodyBubbleOutline(MessageRecord messageRecord, boolean hasWallpaper) {
if (hasWallpaper) {
return false;
} else {
boolean isIncomingViewedOnce = !messageRecord.isOutgoing() && isViewOnceMessage(messageRecord) && ViewOnceUtil.isViewed((MmsMessageRecord) messageRecord);
return isIncomingViewedOnce || messageRecord.isRemoteDelete();
}
}
private boolean isCaptionlessMms(MessageRecord messageRecord) {
@@ -543,6 +568,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
((MmsMessageRecord)messageRecord).getSlideDeck().getThumbnailSlide().isBorderless();
}
private boolean hasNoBubble(MessageRecord messageRecord) {
return hasSticker(messageRecord) || isBorderless(messageRecord);
}
private boolean hasOnlyThumbnail(MessageRecord messageRecord) {
return hasThumbnail(messageRecord) &&
!hasAudio(messageRecord) &&
@@ -599,7 +628,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
}
private void setBodyText(@NonNull MessageRecord messageRecord,
@Nullable String searchQuery)
@Nullable String searchQuery,
boolean messageRequestAccepted)
{
bodyText.setClickable(false);
bodyText.setFocusable(false);
@@ -621,7 +651,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
} else if (isCaptionlessMms(messageRecord)) {
bodyText.setVisibility(View.GONE);
} else {
Spannable styledText = linkifyMessageBody(conversationMessage.getDisplayBody(getContext()), batchSelected.isEmpty());
Spannable styledText = conversationMessage.getDisplayBody(getContext());
if (messageRequestAccepted) {
linkifyMessageBody(styledText, batchSelected.isEmpty());
}
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery);
@@ -645,8 +678,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private void setMediaAttributes(@NonNull MessageRecord messageRecord,
@NonNull Optional<MessageRecord> previousRecord,
@NonNull Optional<MessageRecord> nextRecord,
@NonNull Recipient conversationRecipient,
boolean isGroupThread)
boolean isGroupThread,
boolean hasWallpaper,
boolean messageRequestAccepted)
{
boolean showControls = !messageRecord.isFailed();
@@ -688,7 +722,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
footer.setVisibility(GONE);
} else if (hasLinkPreview(messageRecord)) {
} else if (hasLinkPreview(messageRecord) && messageRequestAccepted) {
linkPreviewStub.get().setVisibility(View.VISIBLE);
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(View.GONE);
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.get().setVisibility(View.GONE);
@@ -821,7 +855,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
mediaThumbnailStub.get().setOnLongClickListener(passthroughClickListener);
mediaThumbnailStub.get().setOnClickListener(passthroughClickListener);
mediaThumbnailStub.get().showShade(TextUtils.isEmpty(messageRecord.getDisplayBody(getContext())) && !hasExtraText(messageRecord));
mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? defaultBubbleColor
mediaThumbnailStub.get().setConversationColor(messageRecord.isOutgoing() ? getDefaultBubbleColor(hasWallpaper)
: messageRecord.getRecipient().getColor().toConversationColor(context));
mediaThumbnailStub.get().setBorderless(false);
@@ -953,8 +987,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
contactPhoto.setAvatar(glideRequests, recipient, false);
}
private SpannableString linkifyMessageBody(@NonNull SpannableString messageBody,
boolean shouldLinkifyAllLinks)
private void linkifyMessageBody(@NonNull Spannable messageBody,
boolean shouldLinkifyAllLinks)
{
int linkPattern = Linkify.WEB_URLS | Linkify.EMAIL_ADDRESSES | Linkify.PHONE_NUMBERS;
boolean hasLinks = Linkify.addLinks(messageBody, shouldLinkifyAllLinks ? linkPattern : 0);
@@ -978,11 +1012,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
for (Annotation annotation : mentionAnnotations) {
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return messageBody;
}
private void setStatusIcons(MessageRecord messageRecord) {
private void setStatusIcons(MessageRecord messageRecord, boolean hasWallpaper) {
bodyText.setCompoundDrawablesWithIntrinsicBounds(0, 0, messageRecord.isKeyExchange() ? R.drawable.ic_menu_login : 0, 0);
if (messageRecord.isFailed()) {
@@ -992,6 +1024,12 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
} else {
alertView.setNone();
}
if (hasWallpaper) {
alertView.setBackgroundResource(R.drawable.wallpaper_message_decoration_background);
} else {
alertView.setBackground(null);
}
}
private void setQuote(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
@@ -1047,9 +1085,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private void setGutterSizes(@NonNull MessageRecord current, boolean isGroupThread) {
if (isGroupThread && current.isOutgoing()) {
ViewUtil.setLeftMargin(this, readDimen(R.dimen.conversation_group_left_gutter));
ViewUtil.setPaddingStart(this, readDimen(R.dimen.conversation_group_left_gutter));
ViewUtil.setPaddingEnd(this, readDimen(R.dimen.conversation_individual_right_gutter));
} else if (current.isOutgoing()) {
ViewUtil.setLeftMargin(this, readDimen(R.dimen.conversation_individual_left_gutter));
ViewUtil.setPaddingStart(this, readDimen(R.dimen.conversation_individual_left_gutter));
ViewUtil.setPaddingEnd(this, readDimen(R.dimen.conversation_individual_right_gutter));
}
}
@@ -1074,7 +1114,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
});
}
private void setFooter(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, @NonNull Locale locale, boolean isGroupThread) {
private void setFooter(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, @NonNull Locale locale, boolean isGroupThread, boolean hasWallpaper) {
ViewUtil.updateLayoutParams(footer, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
footer.setVisibility(GONE);
@@ -1090,11 +1130,27 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
ConversationItemFooter activeFooter = getActiveFooter(current);
activeFooter.setVisibility(VISIBLE);
activeFooter.setMessageRecord(current, locale);
if (hasWallpaper && hasNoBubble((messageRecord))) {
if (messageRecord.isOutgoing()) {
activeFooter.enableBubbleBackground(R.drawable.wallpaper_bubble_background_tintable_11, getDefaultBubbleColor(hasWallpaper));
} else {
activeFooter.enableBubbleBackground(R.drawable.wallpaper_bubble_background_tintable_11, messageRecord.getRecipient().getColor().toConversationColor(context));
activeFooter.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
activeFooter.setIconColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_secondary_color));
}
} else if (hasNoBubble(messageRecord)){
activeFooter.disableBubbleBackground();
activeFooter.setTextColor(ContextCompat.getColor(context, R.color.signal_text_secondary));
activeFooter.setIconColor(ContextCompat.getColor(context, R.color.signal_icon_tint_secondary));
} else {
activeFooter.disableBubbleBackground();
}
}
}
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
if (hasNoBubble(messageRecord)) {
return stickerFooter;
} else if (hasSharedContact(messageRecord) && TextUtils.isEmpty(messageRecord.getDisplayBody(getContext()))) {
return sharedContactStub.get().getFooter();
@@ -1118,30 +1174,27 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
@SuppressLint("SetTextI18n")
private void setGroupMessageStatus(MessageRecord messageRecord, Recipient recipient) {
if (groupThread && !messageRecord.isOutgoing() && groupSender != null && groupSenderProfileName != null) {
if (groupThread && !messageRecord.isOutgoing() && groupSender != null) {
groupSender.setText(recipient.getDisplayName(getContext()));
groupSenderProfileName.setVisibility(View.GONE);
}
}
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord) {
if (groupSender != null && groupSenderProfileName != null) {
private void setGroupAuthorColor(@NonNull MessageRecord messageRecord, boolean hasWallpaper) {
if (groupSender != null) {
int stickerAuthorColor = ContextCompat.getColor(context, R.color.signal_text_primary);
if (shouldDrawBodyBubbleOutline(messageRecord)) {
if (shouldDrawBodyBubbleOutline(messageRecord, false)) {
groupSender.setTextColor(stickerAuthorColor);
groupSenderProfileName.setTextColor(stickerAuthorColor);
} else if (hasSticker(messageRecord) || isBorderless(messageRecord)) {
} else if (!hasWallpaper && hasNoBubble(messageRecord)) {
groupSender.setTextColor(stickerAuthorColor);
groupSenderProfileName.setTextColor(stickerAuthorColor);
} else {
groupSender.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_primary_color));
groupSenderProfileName.setTextColor(ContextCompat.getColor(context, R.color.conversation_item_received_text_primary_color));
}
}
}
@SuppressWarnings("ConstantConditions")
private void setAuthor(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
private void setAuthor(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> previous, @NonNull Optional<MessageRecord> next, boolean isGroupThread, boolean hasWallpaper) {
if (isGroupThread && !current.isOutgoing()) {
contactPhotoHolder.setVisibility(VISIBLE);
@@ -1149,6 +1202,13 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
!DateUtils.isSameDay(previous.get().getTimestamp(), current.getTimestamp()))
{
groupSenderHolder.setVisibility(VISIBLE);
if (hasWallpaper && hasNoBubble(current)) {
groupSenderHolder.setBackgroundResource(R.drawable.wallpaper_bubble_background_tintable_11);
groupSenderHolder.getBackground().setColorFilter(messageRecord.getRecipient().getColor().toConversationColor(context), PorterDuff.Mode.MULTIPLY);
} else {
groupSenderHolder.setBackground(null);
}
} else {
groupSenderHolder.setVisibility(GONE);
}

View File

@@ -0,0 +1,126 @@
package org.thoughtcrime.securesms.conversation;
import android.app.Activity;
import android.graphics.PointF;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.views.Stub;
/**
* Delegate class that mimics the ConversationReactionOverlay public API
*
* This allows us to properly stub out the ConversationReactionOverlay View class while still
* respecting listeners and other positional information that can be set BEFORE we want to actually
* resolve the view.
*/
final class ConversationReactionDelegate {
private final Stub<ConversationReactionOverlay> overlayStub;
private final PointF lastSeenDownPoint = new PointF();
private ConversationReactionOverlay.OnReactionSelectedListener onReactionSelectedListener;
private Toolbar.OnMenuItemClickListener onToolbarItemClickedListener;
private ConversationReactionOverlay.OnHideListener onHideListener;
private float translationY;
ConversationReactionDelegate(@NonNull Stub<ConversationReactionOverlay> overlayStub) {
this.overlayStub = overlayStub;
}
boolean isShowing() {
return overlayStub.resolved() && overlayStub.get().isShowing();
}
void show(@NonNull Activity activity,
@NonNull View maskTarget,
@NonNull Recipient conversationRecipient,
@NonNull MessageRecord messageRecord,
int maskPaddingBottom)
{
resolveOverlay().show(activity, maskTarget, conversationRecipient, messageRecord, maskPaddingBottom, lastSeenDownPoint);
}
void showMask(@NonNull View maskTarget, int maskPaddingTop, int maskPaddingBottom) {
resolveOverlay().showMask(maskTarget, maskPaddingTop, maskPaddingBottom);
}
void hide() {
overlayStub.get().hide();
}
void hideAllButMask() {
overlayStub.get().hideAllButMask();
}
void hideMask() {
overlayStub.get().hideMask();
}
void setOnReactionSelectedListener(@NonNull ConversationReactionOverlay.OnReactionSelectedListener onReactionSelectedListener) {
this.onReactionSelectedListener = onReactionSelectedListener;
if (overlayStub.resolved()) {
overlayStub.get().setOnReactionSelectedListener(onReactionSelectedListener);
}
}
void setOnToolbarItemClickedListener(@NonNull Toolbar.OnMenuItemClickListener onToolbarItemClickedListener) {
this.onToolbarItemClickedListener = onToolbarItemClickedListener;
if (overlayStub.resolved()) {
overlayStub.get().setOnToolbarItemClickedListener(onToolbarItemClickedListener);
}
}
void setOnHideListener(@NonNull ConversationReactionOverlay.OnHideListener onHideListener) {
this.onHideListener = onHideListener;
if (overlayStub.resolved()) {
overlayStub.get().setOnHideListener(onHideListener);
}
}
void setListVerticalTranslation(float translationY) {
this.translationY = translationY;
if (overlayStub.resolved()) {
overlayStub.get().setListVerticalTranslation(translationY);
}
}
@NonNull MessageRecord getMessageRecord() {
if (!overlayStub.resolved()) {
throw new IllegalStateException("Cannot call getMessageRecord right now.");
}
return overlayStub.get().getMessageRecord();
}
boolean applyTouchEvent(@NonNull MotionEvent motionEvent) {
if (!overlayStub.resolved() || !overlayStub.get().isShowing()) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
lastSeenDownPoint.set(motionEvent.getX(), motionEvent.getY());
}
return false;
} else {
return overlayStub.get().applyTouchEvent(motionEvent);
}
}
private @NonNull ConversationReactionOverlay resolveOverlay() {
ConversationReactionOverlay overlay = overlayStub.get();
overlay.setListVerticalTranslation(translationY);
overlay.setOnHideListener(onHideListener);
overlay.setOnToolbarItemClickedListener(onToolbarItemClickedListener);
overlay.setOnReactionSelectedListener(onReactionSelectedListener);
return overlay;
}
}

View File

@@ -56,7 +56,6 @@ public final class ConversationReactionOverlay extends RelativeLayout {
private final Boundary horizontalEmojiBoundary = new Boundary();
private final Boundary verticalScrubBoundary = new Boundary();
private final PointF deadzoneTouchPoint = new PointF();
private final PointF lastSeenDownPoint = new PointF();
private Activity activity;
private Recipient conversationRecipient;
@@ -149,7 +148,8 @@ public final class ConversationReactionOverlay extends RelativeLayout {
@NonNull View maskTarget,
@NonNull Recipient conversationRecipient,
@NonNull MessageRecord messageRecord,
int maskPaddingBottom)
int maskPaddingBottom,
@NonNull PointF lastSeenDownPoint)
{
if (overlayState != OverlayState.HIDDEN) {
@@ -176,10 +176,10 @@ public final class ConversationReactionOverlay extends RelativeLayout {
final float halfWidth = scrubberWidth / 2f + scrubberHorizontalMargin;
final float screenWidth = getResources().getDisplayMetrics().widthPixels;
final float downX = getLayoutDirection() == LAYOUT_DIRECTION_LTR ? lastSeenDownPoint.x : screenWidth - lastSeenDownPoint.x;
final float downX = ViewUtil.isLtr(this) ? lastSeenDownPoint.x : screenWidth - lastSeenDownPoint.x;
final float scrubberTranslationX = Util.clamp(downX - halfWidth,
scrubberHorizontalMargin,
screenWidth + scrubberHorizontalMargin - halfWidth * 2) * (getLayoutDirection() == LAYOUT_DIRECTION_LTR ? 1 : -1);
screenWidth + scrubberHorizontalMargin - halfWidth * 2) * (ViewUtil.isLtr(this) ? 1 : -1);
backgroundView.setTranslationX(scrubberTranslationX);
backgroundView.setTranslationY(scrubberTranslationY);
@@ -275,7 +275,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
}
private int getStart(@NonNull Rect rect) {
if (getLayoutDirection() == LAYOUT_DIRECTION_LTR) {
if (ViewUtil.isLtr(this)) {
return rect.left;
} else {
return rect.right;
@@ -283,7 +283,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
}
private int getEnd(@NonNull Rect rect) {
if (getLayoutDirection() == LAYOUT_DIRECTION_LTR) {
if (ViewUtil.isLtr(this)) {
return rect.right;
} else {
return rect.left;
@@ -292,10 +292,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
public boolean applyTouchEvent(@NonNull MotionEvent motionEvent) {
if (!isShowing()) {
if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
lastSeenDownPoint.set(motionEvent.getX(), motionEvent.getY());
}
return false;
throw new IllegalStateException("Touch events should only be propagated to this method if we are displaying the scrubber.");
}
if ((motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) != 0) {

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

@@ -92,7 +92,7 @@ public class ConversationTitleView extends RelativeLayout {
}
title.setCompoundDrawablesRelativeWithIntrinsicBounds(startDrawable, 0, endDrawable, 0);
TextViewCompat.setCompoundDrawableTintList(title, ColorStateList.valueOf(ContextCompat.getColor(getContext(), R.color.transparent_white_90)));
TextViewCompat.setCompoundDrawableTintList(title, ColorStateList.valueOf(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_80)));
if (recipient != null) {
this.avatar.setAvatar(glideRequests, recipient, false);

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