Compare commits

...

1005 Commits

Author SHA1 Message Date
Greyson Parrelli
69e1146e2c Bump version to 7.18.2 2024-09-25 23:58:17 -04:00
Greyson Parrelli
a0c7b56ab4 Update translations and other static files. 2024-09-25 23:57:55 -04:00
Greyson Parrelli
6b7ea28e8f Fix issue where wallpapers don't immediately render after upgrade. 2024-09-24 14:16:34 -04:00
Greyson Parrelli
6f1949db98 Bump version to 7.18.1 2024-09-24 13:37:20 -04:00
Greyson Parrelli
551d873a1a Update translations and other static files. 2024-09-24 13:36:51 -04:00
Greyson Parrelli
760d5ab2ce Be even more cautious when repairing FTS tables. 2024-09-24 12:51:56 -04:00
Greyson Parrelli
ff4364586b Fix issue with attachments failing to download. 2024-09-24 12:41:27 -04:00
Greyson Parrelli
12b78336c6 Bump version to 7.18.0 2024-09-23 22:53:28 -04:00
Greyson Parrelli
70d6a8f1fe Update baseline profile. 2024-09-23 22:52:56 -04:00
Greyson Parrelli
2e81d717d0 Update translations and other static files. 2024-09-23 22:36:41 -04:00
Greyson Parrelli
6ddd780e0e Fix wire gradle dependency. 2024-09-23 22:36:34 -04:00
Greyson Parrelli
2449b5f4a4 Add more debug info around attachment deduping. 2024-09-23 09:23:57 -04:00
Alex Hart
fde78cf5b8 Remove unused parameter in LinkPreviewViewModel. 2024-09-23 09:53:46 -03:00
Greyson Parrelli
eab1f5944d Add support for long text backup. 2024-09-23 08:31:05 -04:00
Greyson Parrelli
ecd16dbe9c Fix payment notification backup import/export. 2024-09-22 15:09:04 -04:00
Greyson Parrelli
a76f5e600e Fix flakiness of the backup tests.
It's possible that pending writes to the key value store (from using
.apply()) may not be finished by the time we take the DB snapshot,
resulting in us seeing stale data in the snapshot. Now we block on
writes finishing.
2024-09-21 22:51:24 -04:00
Greyson Parrelli
054b517a04 Add backup support for remaining simple chat updates. 2024-09-21 15:49:12 -04:00
Greyson Parrelli
40ca94a7dd Fix sticker backup import/export. 2024-09-21 11:48:41 -04:00
Greyson Parrelli
ba1e8b6c14 Fix handling of invalid quote attachment locators in backups. 2024-09-21 11:22:37 -04:00
Greyson Parrelli
f3a9f7f91d Fix handling of stickers with invalid locators in backups. 2024-09-21 09:59:09 -04:00
Greyson Parrelli
3c0e9c9e4e Fix group receipt handling in backups. 2024-09-21 09:49:21 -04:00
Greyson Parrelli
9888b1a5f8 Fix backup support for account wallpapers. 2024-09-21 07:01:43 -04:00
Greyson Parrelli
ec49352635 Merge various proto utils together in core-util-jvm. 2024-09-20 23:29:08 -04:00
Greyson Parrelli
5b69d98579 Fix potential NPE when reading old attachments. 2024-09-20 22:07:07 -04:00
Greyson Parrelli
90998a4076 Fix various backup import-export inconsistencies. 2024-09-20 21:14:50 -04:00
Cody Henthorne
a10958ee13 Add optimize storage infrastructure for backupsv2. 2024-09-20 16:47:18 -04:00
Alex Hart
7935d12675 Fix incorrect display of ISK recurring cost. 2024-09-20 16:11:47 -03:00
Cody Henthorne
cafa5c9e28 Add more info for various spinner results. 2024-09-20 12:52:37 -04:00
Greyson Parrelli
a7bdfb6d76 Add support for backing up wallpapers. 2024-09-20 12:24:57 -04:00
Alex Hart
e14078d2ec Allow free tier to be enabled when Google Play Billing isn't available. 2024-09-20 10:52:18 -04:00
Alex Hart
12e25b0f40 Add google play billing token conversion endpoint and job. 2024-09-20 10:52:18 -04:00
Alex Hart
d23ef647d8 Hide paid tier on devices where the billing API is not available. 2024-09-20 10:52:18 -04:00
Alex Hart
d88265ede6 Update view state after enabling mic permissions to match view model state. 2024-09-20 10:52:18 -04:00
Greyson Parrelli
0e83e25e6e Setup infra for better archive upload progress tracking. 2024-09-20 10:52:18 -04:00
Jim Gustafson
1597ee70ba Update to RingRTC v2.48.0 2024-09-20 10:52:18 -04:00
Greyson Parrelli
01ee98af91 Use better update string for manual installs.
Fixes #13700
2024-09-20 10:52:18 -04:00
Greyson Parrelli
9a1d5f4dce Update to latest Backup.proto. 2024-09-20 10:52:18 -04:00
Greyson Parrelli
60bf121974 Update to libsignal 0.58.0 2024-09-20 10:52:18 -04:00
Cody Henthorne
46844ced7c Log notification posting exception when encountered. 2024-09-20 10:52:18 -04:00
Greyson Parrelli
1ac19e84c2 Fix issues with archive uploads matching digest. 2024-09-20 10:52:18 -04:00
Alex Hart
48bd57c56a Start re-work of play billing checkout flow. 2024-09-20 10:52:18 -04:00
Gaëtan Muller
b340097f9c Remove Multidex usages.
Since the min SDK is at least 21, it is no longer necessary to use the Multidex library.

See the following for more info: https://developer.android.com/build/multidex#mdex-on-l

Resolves #13696
2024-09-20 10:52:18 -04:00
Cody Henthorne
a1bf4d62ab Fix thumbnail rendering and refreshing on full download. 2024-09-20 10:52:18 -04:00
Michelle Tang
b74f04495e Update verified icon. 2024-09-20 10:52:18 -04:00
Greyson Parrelli
ba06efe35a Improve the Banner system. 2024-09-20 10:52:18 -04:00
Greyson Parrelli
24133c6dac Fix potential crash when reading very old attachments. 2024-09-20 10:52:18 -04:00
Alex Hart
64ada79e8f Switch wording for group link administration. 2024-09-20 10:52:18 -04:00
Alex Hart
8933d89b56 Filter call link events we don't have root keys for and disambiguate return / join. 2024-09-20 10:52:18 -04:00
Cody Henthorne
88d1c0cf87 Fix internal message details rendering warning. 2024-09-20 10:52:18 -04:00
Greyson Parrelli
703c00b9af Fix banner background. 2024-09-20 10:52:18 -04:00
Greyson Parrelli
c0d115325a Fix crash in legacy migration. 2024-09-20 10:52:18 -04:00
Greyson Parrelli
6f3f204cbe Log PNP setting change events. 2024-09-20 10:52:18 -04:00
Alex Hart
cd846f2b6d Fix call link join issue and add denial dialogs into call UI v2. 2024-09-20 10:52:17 -04:00
Alex Hart
5bd3eda17d Add snackbar that is displayed if you're currently in a different call. 2024-09-20 10:52:17 -04:00
Greyson Parrelli
c36c6e62e2 Add Flow.throttleLatest extension. 2024-09-20 10:52:17 -04:00
Cody Henthorne
6b9e921888 Fix incorrect image showing in gallery when other media is unavailable. 2024-09-20 10:52:17 -04:00
mtang-signal
f57b1a8f5e Restore picker after editing a message. 2024-09-20 10:52:17 -04:00
Greyson Parrelli
7727deef9f Fix handling of common backup status codes. 2024-09-20 10:52:17 -04:00
ᡠᠵᡠᡳ ᡠᠵᡠ ᠮᠠᠨᡩ᠋ᠠᠨ
789aea3a3a Set kotlin jvmToolchain for jvm modules.
Closes #13686

Fixes #13523
2024-09-20 10:52:17 -04:00
mtang-signal
81b4339bea Add capitalization to profile names. 2024-09-20 10:52:17 -04:00
RohitBeatroute
76175c7a6b Fix username discriminator from disappearing.
Closes #13687

Fixes #13680
2024-09-20 10:52:17 -04:00
Greyson Parrelli
e81fc2900d Bump version to 7.17.5 2024-09-19 16:03:42 -04:00
Greyson Parrelli
db9a2f04f3 Update translations and other static files. 2024-09-19 16:03:15 -04:00
Alex Hart
1d719333a3 Heal SEPA transfer keep-alive failures. 2024-09-19 12:23:51 -03:00
Alex Hart
71f6c77b42 Fix missing paymentMethodType in keep-alive payment creation. 2024-09-19 11:35:34 -03:00
Greyson Parrelli
7a66533e70 Bump version to 7.17.4 2024-09-18 07:12:04 -04:00
Greyson Parrelli
9106812b74 Reset the upload timestamp on attachments with fixed digests. 2024-09-18 07:05:52 -04:00
Greyson Parrelli
fcb2e3cc74 Make LimitedInputStream less strict. 2024-09-18 07:05:29 -04:00
Greyson Parrelli
1f638db959 Bump version to 7.17.3 2024-09-17 23:03:41 -04:00
Greyson Parrelli
832d15ff47 Ensure call link table matches upgraded table. 2024-09-17 23:02:49 -04:00
Greyson Parrelli
f8846e3593 Clear attachment uploadTimestamps. 2024-09-17 23:02:26 -04:00
Greyson Parrelli
59e0afde14 Bump version to 7.17.2 2024-09-16 13:13:40 -04:00
Greyson Parrelli
00058f7762 Update baseline profile. 2024-09-16 13:13:09 -04:00
Greyson Parrelli
56159043e3 Update translations and other static files. 2024-09-16 13:03:03 -04:00
Greyson Parrelli
2180b78466 Ensure username is reclaimed after account restore. 2024-09-16 12:54:58 -04:00
Greyson Parrelli
dc77226995 Address a FTS table configuration crash. 2024-09-16 09:51:14 -04:00
Alex Hart
0a346eda5b Fix calls count when there are no entries to display. 2024-09-16 09:42:13 -03:00
Cody Henthorne
6188502cb1 Bump version to 7.17.1 2024-09-13 13:52:40 -04:00
Cody Henthorne
b425920144 Update baseline profile. 2024-09-13 13:49:46 -04:00
Cody Henthorne
db60a3cb2c Update translations and other static files. 2024-09-13 13:46:26 -04:00
Cody Henthorne
6b2ff05adb Fix draft state being reapplied on input state change. 2024-09-13 13:38:22 -04:00
Cody Henthorne
0108a1d3e3 Bump version to 7.17.0 2024-09-13 11:18:56 -04:00
Cody Henthorne
64e61ccce3 Update baseline profile. 2024-09-13 11:06:37 -04:00
Cody Henthorne
efef179124 Update translations and other static files. 2024-09-13 11:00:26 -04:00
Jim Gustafson
6789715556 Update to RingRTC v2.47.1 2024-09-13 10:51:30 -04:00
Alex Hart
463fabcbc4 Polish pending participants views. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
23d82a3a01 Remove skip/forgot PIN special case. 2024-09-13 10:51:30 -04:00
Alex Hart
d1475228f7 Add chevron to pending participants view. 2024-09-13 10:51:30 -04:00
Cody Henthorne
636b5a4ba6 Prevent sharing and clear drafts when entering disabled send conversations. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
850515b363 Make FTS recovery more resiliant. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
5c6644d1a1 Add extra transaction protections. 2024-09-13 10:51:30 -04:00
Cody Henthorne
0d37013481 Fix more voice note playback NPEs. 2024-09-13 10:51:30 -04:00
Cody Henthorne
5647215659 Fix state exception when registering without play services. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
e80ebd87fe Refactor and simplify attachment archiving. 2024-09-13 10:51:30 -04:00
Cody Henthorne
816006c67e Refactor and cleanup backupv2 media restore. 2024-09-13 10:51:30 -04:00
Alex Hart
baa6032770 Fix overlap of join banner and camera toggle. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
7735ca9dab Fix crash when downloading attachment from S3. 2024-09-13 10:51:30 -04:00
Alex Hart
36a8a399d9 Only display latest call link event in calls tab. 2024-09-13 10:51:30 -04:00
Alex Hart
9912a5fdfe Allow anyone to join a call link. 2024-09-13 10:51:30 -04:00
Alex Hart
c3be92d365 Upgrade several AndroidX libraries and Compose to latest stable versions. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
0fe9df3023 Properly clear unknown ids from storage service. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
cb126a2f08 Fix runAttempt not updating in job cache.
Thank you to @valldrac for finding this and diagnosing it!

Fixes #13679
2024-09-13 10:51:30 -04:00
Greyson Parrelli
7835b1d1fc Move more networking stuff into SignalNetwork. 2024-09-13 10:51:30 -04:00
Nicholas Tinsley
e247d311d8 Add call link support to storage service. 2024-09-13 10:51:30 -04:00
Alex Hart
1f2b5e90a3 Remove unnecessary check in call link processing. 2024-09-13 10:51:30 -04:00
Jim Gustafson
ee033b49fe Update to RingRTC v2.47.0 2024-09-13 10:51:30 -04:00
Greyson Parrelli
a7b958d811 Only run BackupMessageJob after the digest backfill has finished. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
c4bcb7dc93 Improve digest backfill migration. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
1e8626647e Fix digests for non-zero padding. 2024-09-13 10:51:30 -04:00
Nicholas Tinsley
a50f316659 Harden null safety in VoiceNotePlaybackService.
Addresses #13673.
2024-09-13 10:51:30 -04:00
Alex Hart
1f09f48e6b Add proper call tab return state. 2024-09-13 10:51:30 -04:00
Nicholas Tinsley
514f7cc767 Fix tests after reg v1 cleanup. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
b858161f92 Fix NetworkResult handling of websocket timeouts. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
85d90aa121 Add the ability to set no limit on LimitedInputStream. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
a8fb4eb21a Rename TruncatingInputStream -> LimitedInputStream. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
a6767e4f8a Replace other limiting streams with TruncatingInputStream. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
b00855b097 Add support for more methods in TruncatingInputStream. 2024-09-13 10:51:30 -04:00
Cody Henthorne
929942de9d Verify digest for backupv2 local media restore. 2024-09-13 10:51:30 -04:00
Greyson Parrelli
6112ee9bd3 Initialize AppDependencies if needed in AvatarProvider. 2024-09-13 10:51:30 -04:00
Nicholas Tinsley
9261c34213 Clean up registration java packages hierarchy. 2024-09-13 10:51:30 -04:00
Nicholas Tinsley
f29d4f980a Removal final usage of VerifyResponseProcessor. 2024-09-11 15:14:05 -04:00
Nicholas Tinsley
bf46e5bc24 Consolidate odds and ends from reg v1 into reg v2. 2024-09-11 15:14:05 -04:00
Nicholas Tinsley
c9746b59ed Clean up reg v1 remnants using safe delete. 2024-09-11 15:14:05 -04:00
Alex Hart
2123c642a5 Change admin approval string for call links. 2024-09-11 15:14:04 -04:00
Alex Hart
118085f692 Fix aspect ratio of link preview thumbnails. 2024-09-11 15:14:04 -04:00
Cody Henthorne
2701b570bb Use trailing job to clear media restore progress. 2024-09-11 15:14:04 -04:00
Cody Henthorne
390ea341ca Fix incorrect padding buffer reuse. 2024-09-11 15:14:04 -04:00
Alex Hart
b7abd85992 Fix status bar theming in children of FragmentWrapperActivity. 2024-09-11 15:14:04 -04:00
Alex Hart
982b90d423 Add BillingDependencies and shared implementation. 2024-09-11 15:14:04 -04:00
Alex Hart
36bfd19bcf Fix db access in RemoteMegaphoneRepository. 2024-09-11 15:14:04 -04:00
Greyson Parrelli
7eac9ce1f4 Improve attachment deduping for videos. 2024-09-11 15:14:04 -04:00
mtang-signal
ba2d5bce41 Allow linking of devices if no passlock is set. 2024-09-11 15:14:04 -04:00
Michelle Tang
93c8cd133d Add education sheet to linked device biometrics. 2024-09-11 15:14:04 -04:00
Greyson Parrelli
d59985c7b1 Add migration to backfill digests. 2024-09-11 15:14:04 -04:00
Cody Henthorne
a8bf03af89 Add restore local backupv2 infra. 2024-09-11 15:14:04 -04:00
Greyson Parrelli
00d20a1917 Introduce SignalNetwork, share PushServiceSocket. 2024-09-11 15:14:04 -04:00
Greyson Parrelli
4e35906680 Add blocked check when adding 'user joined' message. 2024-09-11 15:14:04 -04:00
Alex Hart
4d23f11f6e Add shared calling intent system. 2024-09-11 15:14:04 -04:00
Greyson Parrelli
e5b482c7ad Fix error handling in NetworkResult.fromWebSocketRequest() 2024-09-11 15:14:04 -04:00
Greyson Parrelli
6c09b59d1b Close stream after calculating length. 2024-09-11 15:14:04 -04:00
Greyson Parrelli
8070f26207 Save correct size after attachment upload. 2024-09-11 15:14:04 -04:00
Nicholas Tinsley
623312d8f6 Inline StreamingTranscoder.
Delete InMemoryTranscoder.
2024-09-11 15:14:04 -04:00
Greyson Parrelli
ac9e5505ae Save IV on attachment download. 2024-09-11 15:14:04 -04:00
Greyson Parrelli
4b47d38d78 Add IV to the attachment table. 2024-09-11 15:14:04 -04:00
Cody Henthorne
07289b417b Bump version to 7.16.4 2024-09-11 15:07:24 -04:00
Cody Henthorne
6827955c41 Update baseline profile. 2024-09-11 15:06:15 -04:00
Cody Henthorne
269d3c43f6 Update translations and other static files. 2024-09-11 15:00:09 -04:00
Greyson Parrelli
ac10ff4cbe Improve validations on envelope. 2024-09-11 14:45:02 -04:00
Alex Hart
b681b4169f Fix callbacks for DonationPending and UserLaunchedExternalApplication donation delegate methods. 2024-09-11 14:37:16 -04:00
Alex Hart
7472166628 Bump version to 7.16.3 2024-09-05 12:36:56 -03:00
Alex Hart
04f9468cc6 Update baseline profile. 2024-09-05 11:59:48 -03:00
Alex Hart
c592a5b39d Update translations and other static files. 2024-09-05 11:54:04 -03:00
Alex Hart
a992da9a7b Fix test users for benchmarking. 2024-09-05 11:49:57 -03:00
Greyson Parrelli
1aed8eefcd Improve reliability of rebuilding the search index. 2024-09-05 11:49:57 -03:00
Greyson Parrelli
6682815663 Fix NPE in VoiceNotePlaybackService. 2024-09-05 11:49:57 -03:00
Cody Henthorne
676be03ffc Bump version to 7.16.2 2024-09-03 16:15:10 -04:00
Cody Henthorne
527096cc0e Update translations and other static files. 2024-09-03 16:09:12 -04:00
Cody Henthorne
83c3cc6a6d Fix notifications not showing after contact permission revoked on Android 15.
Fixes #13671
2024-09-03 15:56:12 -04:00
Cody Henthorne
0c4725dfa7 Fix unnecessary timer change message insert on sync messages. 2024-09-03 15:43:01 -04:00
Michelle Tang
2c7668253e Fix missing photos in gallery. 2024-09-03 12:20:38 -07:00
Nicholas Tinsley
ab7bdc3c03 Bump version to 7.16.1 2024-09-01 11:52:54 -04:00
Nicholas Tinsley
bb1b548c27 Update translations and other static files. 2024-09-01 10:29:32 -04:00
Cody Henthorne
216073f4c2 Fix versioned expiration timer capability bug. 2024-08-30 16:18:23 -04:00
Nicholas Tinsley
84ae8db549 Bump version to 7.16.0 2024-08-30 13:16:37 -04:00
Nicholas Tinsley
09bd460875 Update translations and other static files. 2024-08-30 13:07:53 -04:00
Greyson Parrelli
97ea5dc45e Protect against NPE in search. 2024-08-30 12:55:23 -04:00
Nicholas Tinsley
85449802d1 Properly handle video transcoding failures. 2024-08-30 12:55:23 -04:00
Nicholas Tinsley
d683b8a321 Preclude cancelation of pre-uploaded video attachments.
Addresses ##10225.
2024-08-30 12:55:23 -04:00
Greyson Parrelli
2b1bbdda15 Inline the withinTransaction() function. 2024-08-30 12:55:23 -04:00
Greyson Parrelli
011a36c8f3 Move back to manually implementing secure-delete. 2024-08-30 12:55:23 -04:00
Nicholas Tinsley
dd1976d431 Log why we're showing a debug log prompt. 2024-08-30 12:55:23 -04:00
Jim Gustafson
643f64e181 Use the Oboe ADM for some custom roms 2024-08-30 12:55:23 -04:00
Nicholas Tinsley
659e36673b Fix wrong string in pending group join request banner. 2024-08-30 12:55:23 -04:00
Nicholas Tinsley
907918d3fa Logging around attachments pre-uploads. 2024-08-30 12:55:23 -04:00
Nicholas Tinsley
243c86cec3 Prevent ISE on cell signal loss. 2024-08-30 12:55:23 -04:00
Nicholas Tinsley
dca10634e6 Attempt to fix impossible index out of bounds exception? 2024-08-30 12:55:22 -04:00
Nicholas Tinsley
5dfc4c422e Update styling of Call Link join button. 2024-08-30 12:55:22 -04:00
Greyson Parrelli
46753fc617 Another attempt at rebuilding the FTS tables. 2024-08-30 12:55:22 -04:00
Greyson Parrelli
e263d7da73 Fix crash when reading some contact cards. 2024-08-30 12:55:22 -04:00
Greyson Parrelli
c4ba579310 Mitigate app migration failing on missing table.
In an ideal world, we'd fix this with a database migration... but we're
seeing _really_ weird behavior around FTS tables, and I'd rather not
press my luck.
2024-08-30 12:55:22 -04:00
Jim Gustafson
d6d9e5ca64 Update to RingRTC v2.46.2 2024-08-30 12:55:22 -04:00
Cody Henthorne
90a8d90e40 Allow building libsignal from source.
Co-authored-by: Jordan Rose <jrose@signal.org>
2024-08-30 12:55:22 -04:00
Greyson Parrelli
b61ca37523 Do not link contacts to notification unless we have permission. 2024-08-30 12:55:22 -04:00
Nicholas Tinsley
b7af1e09e2 Increase logging around backup restores. 2024-08-30 12:55:22 -04:00
Nicholas Tinsley
ff47f784a3 Prevent IndexOutOfBounds exception when media is deleted. 2024-08-30 12:55:22 -04:00
Cody Henthorne
1f196f74ff Add support for versioned expiration timers.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2024-08-30 12:55:22 -04:00
Jordan Rose
4152294b57 Update to NDK r27, and explicitly specify it in the app build. 2024-08-30 12:55:22 -04:00
Greyson Parrelli
1aaa833127 Replace manual FTS5 fix with SQLite secure-delete flag.
We used to workaround this by manually optimizing the search index, but secure-delete does that for us with less work.
2024-08-30 12:55:22 -04:00
Nicholas Tinsley
2cfd19add6 Minor log statement rewording. 2024-08-27 13:21:20 -04:00
Greyson Parrelli
8e3000d852 Update sqlcipher to 4.6.0-S1 2024-08-27 13:21:20 -04:00
Greyson Parrelli
4e48a445bf Disable flaky test. 2024-08-27 13:21:20 -04:00
Nicholas Tinsley
45833ef24a Reset upload progress if attachment upload is interrupted. 2024-08-27 13:21:20 -04:00
Nicholas Tinsley
4354a9ff5e Add small logging to attachment finalization process. 2024-08-27 13:21:20 -04:00
Nicholas Tinsley
f1bf6105ea Don't try to download attachment if it's being restored. 2024-08-27 13:21:20 -04:00
Alex Hart
282ec6918b Add call audio toggle to calling v2. 2024-08-27 13:21:20 -04:00
Nicholas Tinsley
69d62d385e Small fixes for the video transcoding playground app. 2024-08-27 13:21:20 -04:00
Nicholas Tinsley
0f7f866562 Experimental HEVC encoding support for videos. 2024-08-27 13:21:20 -04:00
Alex Hart
5f66e2eb15 Add visibility rules and timeout for call controls for v2. 2024-08-27 13:21:20 -04:00
Alex Hart
3f71f90234 Add call participants overflow to calling v2 screen. 2024-08-27 13:21:20 -04:00
Nicholas Tinsley
204fcc28c7 Bump version to 7.15.4 2024-08-27 13:17:16 -04:00
Nicholas Tinsley
f53cb19943 Update translations and other static files. 2024-08-27 13:15:14 -04:00
Greyson Parrelli
cea8546ce5 Fix serialization issue during registration. 2024-08-27 12:14:01 -04:00
Alex Hart
bb7ee5915c Add billing fix. 2024-08-27 11:22:06 -04:00
Nicholas Tinsley
cf8e05fa39 Bump version to 7.15.3 2024-08-26 17:51:07 -04:00
Nicholas Tinsley
01cf0b69e0 Update translations and other static files. 2024-08-26 17:38:41 -04:00
Nicholas Tinsley
0aa764586e Dark mode support for DefaultBanner. 2024-08-26 17:35:57 -04:00
Nicholas Tinsley
532441db24 Log errors during media controller initialization. 2024-08-26 16:44:52 -04:00
Greyson Parrelli
2bc07e87d8 Add stopgap for FTS migration crash. 2024-08-26 15:58:22 -04:00
Greyson Parrelli
60ad879cac Improve network reliability. 2024-08-26 14:53:08 -04:00
mtang-signal
e1bc04a811 Bump version to 7.15.2 2024-08-23 15:06:43 -07:00
mtang-signal
23b53ef0f8 Update translations and other static files. 2024-08-23 15:02:14 -07:00
Alex Hart
047ec137c9 Update exclusions. 2024-08-23 13:44:33 -07:00
mtang-signal
72cb1528ad Fix payments overlap. 2024-08-23 13:26:27 -07:00
mtang-signal
4b78b44b29 Bump version to 7.15.1 2024-08-22 15:40:06 -07:00
mtang-signal
e0763fbf86 Update translations and other static files. 2024-08-22 15:30:15 -07:00
Cody Henthorne
1758a20174 Remove incorrect play flavor android manifest. 2024-08-22 18:14:06 -04:00
mtang-signal
c3c7bb7fba Bump version to 7.15.0 2024-08-22 13:44:30 -07:00
mtang-signal
3818eb6937 Update translations and other static files. 2024-08-22 13:16:34 -07:00
Nicholas Tinsley
d15bb05ae3 Observe sharedprefs for banner updates. 2024-08-22 13:05:00 -07:00
Alex Hart
244a81ef24 Move billing code to shared module. 2024-08-22 13:04:59 -07:00
Cody Henthorne
4447433ffe Fix contact sync by not requiring upload specs for avatars. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
46bc2589b5 Update Jetpack Compose to BOM 2024.06.00 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
076df8c429 Update video sample app to read and write from private app storage. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
8727f0d90d Update video sample app for API 34. 2024-08-22 13:04:59 -07:00
mtang-signal
8fc21876fe Fix crash when opening stories. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
a3c476f2ab Only query service outage every 60 seconds, even if not reachable. 2024-08-22 13:04:59 -07:00
Cody Henthorne
a76d400bd1 Upgrade to libsignal 0.56.0 2024-08-22 13:04:59 -07:00
Cody Henthorne
9b23264502 Include additional key preference. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
1e58f8097a Prevent startup crash on first launch. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
9a24455085 Delete the reminders system. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
4002dea05d Observe service outages in a lifecycle-aware fashion. 2024-08-22 13:04:59 -07:00
Alex Hart
fd31bc60b2 Read and use backups data to structure tier feature sets. 2024-08-22 13:04:59 -07:00
Alex Hart
478e3a7233 Add more api calls for billing integration. 2024-08-22 13:04:59 -07:00
Alex Hart
26e79db057 Begin re-architecture of calling screen. 2024-08-22 13:04:59 -07:00
Cody Henthorne
71b5a9f865 Fix avatar loading in OS views when app is not running. 2024-08-22 13:04:59 -07:00
Jim Gustafson
8a4d9fc635 Update to RingRTC v2.46.1 2024-08-22 13:04:59 -07:00
Alex Hart
d3a6d31873 Add empty billing factory to nightly variant. 2024-08-22 13:04:59 -07:00
Alex Hart
57f36e7b41 Fix formatting. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
43491daff9 Add banner listener for the banners being hidden and shown. 2024-08-22 13:04:59 -07:00
Cody Henthorne
4c9b5926b9 Do not enqueue no-op read receipt jobs. 2024-08-22 13:04:59 -07:00
Alex Hart
cda029cd93 Add billing module and include in play implementation. 2024-08-22 13:04:59 -07:00
Alex Hart
82443af8f7 Check remote subscription object to determine if a cancel is necessary. 2024-08-22 13:04:59 -07:00
Alex Hart
1f8481d287 Add check for KEEP_ALIVE job state to allow for re-submission. 2024-08-22 13:04:59 -07:00
Alex Hart
b7e9446cde Fix missed cancelation check. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
cc615fbf87 Fix showing banners in conversation view. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
112473bc5c Adjust DefaultBanner padding values. 2024-08-22 13:04:59 -07:00
Nicholas Tinsley
8d38f6f5e7 Clean up unused custom camera controller. 2024-08-22 13:04:59 -07:00
Alex Hart
66278a0eac Catch and log END state payments when running keep-alive. 2024-08-22 13:04:59 -07:00
Cody Henthorne
1fdb3ffb03 Fix doze mode sleeping for message retrieval thread.
Fixes #13650
2024-08-22 13:04:59 -07:00
Nicholas Tinsley
560086a1c2 Fix dismissible banners. 2024-08-22 13:04:59 -07:00
Greyson Parrelli
630875dae2 Reduce noise of flaky test. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
627b939326 Fix erroneously display "Canceling subscription" during account deletion. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
5c9b7ce7d5 Attempt to send media message interrupted by Safety Number Change. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
5171986aca Wire up voice note playback on Message Details Screen. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
75c84c452b Convert MessageDetailFragment to Kotlin. 2024-08-22 12:58:33 -07:00
Greyson Parrelli
110e2c9eb0 Fix ArchiveImportExport tests. 2024-08-22 12:58:33 -07:00
Greyson Parrelli
330cef2702 Remove unnecessary full JobSpecs from job deletion code. 2024-08-22 12:58:33 -07:00
Cody Henthorne
8eb0b2f960 Add initial local archive export support. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
c39a1ebdb6 Fix conversation Banner display conditions. 2024-08-22 12:58:33 -07:00
Greyson Parrelli
69e8c9351d Improve network reliability. 2024-08-22 12:58:33 -07:00
Greyson Parrelli
7f71d08e11 Convert the TlsProxySocketFactory to kotlin. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
6f2cc923c2 Add dependency to convert RxJava to Kotlin Flows. 2024-08-22 12:58:33 -07:00
moiseev-signal
5f40144ae9 Upgrade to libsignal 0.55.0 2024-08-22 12:58:33 -07:00
Greyson Parrelli
7936552d53 Give chat settings button bar labels more lines. 2024-08-22 12:58:33 -07:00
Greyson Parrelli
5ffb7b07da Update to latest Backup.proto and fix various backup bugs. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
e2e6a73e8d Add Banners to all reminder usages behind remote config. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
f296fcd716 Restore-after-registration fixes. 2024-08-22 12:58:33 -07:00
Alex Hart
fa7e4c9686 Add ignore for checkout flow test for the time being. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
22dd1da985 Only show playable videos in the media preview fragment. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
32a00b5c75 Update downloadAttachmentIfNeeded() to use a when expression. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
263690d3e2 Delete PagingMediaLoader.java 2024-08-22 12:58:33 -07:00
Alex Hart
54c07dd966 Ensure unread bubble is hidden on mentions scroll button. 2024-08-22 12:58:33 -07:00
Nicholas Tinsley
e036c8992f Don't display erroneous remaining time in video player. 2024-08-22 12:58:33 -07:00
mtang-signal
879c794324 Bump version to 7.14.2 2024-08-22 11:50:42 -07:00
mtang-signal
57af49953a Update translations and other static files. 2024-08-22 11:45:55 -07:00
Nicholas Tinsley
6f6665e1d4 Catch device-specific media button receiver exceptions during initialization. 2024-08-22 13:53:13 -04:00
Cody Henthorne
d20b610768 Improve network reliability. 2024-08-22 13:02:10 -04:00
Cody Henthorne
bcbe39c85c Bump version to 7.14.1 2024-08-13 16:00:20 -04:00
Cody Henthorne
b2e1f41b0e Update baseline profile. 2024-08-13 15:55:08 -04:00
Cody Henthorne
7cb2c3415a Update translations and other static files. 2024-08-13 15:48:26 -04:00
mtang-signal
d0a06ab3de Fix repeating battery optimization alerts. 2024-08-13 11:32:27 -07:00
Michelle Tang
ae923c9221 Remove immediate screen lock option. 2024-08-13 11:24:08 -07:00
mtang-signal
1a5ce9b4b9 Fix ISE in media gallery. 2024-08-13 11:09:09 -07:00
Greyson Parrelli
748cd00883 Improve network reliability. 2024-08-13 12:47:41 -04:00
Greyson Parrelli
1e4e6b6b41 Potential fix for missing FTS tables. 2024-08-13 11:30:18 -04:00
Cody Henthorne
19f3219224 Fix crash when outgoing call picked up while in the background. 2024-08-13 10:51:12 -04:00
Greyson Parrelli
6358589e19 Add disclaimer about bundletool bug. 2024-08-13 10:36:37 -04:00
Michelle Tang
08d8564c00 Add minor UI updates to screen lock. 2024-08-12 16:22:43 -07:00
mtang-signal
68c7ce5823 Bump version to 7.14.0 2024-08-08 12:51:34 -07:00
mtang-signal
bfcbfed0a6 Update baseline profile. 2024-08-08 12:38:33 -07:00
mtang-signal
8f99930168 Update translations and other static files. 2024-08-08 12:30:12 -07:00
Michelle Tang
21019d1726 Add manufacturer to notification help. 2024-08-08 12:22:34 -07:00
Nicholas Tinsley
7807d92825 Display progress for RestoreAttachmentJobs as a Banner. 2024-08-08 12:22:34 -07:00
Nicholas Tinsley
4af6e0480a Fix playback position indicator for trimmed video clips. 2024-08-08 12:22:34 -07:00
Alex Hart
1d6917476e Pop the "Create backup now" sheet after different instances of subscribing to backups. 2024-08-08 12:22:34 -07:00
Michelle Tang
3bdbd69a7d Update screen lock. 2024-08-08 12:22:34 -07:00
Alex Hart
c880db0f4a Fix checkout flow activity close on error. 2024-08-08 12:22:34 -07:00
Michelle Tang
385ba3590c Fix device bottom sheet ISE 2024-08-08 12:22:34 -07:00
Alex Hart
c63beb5b2c Hide optimization option if the user doesn't have backups enabled. 2024-08-08 12:22:34 -07:00
Nicholas Tinsley
7dc15f8bd3 Wire up event bus in EnterCodeFragment. 2024-08-08 12:22:34 -07:00
Nicholas Tinsley
e83c6dc7c2 Prevent ISE during code entry in registration. 2024-08-08 12:22:34 -07:00
Nicholas Tinsley
d45acd0e24 Migrate existing Reminders to Banners. 2024-08-08 12:22:34 -07:00
Alex Hart
16a732171a Add 'create a pin' button to backups sub pin entry sheet. 2024-08-08 12:22:34 -07:00
Alex Hart
b9da045f79 Add proper styling for incorrect pin text. 2024-08-08 12:22:34 -07:00
Alex Hart
ccabd9edd8 Add espresso tests for donations flow. 2024-08-08 12:22:34 -07:00
Nicholas Tinsley
11d165a17b Introduce Banners. 2024-08-08 12:22:34 -07:00
Nicholas Tinsley
ef2c67d808 Registration: check if phone number is possible.
As opposed to "valid."
2024-08-08 12:19:01 -07:00
Alex Hart
6ac510a156 Add error state for pin entry during backups flow. 2024-08-08 12:19:01 -07:00
Alex Hart
da74874815 Refresh settings screen after disabling backups. 2024-08-08 12:19:01 -07:00
Alex Hart
ff9c77c2e2 Fix backups checkout flow next state. 2024-08-08 12:19:01 -07:00
Greyson Parrelli
2c11a27897 Fix emoji full text search.
Co-authored-by: Nolan Woods <innovate.invent@gmail.com>
2024-08-08 12:19:01 -07:00
Alex Hart
8dc910e71d Move pip above controls when expanded or not enough space in landscape. 2024-08-08 12:19:01 -07:00
Nicholas Tinsley
cbd587f142 Fix post-registration restore closing the app. 2024-08-08 12:19:01 -07:00
Jim Gustafson
ffd2e164bf Update to RingRTC v2.46.0 2024-08-08 12:19:01 -07:00
Greyson Parrelli
ddb367edbe Update WebView fields in AndroidManifest. 2024-08-08 12:19:01 -07:00
Greyson Parrelli
981e7a4270 Tweak when decryption drained state is reset. 2024-08-08 12:19:01 -07:00
Michelle Tang
b46b04fcdf Update strings. 2024-08-08 12:19:01 -07:00
Cody Henthorne
aebaff736c Fix mic usage for api34 when app is backgrounded. 2024-08-08 12:19:01 -07:00
Greyson Parrelli
8932eef991 Convert some SignalServiceAttachment* classes to kotlin. 2024-08-08 12:19:01 -07:00
Fedor Indutny
bb01c0501b Fix info string in deriveMediaId. 2024-08-08 12:19:00 -07:00
Alex Hart
1d1ea01cc1 Add optimize storage setting and sheet. 2024-08-08 12:19:00 -07:00
Cody Henthorne
2677665069 Fix group send error handling for single targets. 2024-08-08 12:19:00 -07:00
Greyson Parrelli
ea215ef488 Fix network interaction in backup service. 2024-08-08 12:19:00 -07:00
Greyson Parrelli
d9586e8d00 Revert "Upgrade Jetpack Compose to 2024.04.00."
This reverts commit f1fbd647685ab51e54863a763b37978505251b8e.
2024-08-08 12:19:00 -07:00
Greyson Parrelli
e1283a66fd Fix issue where mediaName is stored in proto incorrectly. 2024-08-08 12:19:00 -07:00
Alex Hart
fb82703740 Ensure calling pip is in landscape if we leave the activity while in landscape mode. 2024-08-08 12:19:00 -07:00
Greyson Parrelli
5e03e31ffd Fix various AccountData backupV2 import/export bugs.
I'm not including the auto-generated test files yet because I'm still
making tweaks, but these are all valid fixes that got the current
(uncommitted) batch of test files passing.
2024-08-08 12:19:00 -07:00
Nicholas Tinsley
4091af3632 Upgrade Jetpack Compose to 2024.04.00. 2024-08-08 12:19:00 -07:00
Nicholas Tinsley
1b729c42b6 Upgrade AndroidX Lifecycle to 2.8.4.
This also adds a dependency on the lifecycle-runtime-compose artifact.
2024-08-08 12:19:00 -07:00
Alex Hart
5c139aa5b1 Fix reaction positioning in large group calls. 2024-08-08 12:19:00 -07:00
Cody Henthorne
dc7208922c Update target SDK to 34. 2024-08-08 12:19:00 -07:00
Nicholas Tinsley
6424c6bc99 Fixes for restoring a backup after completing registration. 2024-08-08 12:19:00 -07:00
Michelle Tang
57adab858c Add selected photos access. 2024-08-08 12:19:00 -07:00
Alex Hart
4f001a0c95 Default admin approval to on in call links UI. 2024-08-08 12:19:00 -07:00
Jim Gustafson
12529e9fb0 Update to RingRTC v2.45.0 2024-08-08 12:19:00 -07:00
Alex Hart
d406e8f5b8 Add support for several backup alert sheets. 2024-08-08 12:19:00 -07:00
Greyson Parrelli
fb2a332513 Update to latest Backup.proto. 2024-08-08 12:19:00 -07:00
Greyson Parrelli
c2bdac80dc Bump version to 7.13.3 2024-08-06 11:40:32 -04:00
Greyson Parrelli
db00000d66 Update translations and other static files. 2024-08-06 11:38:54 -04:00
Greyson Parrelli
f551a94e58 Fix bug with story sends. 2024-08-06 11:06:31 -04:00
Alex Hart
58a7d3dc08 Bump version to 7.13.2 2024-08-01 16:20:06 -03:00
Alex Hart
3380fa722d Update baseline profile. 2024-08-01 16:06:48 -03:00
Alex Hart
216f57f3ea Update translations and other static files. 2024-08-01 16:03:18 -03:00
Alex Hart
bf4aa85ac3 Fix misplaced call controls when restoring from PIP. 2024-08-01 15:39:02 -03:00
Cody Henthorne
e4966da3ef Fix crash when receiving GSE before state membership updated. 2024-07-31 16:32:46 -04:00
Alex Hart
79c7c2345f Bump version to 7.13.1 2024-07-31 16:35:16 -03:00
Alex Hart
d516037be9 Update baseline profile. 2024-07-31 16:25:15 -03:00
Alex Hart
008b38594d Update translations and other static files. 2024-07-31 16:18:56 -03:00
Cody Henthorne
15b59457f7 Fix expiration clock UI reseting incorrectly on edit. 2024-07-31 14:11:36 -04:00
Cody Henthorne
5ddd1651ee Fix stream reset error handling. 2024-07-30 13:55:08 -04:00
Cody Henthorne
8ca89d2024 Add additional debugging info to group processing lock handling. 2024-07-29 12:37:40 -04:00
Alex Hart
585c8cd863 Fix donation action routing. 2024-07-29 12:24:19 -03:00
Alex Hart
faf6ab233f Remove instance save/restore for the time being. 2024-07-29 10:28:52 -03:00
Nicholas Tinsley
95cbc91bf0 Bump version to 7.13.0 2024-07-26 23:53:15 +02:00
Nicholas Tinsley
9480e23455 Update translations and other static files. 2024-07-26 23:47:28 +02:00
Nicholas Tinsley
727a0f8273 Add additional verification code parser test case. 2024-07-26 23:43:54 +02:00
Nicholas Tinsley
279e55d65f Wire in time remaining for registration lock responses. 2024-07-26 23:43:54 +02:00
Nicholas Tinsley
c36fba2ab7 Handle registration error codes if the user backs out from the enter code screen. 2024-07-26 23:43:54 +02:00
Alex Hart
1d0997379f Add support for several BackupAlert sheet primary actions. 2024-07-26 23:43:54 +02:00
Alex Hart
1a7611d505 Add worker method for determining where to take a user when they press signal backups. 2024-07-26 23:43:53 +02:00
Nicholas Tinsley
36846301de Add missing handling for sessions that are already verified. 2024-07-26 23:43:53 +02:00
Nicholas Tinsley
b8e81e6677 Add missing registration lock route. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
3d169bffd0 Reserve server-only field in SignalService.proto. 2024-07-26 23:43:53 +02:00
Nicholas Tinsley
556a25447e Add logging around registration code autofill. 2024-07-26 23:43:53 +02:00
Alex Hart
b42e48a08a Add ability to long press 'Chats' to get backups subscriber id. 2024-07-26 23:43:53 +02:00
Alex Hart
b1a4e889bc Add support for downgrading backup. 2024-07-26 23:43:53 +02:00
mtang-signal
e6fb01a67b Fix strings. 2024-07-26 23:43:53 +02:00
Nicholas Tinsley
6c042f6e47 Add small debugging log statements to ReRegisterWithPinFragment. 2024-07-26 23:43:53 +02:00
Nicholas Tinsley
f87ff58701 Convert registration error handling from callbacks to observers. 2024-07-26 23:43:53 +02:00
Cody Henthorne
4fb335de28 Clear drafts when leaving groups. 2024-07-26 23:43:53 +02:00
Alex Hart
725d8dc85d Fixup spinner build. 2024-07-26 23:43:53 +02:00
Alex Hart
e76153b2fd Hide waiting to be let in banner by default. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
0b98901634 Integrate libsignal validator into backup tests. 2024-07-26 23:43:53 +02:00
Nicholas Tinsley
57feb272d2 Clear out any existing registration sessions if the E164 changes. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
7b0badef19 Get shared backup tests working. 2024-07-26 23:43:53 +02:00
Alex Hart
36640edfee Add more error messaging for backups. 2024-07-26 23:43:53 +02:00
Alex Hart
4e07c07ca9 Add backups error string for payment setup errors. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
31ddc5bcc0 Do not crash on invalid sessions. 2024-07-26 23:43:53 +02:00
Alex Hart
c80f459c37 Add CreateBackupBottomSheet. 2024-07-26 23:43:53 +02:00
Alex Hart
2a6dab41f5 Route InAppPaymentType and begin splitting out error messages. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
e6d8e36141 Add new backup testing infrastructure. 2024-07-26 23:43:53 +02:00
Alex Hart
816c9360cd Implement backup receipt generation. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
82c3265be5 Remove now-unnecessary exclusion from apkdiff.py.
Used to need it because baselineprof was non-deterministic, but google
has since fixed this in newer versions of AGP.
2024-07-26 23:43:53 +02:00
Greyson Parrelli
ab03a627da Upgrade libsignal-client to 0.52.5 2024-07-26 23:43:53 +02:00
Greyson Parrelli
81a45ddc09 Fix JobDatabase queue nullability crash. 2024-07-26 23:43:53 +02:00
Cody Henthorne
f1115130b2 Fix incorrect thread used for delete chat history. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
2d557215a0 Don't check for linked devices if not registered. 2024-07-26 23:43:53 +02:00
Alex Hart
cc806a2f79 Add generic payment in progress strings. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
853c934a5a Rotate the crash config feature flag. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
f1ba947a59 Add a "connectivity warning" bottom sheet. 2024-07-26 23:43:53 +02:00
Alex Hart
44b2c62a0e Add finalized strings to strings.xml for backups. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
06d475fb6e Move constraint filtering down into JobStorage to improve perf. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
36dface175 Fix job deletion bug, add performance tests. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
86cf8200b5 Remove cases where all jobs were expected to be in memory. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
973dc72cfa Use a minimal job spec representation in memory. 2024-07-26 23:43:53 +02:00
Greyson Parrelli
eb59afc33c Improve efficiency of sorting jobs in FastJobStorage. 2024-07-26 23:43:53 +02:00
Alex Hart
625ca832b0 Fix bad padding in expired story quotes. 2024-07-26 23:43:53 +02:00
Clark
bc6face873 Fix edit message clearing story reply quote. 2024-07-26 23:43:53 +02:00
Clark
0aeaec8b67 Show edited time instead of original time. 2024-07-22 11:59:12 -04:00
Alex Hart
b70b058925 Fix crash with reused destroyed context. 2024-07-22 11:59:12 -04:00
Greyson Parrelli
e17cf37799 Allow use of the in-app emoji picker when using system emoji. 2024-07-22 11:59:12 -04:00
Alex Hart
330debcf37 Add Snackbar displaying message that user is awaiting entry to an ad hoc call. 2024-07-22 11:59:12 -04:00
mtang-signal
6641cc4806 Update device notification prompt. 2024-07-22 11:59:12 -04:00
Michelle Tang
bd3ab2cc38 Update editing animation. 2024-07-22 11:59:12 -04:00
Clark
fa487e1885 Dont set subscriber data if subscriber is null. 2024-07-22 11:59:12 -04:00
Clark
eb2fc33940 Cap size of group updates. 2024-07-22 11:59:11 -04:00
Alex Hart
c39739bcb4 Add call info treatment for unknown members. 2024-07-22 11:59:11 -04:00
Alex Hart
e7720640d1 Implement landscape calling. 2024-07-22 11:59:11 -04:00
Greyson Parrelli
6e55bc04ab Bump version to 7.12.2 2024-07-22 11:58:41 -04:00
Greyson Parrelli
56ab95b0e1 Update translations and other static files. 2024-07-22 11:58:11 -04:00
Greyson Parrelli
678c1459e9 Fix welcome screen button after pressing back on transfer screen. 2024-07-22 11:40:25 -04:00
Greyson Parrelli
e87b73cc19 Bump version to 7.12.1 2024-07-19 16:09:27 -04:00
Greyson Parrelli
45e1ecd07e Update baseline profile. 2024-07-19 16:09:05 -04:00
Greyson Parrelli
7b043d4143 Update translations and other static files. 2024-07-19 15:50:45 -04:00
Alex Hart
046d439887 Do not restrict parcelized type. 2024-07-19 16:32:29 -03:00
Greyson Parrelli
b34bf4b8b0 Initialize app dependencies earlier. 2024-07-18 12:03:23 -04:00
Nicholas Tinsley
fddc99ab4f Remove noise log statement from ExoPlayer. 2024-07-18 11:07:27 -04:00
Rashad Sookram
1007111310 Update to RingRTC v2.44.4 2024-07-18 08:54:54 -04:00
Greyson Parrelli
3346a1e918 Bump version to 7.12.0 2024-07-17 15:44:50 -04:00
Greyson Parrelli
9c391eb2c9 Update baseline profile. 2024-07-17 15:44:50 -04:00
Greyson Parrelli
f714e038a0 Update translations and other static files. 2024-07-17 15:30:28 -04:00
Greyson Parrelli
61405a62c2 Fix NPE in exoplayer transfer listener. 2024-07-17 15:21:14 -04:00
Alex Hart
d424a60345 Fix group call flickering missed. 2024-07-17 15:21:14 -04:00
Clark
3c10966a36 Add logging for Conversation activity restart due to config changes. 2024-07-17 15:21:14 -04:00
Clark
e210d5939c Dismiss battery saver prompt on continue. 2024-07-17 15:21:14 -04:00
Cody Henthorne
1fafcc69ff Improve large upload over slow connections. 2024-07-17 15:21:14 -04:00
Clark
3184368fa7 Various backup/restore bug fixes. 2024-07-17 15:21:14 -04:00
Greyson Parrelli
c622b7fdb1 Remove all legacy uploads to cdn0. 2024-07-17 15:21:14 -04:00
Alex Hart
29ead80e17 Remove outline on generated call link preview. 2024-07-17 15:21:14 -04:00
Alex Hart
9a72833e06 Filter out outgoing calls and call links in missed call filter. 2024-07-17 15:21:14 -04:00
Alex Hart
d0baf1dc95 Remove appbar offset listener when call log is unbound. 2024-07-17 15:21:14 -04:00
Michelle Tang
7f1227ee19 Add discard draft confirmation when editing. 2024-07-16 17:08:10 -07:00
Nicholas Tinsley
34c95dc082 Audio device logging. 2024-07-16 17:57:05 -04:00
Nicholas Tinsley
10ad73f201 Add logging around data source reading. 2024-07-16 17:48:47 -04:00
Nicholas Tinsley
21fab7c5ba Fix backing out of group story text send. 2024-07-16 17:36:14 -04:00
Nicholas Tinsley
e9c2f96bb9 Increase logging around attachment compression/upload job lifecycle. 2024-07-16 17:31:45 -04:00
Nicholas Tinsley
a950462451 Apply phone number formatting immediately. 2024-07-16 16:54:39 -04:00
Nicholas Tinsley
809317c0fd Run registration UI callbacks in LifeycleScope. 2024-07-16 16:54:39 -04:00
Cody Henthorne
6634540183 Fix generation and update baseline profile. 2024-07-16 16:54:39 -04:00
Alex Hart
89bfba3ee9 Backups subscription flow odds and ends. 2024-07-16 16:54:39 -04:00
Alex Konradi
97974291d2 Update to libsignal 0.52.2 2024-07-16 16:54:38 -04:00
Cody Henthorne
6daee5719b Allow for larger input videos for sending. 2024-07-16 16:54:38 -04:00
Clark
58443c46be Fix username constraint on re-reg from prod->staging. 2024-07-16 16:54:38 -04:00
Nicholas Tinsley
8cbecc2992 Upgrade libphonenumber to 8.13.40. 2024-07-16 16:54:38 -04:00
Cody Henthorne
4c0ca48af3 Handle ChatServiceException in response processors. 2024-07-16 16:54:38 -04:00
Greyson Parrelli
91eeda6c6e Allow RemoteConfig to be lazily initialized. 2024-07-16 16:54:38 -04:00
Greyson Parrelli
04e75c18dd Bump version to 7.11.3 2024-07-16 16:48:08 -04:00
Greyson Parrelli
f5fbfbc7fd Update translations and other static files. 2024-07-16 16:40:44 -04:00
Cody Henthorne
0e1df94b54 Fix processing incoming group invites. 2024-07-16 15:45:22 -04:00
Cody Henthorne
dd923629f6 Bump version to 7.11.2 2024-07-12 17:02:55 -04:00
Cody Henthorne
df8992aaca Update translations and other static files. 2024-07-12 16:57:01 -04:00
Cody Henthorne
63b9700865 Fix group sync message sent bug. 2024-07-12 16:48:12 -04:00
Nicholas Tinsley
d309877d63 Fix ISE in registration. 2024-07-12 16:47:51 -04:00
Nicholas Tinsley
f247fd78c6 Constant bitrate video encoding. 2024-07-12 11:01:03 -04:00
Cody Henthorne
4f96cb7439 Bump version to 7.11.1 2024-07-11 16:25:44 -04:00
Cody Henthorne
6e6e3a5eba Update translations and other static files. 2024-07-11 16:14:04 -04:00
Nicholas Tinsley
9166ed64fb Fix crash in nickname update button. 2024-07-11 16:02:15 -04:00
Nicholas Tinsley
25e4eaa8e8 Log successes in audio device switching. 2024-07-10 13:24:15 -04:00
Nicholas Tinsley
6e742ce770 Prevent crash from showing bottom sheet. 2024-07-09 18:20:15 -04:00
Nicholas Tinsley
c134c3033e Prevent ISE with view binding in registration. 2024-07-09 18:20:15 -04:00
Nicholas Tinsley
fb43a8257c Add more logging for rate limited scenarios. 2024-07-09 16:54:30 -04:00
Nicholas Tinsley
cecfe80d61 Simplify reg v2 nav graph names. 2024-07-09 16:13:22 -04:00
Cody Henthorne
6c302b708a Bump version to 7.11.0 2024-07-09 13:55:58 -04:00
Cody Henthorne
6a22919c50 Update baseline profile. 2024-07-09 13:49:15 -04:00
Cody Henthorne
9073ce5c7b Update translations and other static files. 2024-07-09 13:46:48 -04:00
Michelle Tang
9024c19169 Update device-specific notification support configs. 2024-07-09 13:40:41 -04:00
Arthur-GYT
60a0565ba8 Show max edits warning before editing.
Fixes #13428
Closes #13615

Signed-off-by: Arthur-GYT <a.gayot@ik.me>
2024-07-09 13:40:41 -04:00
Cody Henthorne
383f7556e3 Fix delete for everyone dialog option in note to self. 2024-07-09 13:40:41 -04:00
Greyson Parrelli
94795599e2 Inline the delete sync feature flag. 2024-07-09 13:40:41 -04:00
Nicholas Tinsley
84fbb7c466 Update "lower hand" label to "lower" 2024-07-09 13:40:41 -04:00
Cody Henthorne
c339f39b70 Fix camera manager memory leak. 2024-07-09 13:40:41 -04:00
Cody Henthorne
41a3609f06 Fix edit quote draft loading incorrectly bug. 2024-07-09 13:40:41 -04:00
Cody Henthorne
f5abd7acdf Add Group Send Endorsements support. 2024-07-09 13:40:41 -04:00
Nicholas Tinsley
414368e251 Prevent crash when trying to display Contact Support bottom sheet multiple times. 2024-07-09 13:40:41 -04:00
Jim Gustafson
a3d1197aef Update to RingRTC v2.44.3 2024-07-09 13:40:41 -04:00
Nicholas Tinsley
d91760eefc Upgrade AndroidX Media3 to 1.3.1. 2024-07-09 13:40:41 -04:00
Greyson Parrelli
ee20ced238 Switch MediaName to hex encoding. 2024-07-09 13:40:41 -04:00
Alex Hart
066892c11a Make group subtitles auto-update as names change. 2024-07-09 13:40:41 -04:00
Alex Hart
69fd4f79db Stop reading redemptionTime field. 2024-07-09 13:40:41 -04:00
Alex Hart
f49e2768c1 Fix crash in review card repository. 2024-07-09 13:40:41 -04:00
Greyson Parrelli
70378b85d7 Remove unused capabilities. 2024-07-09 13:40:41 -04:00
Nicholas Tinsley
585401a98e Do not download attachment if we do not have a digest. 2024-07-09 13:40:41 -04:00
Alex Hart
cf7ebfa03d Do not mark update unread if user was ever in the call. 2024-07-09 13:40:41 -04:00
Nicholas Tinsley
aec0a9951a Prevent backups from being scheduled twice within the jitter window.
Fixes #13559.
2024-07-09 13:40:41 -04:00
Nicholas Tinsley
b113eec940 Show "Update" action button on profile name change. 2024-07-09 13:40:41 -04:00
Michelle Tang
a966812bfc Add full send attachments. 2024-07-09 13:40:41 -04:00
Alex Hart
3879a8ffdb Pluralize backup strings. 2024-07-09 13:40:41 -04:00
Alex Hart
5b949b0116 Fix call id serialization. 2024-07-09 13:40:41 -04:00
Rashad Sookram
3c13619ce8 Update to RingRTC v2.44.2 2024-07-09 13:40:41 -04:00
Cody Henthorne
24bba98122 Fix sync thread delete sending another sync back. 2024-07-09 13:40:41 -04:00
Cody Henthorne
a96e5e6ae6 Fix delete sync capability updating on linked devices. 2024-07-09 13:40:41 -04:00
Alex Hart
4cfdfab31e Rename more in-app-payment classes. 2024-07-09 13:40:41 -04:00
Alex Hart
77d3116431 Rename DonationValues to InAppPaymentValues. 2024-07-09 13:40:41 -04:00
Alex Hart
b943df1ce4 Add translatable copy for backup alert fragment. 2024-07-09 13:40:41 -04:00
Alex Hart
8bbb7d56e0 Implements a bunch of missing things in the backup checkout flow stuff. 2024-07-09 13:40:41 -04:00
Clark
079a3d4fee Add import/export tests for contact messages and link previews. 2024-07-09 13:40:41 -04:00
Jim Gustafson
176e0e7765 Update to RingRTC v2.44.1 2024-07-09 13:40:41 -04:00
Greyson Parrelli
c73e80f8d9 Include username link entropy in backups. 2024-07-09 13:40:41 -04:00
Greyson Parrelli
47cd1b568f Add lock screen help dialog. 2024-07-09 13:40:41 -04:00
Clark
058c523329 Add support for import/export of shared contact messages. 2024-07-09 13:40:40 -04:00
Clark
84515482a6 Message backup support for link previews. 2024-07-09 13:40:40 -04:00
Chris Eager
02629020df Remove Option.RECAPTCHA from ProofRequiredException. 2024-07-09 13:40:40 -04:00
Clark
58d769b21f Allow exporting backup tests to binproto. 2024-07-09 13:40:40 -04:00
Greyson Parrelli
9dc67e0466 Do not use quote contents in edit sync messages.
Resolves #13571
2024-07-09 13:40:40 -04:00
Greyson Parrelli
72d02104dc Move StringExtensions to core-util-jvm. 2024-07-09 13:40:40 -04:00
Cody Henthorne
371a39049d Ignore flakey delete sync test. 2024-07-03 14:02:59 -04:00
Clark
47e4a6cf5a Regularly delete any archived media we don't know about. 2024-07-03 14:02:59 -04:00
Clark Chen
4a41e9f9a1 Bump version to 7.10.3 2024-07-02 11:21:45 -04:00
Clark Chen
9fa1b58019 Update translations and other static files. 2024-07-02 11:21:45 -04:00
Michelle Tang
c24473e176 Fix default name for linked devices. 2024-07-01 16:44:53 -04:00
mtang-signal
1311ec498f Default to back camera when linking device. 2024-07-01 08:59:35 -04:00
Nicholas Tinsley
251cec5dee Bump version to 7.10.2 2024-06-28 15:23:01 -04:00
Nicholas Tinsley
1e15a8c1d3 Update translations and other static files. 2024-06-28 15:22:12 -04:00
Michelle Tang
9c5c58794b Fix invalid qr code crash. 2024-06-27 16:12:38 -04:00
Nicholas Tinsley
50063854d7 Bump version to 7.10.1 2024-06-26 13:57:39 -04:00
Nicholas Tinsley
79d2041e46 Update translations and other static files. 2024-06-26 13:45:34 -04:00
Nicholas Tinsley
479b27ce94 Fix benchmark tests. 2024-06-26 13:39:28 -04:00
Cody Henthorne
a66857a7cc Fix incorrect local group state bug. 2024-06-26 13:39:28 -04:00
Alex Hart
37815a3f39 Fix bug with cut off avatars. 2024-06-26 13:39:28 -04:00
Alex Hart
b55ba67b66 Split out backup and subscription error sheet handling. 2024-06-26 13:39:28 -04:00
Michelle Tang
37a2d5fbca Release updated linked devices. 2024-06-26 13:39:28 -04:00
Nicholas Tinsley
d7b5c6bff3 Delete registration V1. 2024-06-26 13:39:28 -04:00
Alex Hart
f11028529e Fix hard coded placeholder in iDEAL dialog. 2024-06-25 10:19:27 -03:00
Greyson Parrelli
93ec322bb9 Bump version to 7.10.0 2024-06-24 15:37:57 -04:00
Greyson Parrelli
71e0468d2c Update translations and other static files. 2024-06-24 15:37:08 -04:00
moiseev-signal
816e3442a0 Adopt libsignal 0.51.1 2024-06-24 15:04:03 -04:00
Cody Henthorne
c37ed722dc Attempt to fix potential draft setting loop. 2024-06-24 15:04:03 -04:00
Michelle Tang
e08c2966c3 Move biometrics check when linking a device. 2024-06-24 15:04:03 -04:00
Cody Henthorne
976f80ff7e Fix conversation not closing after delete bug. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
34a4bda331 Do not send PNI-hello-worlds for new installs. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
a4077ccb4a Add app bundle support.
Co-authored-by: Joshua Lund <josh@signal.org>
2024-06-24 15:04:03 -04:00
Cody Henthorne
21ada2a503 Fix request to rejoin after group updates bug. 2024-06-24 15:04:03 -04:00
Alex Hart
57a70c3085 Quiet down auth check job. 2024-06-24 15:04:03 -04:00
Cody Henthorne
16c8b88f0f Fix multiple text attachments when forwarding to multiple recipients.
Fixes #13593
2024-06-24 15:04:03 -04:00
Michelle Tang
b806952430 Add device-specific support configs. 2024-06-24 15:04:03 -04:00
Alex Hart
c0da0bd272 Add proper payment method type to BackupTypeSettings screen. 2024-06-24 15:04:03 -04:00
Alex Hart
45239c2264 Add payment history screens for backups. 2024-06-24 15:04:03 -04:00
Alex Hart
690236c4e5 Handle manual cancellation UI hint in DonationValues. 2024-06-24 15:04:03 -04:00
Clark
ebee3f72e6 Add test infrastructure for backup binprotos. 2024-06-24 15:04:03 -04:00
Alex Hart
37cec7d44f Implement 1:1 call event delete syncs. 2024-06-24 15:04:03 -04:00
Alex Hart
187fd63a75 Add content for disk full alert for backups. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
362cdfc463 Use a snapshot of the SignalStore during backups. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
863b443317 Convert SignalStore to kotlin. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
341c474610 Remove some indirect database reads from backup export. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
cbb3c0911c Create backups from copies of the database file.
Still more work here to do with regards to certain tables, like
SignalStore and Recipient.
2024-06-24 15:04:03 -04:00
Greyson Parrelli
890facc6f6 Clean up ChatItemExportIterator. 2024-06-24 15:04:03 -04:00
Greyson Parrelli
6fa8337058 Update to the latest backup v2 spec.
Removes some dead protos, removes some sticker details, adds in gift
badges.
2024-06-24 15:04:03 -04:00
Alex Hart
3f1cb65e02 Migrate translatable strings from fragment to xml. 2024-06-24 15:04:03 -04:00
Cody Henthorne
3551e7ec00 Remove rx send remote config and group send using rx always. 2024-06-24 15:04:03 -04:00
Alex Hart
5ecf60a306 Add ability to turn off and delete backups. 2024-06-24 15:04:03 -04:00
Cody Henthorne
6659700a1c Improve delete sync coverage for partial expiring threads. 2024-06-24 15:04:02 -04:00
Cody Henthorne
070174fee6 Add delete sync capability to log section. 2024-06-24 15:04:02 -04:00
Cody Henthorne
09003d85b1 Add single attachment delete sync. 2024-06-24 15:04:02 -04:00
Alex Hart
ea87108def Heal InAppPaymentSubscriber currency if we have a payment with a matching subscriber id. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
7a696f9a62 Increase pluralization for raised hand snackbar. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
8ba57a2733 Upgrade OkHttp to 4.12.
Addresses #13491
2024-06-24 15:04:02 -04:00
Cody Henthorne
9824cc2cbe Update delete sync strings. 2024-06-24 15:04:02 -04:00
Michelle Tang
ad60cc72cb Add empty linked device state. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
1950b80402 Update AndroidX Camera libraries to 1.3.4. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
2acb47952b Differently pluralize raise hand strings. 2024-06-24 15:04:02 -04:00
Cody Henthorne
14cacaef86 Add verbose tracking to DelteForMeSync test to help finding flakey test. 2024-06-24 15:04:02 -04:00
Michelle Tang
958e815933 Remove ability to scan qr code from gallery. 2024-06-24 15:04:02 -04:00
Alex Hart
6b50be78c0 Implement start of backups payment integration work. 2024-06-24 15:04:02 -04:00
Michelle Tang
680223c4b6 Update permission buttons for contacts.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2024-06-24 15:04:02 -04:00
Greyson Parrelli
1af914d5ef Use SubsamplingImageView for everything except GIFs.
Fixes #10324
2024-06-24 15:04:02 -04:00
Cody Henthorne
a2fc710261 Add support for addressing attachments within a message. 2024-06-24 15:04:02 -04:00
Dan Brunwasser
10922594b3 Improve system emoji rendering across the app with EmojiCompat2.
Resolves #13327
2024-06-24 15:04:02 -04:00
Michelle Tang
abd80c5204 Update linked devices UI. 2024-06-24 15:04:02 -04:00
Alex Hart
ff589e3b91 Fix call link export crash. 2024-06-24 15:04:02 -04:00
Cody Henthorne
c80ccd70ec Add additional delete sync support. 2024-06-24 15:04:02 -04:00
Jim Gustafson
d22d18da47 Update to RingRTC v2.44.0 2024-06-24 15:04:02 -04:00
Clark
75b41c34ea Add import/export for stickers and sticker packs. 2024-06-24 15:04:02 -04:00
Alex Hart
11557e4815 Rewrite fallbackphoto system. 2024-06-24 15:04:02 -04:00
Greyson Parrelli
d698f74d0b Rename FeatureFlags -> RemoteConfig. 2024-06-24 15:04:02 -04:00
Greyson Parrelli
ecbea9fd95 Improve FeatureFlag change detection, use for SVR3. 2024-06-24 15:04:02 -04:00
Greyson Parrelli
13f7a64139 Refactor FeatureFlags. 2024-06-24 15:04:02 -04:00
Michelle Tang
39cb1c638e Remove sms tag from contacts. 2024-06-24 15:04:02 -04:00
Nicholas Tinsley
489b58ad67 Abort transcoding if frame processing gets stuck. 2024-06-24 15:04:02 -04:00
Cody Henthorne
f20fe33af9 Ignore flakey delete sync test. 2024-06-24 15:04:02 -04:00
Clark
6adddf4a0c Add display of last backup time to restore flow. 2024-06-24 15:04:01 -04:00
Clark Chen
16773c9b17 Fix import/export tests with my story. 2024-06-24 15:04:01 -04:00
Michelle Tang
4b50365fa9 Release updated linked devices behind feature flag. 2024-06-24 15:04:01 -04:00
Clark Chen
98766b9ebb Fix backup type dark mode. 2024-06-24 15:04:01 -04:00
Cody Henthorne
45a739ce92 Show notification for group adds. 2024-06-24 15:04:01 -04:00
Clark
c0d7145ada Add handling for "My Story" import/export. 2024-06-24 15:04:01 -04:00
Clark
f94c007af8 Make message backup settings screen update properly. 2024-06-24 15:04:01 -04:00
Michelle Tang
df19cb5795 Increase minimum height of keyboard. 2024-06-24 15:04:01 -04:00
Nicholas Tinsley
e6ceb55092 Match account deletion number by short NSN.
Fixes #13583.
2024-06-24 15:04:01 -04:00
Michelle Tang
bfe2b5cba9 Add loading screen to linked devices. 2024-06-21 09:19:47 -03:00
Alex Hart
571004df50 Tokenize group title search. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
f32b59f0aa Fix crash with AnalyzeDatabaseJob. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
e4019d8595 Ignore deprecated backup tests. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
0b66a8701e Convert FeatureFlags to kotlin. 2024-06-21 09:19:47 -03:00
Clark
e62b8de1bc Fix most of import/export integration tests. 2024-06-21 09:19:47 -03:00
Michelle Tang
d5cd790871 Remove redundant gallery permission ask. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
664c22d8f1 Add mostly-working SVR3 implementation behind flag. 2024-06-21 09:19:47 -03:00
Cody Henthorne
143a61e312 Fix calling error state display bugs. 2024-06-21 09:19:47 -03:00
Michelle Tang
baaad0e475 Fix camera-first qr scans. 2024-06-21 09:19:47 -03:00
Michelle Tang
7086709082 Update devices screen after linking a new device. 2024-06-21 09:19:47 -03:00
Michelle Tang
7bd5ad8c0b Use common compose qr screen for usernames. 2024-06-21 09:19:47 -03:00
Michelle Tang
df19c91ae2 Add padding to quoted messages. 2024-06-21 09:19:47 -03:00
Clark
e5872037e0 Add import/export of chat colors. 2024-06-21 09:19:47 -03:00
Clark
b782fabbb6 Update backup proto with subscriber and recipient changes. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
485b466bd2 Crash on RuntimeExceptions thrown during all Jobs. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
3beac6dfa9 Fix linked device inactive filtering. 2024-06-21 09:19:47 -03:00
Michelle Tang
98290a9fa3 Update max limit string. 2024-06-21 09:19:47 -03:00
Michelle Tang
13dd59f226 Skip biometrics check if unavailable when linking a device. 2024-06-21 09:19:47 -03:00
Michelle Tang
d9c42a4135 Add ability to scan linked device qr code from gallery. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
644b93e5a3 Provide default text background color. 2024-06-21 09:19:47 -03:00
Greyson Parrelli
3ff218f9c6 Make build deprecation more resilient to clock skew. 2024-06-21 09:19:47 -03:00
Alex Hart
f572eb5322 Add CallLink Observed event and handling. 2024-06-21 09:19:47 -03:00
Michelle Tang
d3eb480d31 Update add linked devices screen. 2024-06-21 09:19:47 -03:00
Michelle Tang
ac52b5b992 Update linked devices screen. 2024-06-21 09:19:47 -03:00
Michelle Tang
5c181e774f Prevent editing on stickers. 2024-06-21 09:19:46 -03:00
Michelle Tang
05d25718da Add animation when editing a message. 2024-06-21 09:19:46 -03:00
Clark
66c50bef44 Hook up message backup restore flow to reg v2.
Co-authored-by: Nicholas Tinsley <nicholas@signal.org>
2024-06-21 09:19:46 -03:00
Alex Hart
26bd59c378 Bump version to 7.9.6 2024-06-20 18:13:53 -03:00
Alex Hart
e90eae6080 Update baseline profile. 2024-06-20 18:08:30 -03:00
Alex Hart
86a7db7653 Update translations and other static files. 2024-06-20 18:05:24 -03:00
Cody Henthorne
230de7e9dc Use URL for S3.
Thanks to Oscar Mira <valldrac@molly.im> for bringing this to our attention.
2024-06-20 12:15:49 -04:00
Greyson Parrelli
4b8546a151 Bump version to 7.9.5 2024-06-14 15:16:17 -04:00
Greyson Parrelli
ecd214b91b Update translations and other static files. 2024-06-14 15:15:35 -04:00
Greyson Parrelli
6b5de6e3e5 Only do local donation cancel if it's currently active. 2024-06-14 15:06:13 -04:00
Greyson Parrelli
58b6e49aae Fix NPE when canceling a donation. 2024-06-14 15:06:13 -04:00
Greyson Parrelli
c480512600 Bump version to 7.9.4 2024-06-13 18:34:38 -04:00
Greyson Parrelli
3a5b6476aa Updated language translations. 2024-06-13 18:32:18 -04:00
Alex Hart
cb171092cf Fix crash loop when writing invalid currency . 2024-06-13 18:03:20 -03:00
Nicholas Tinsley
71979b34db Alert user to file system errors during backup restore. 2024-06-13 16:13:43 -04:00
Nicholas Tinsley
73142cea39 Don't hold lazy reference to view binding in delayed runnable. 2024-06-13 15:43:51 -04:00
Nicholas Tinsley
2ab2c6f039 Ensure that substrings match in the registration contact support bottom sheet. 2024-06-13 15:36:41 -04:00
Greyson Parrelli
0bea15c0af Bump version to 7.9.3 2024-06-12 11:23:20 -04:00
Greyson Parrelli
cbd78d78ba Update translations and other static files. 2024-06-12 11:22:44 -04:00
Cody Henthorne
ac0604a753 Fix rare remote megaphone crash. 2024-06-12 11:10:59 -04:00
Nicholas Tinsley
0e57335be1 Split Raise Hand plurals into separate strings. 2024-06-11 11:43:58 -04:00
Greyson Parrelli
c4e64f6fa3 Bump version to 7.9.2 2024-06-10 16:11:51 -04:00
Greyson Parrelli
bf9716f206 Update translations and other static files. 2024-06-10 16:10:42 -04:00
Cody Henthorne
057ffdbaaf Fix conversation memory leak. 2024-06-10 14:54:02 -04:00
Nicholas Tinsley
65dc0d3f34 Disable verbose logging in media converter. 2024-06-10 14:38:19 -04:00
Clark
173ee95e62 Fix backup jitter and add unit tests. 2024-06-10 14:20:56 -04:00
Nicholas Tinsley
789339afa7 Update Raise Hand string. 2024-06-10 11:01:56 -04:00
Nicholas Tinsley
21b518da7a Don't show volume indicator nor switch camera button until incoming call connects. 2024-06-10 11:01:56 -04:00
Nicholas Tinsley
57b6b8dcf1 Improve Raise Hand behavior when in a call with a linked device. 2024-06-07 13:53:58 -04:00
Cody Henthorne
543a85316e Improve FCM check clock skew handling. 2024-06-07 13:02:44 -04:00
Cody Henthorne
2fedb3a0ee Bump version to 7.9.1 2024-06-07 11:59:02 -04:00
Cody Henthorne
ae450aed67 Update baseline profile. 2024-06-07 11:48:08 -04:00
Cody Henthorne
0abb4727fc Update translations and other static files. 2024-06-07 11:30:34 -04:00
Alex Hart
4bc6eb96ff Fix 3DS waiting-for-auth state when launching external application. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
e6a126d416 Only submit captcha once in Registration V2. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
fdf858f379 Prevent crash if linked device also raises their hand. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
4151d123cd Fix crash in registration v2 country code drop down. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
c8a9759eba Unify Raise Hand copy. 2024-06-07 11:05:09 -04:00
Cody Henthorne
c59b74627f Improve strings for localization. 2024-06-07 11:05:09 -04:00
Nicholas Tinsley
f2191d2996 Adjust text colors in dark mode in Registration V2. 2024-06-07 11:05:09 -04:00
Cody Henthorne
7dfffbd50b Add missing windows aapt2. 2024-06-06 10:21:22 -04:00
Cody Henthorne
329fc52077 Bump version to 7.9.0 2024-06-05 16:20:29 -04:00
Cody Henthorne
8976111f61 Update translations and other static files. 2024-06-05 15:54:32 -04:00
Cody Henthorne
7402959ac6 Fix error handling for resumable uploads to cdn3. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
220d3877a2 Fix phone number autofill in Registration V2. 2024-06-05 15:46:01 -04:00
Clark Chen
380c33642c Clean quote when canceling edit message. 2024-06-05 15:46:01 -04:00
Alex Hart
7acb2bef3d Fix private story listing My Story as a recipient. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
1a103106a5 Catch more stuff in SqlCipherDeletingErrorHandler.
Fixes #13577
2024-06-05 15:46:01 -04:00
Greyson Parrelli
6025e423e8 Fix payment request message text. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
54656ea14e Potentially fix 'design assumption violated' ISE. 2024-06-05 15:46:01 -04:00
Alex Hart
fd00ed71b5 Fix clickable link in donation thanks sheet. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
d4fba5f3c7 Ship Raise Hand. 2024-06-05 15:46:01 -04:00
Alex Hart
ce244f2e8f Fix hit detection for story link previews. 2024-06-05 15:46:01 -04:00
Cody Henthorne
4ad466390f Fix second person translations for group story reactions. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
c5c9b09f7b Turn on Registration V2 and Change Number V2. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
0638b31c1f More registration lock V2 improvements. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
c3c713a75a Prevent getting stuck on registration lock V2 fragment. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
9af1c72233 Registration V2 restore tweaks. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
500a1e46ad Improve ABS logging. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
4b446877af Change number captcha submission improvement. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
015548613a Fix change view model test.
Co-authored-by: Cody Henthorne <cody@signal.org>
2024-06-05 15:46:01 -04:00
Alex Hart
30b339a482 Remove unnecessary subscriber call for manual cancel. 2024-06-05 15:46:01 -04:00
Cody Henthorne
6dcb2e8d24 Fix androidTest message content fuzer for attachment pointers. 2024-06-05 15:46:01 -04:00
Cody Henthorne
3e8e17526b Fix missing group context on message records. 2024-06-05 15:46:01 -04:00
Clark Chen
30ecaf7aea Remove double remote deleted column from ChatItemImportInserter. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
f761008509 Clean up some stuff around ImportExportTest. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
c3ab8dddd0 Fix runPostSuccessfulTransaction behavior. 2024-06-05 15:46:01 -04:00
Alex Hart
164f089d37 Fix NPE in deleteAll call. 2024-06-05 15:46:01 -04:00
Alex Hart
a021b400bd Fix SafetyNumberBottomSheetRepositoryTest. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
fac8f403be Remove outdated dlist test. 2024-06-05 15:46:01 -04:00
Clark
d85ab37828 Add import and tombstones for mobile coin payments. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
1e35403c87 Change Number V2. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
b99c2165fa Handle SVR exceptions in Registration V2. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
303100bb6b Further registration lock improvements in Registration V2. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
b71ba79b8a Flesh out registration lock support for Registration V2. 2024-06-05 15:46:01 -04:00
Clark
54cd84b842 Add handling for import/export of edited messages. 2024-06-05 15:46:01 -04:00
Clark
1565ecdcea Fix multiple scheduled backups due to jitter. 2024-06-05 15:46:01 -04:00
Clark
0a99b68d25 Fix shared contacts avatar double upload. 2024-06-05 15:46:01 -04:00
Alex Hart
f4fac5bd90 Prevent reactions from being overlaid by raise hand. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
f6760b90da Flesh out verification challenge support for registration v2. 2024-06-05 15:46:01 -04:00
Clark
ad9b1f05b4 Disable restore on open if auto-download is off. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
17581a7a5e Update SignalProgressDialog.
Fixes #12949.
2024-06-05 15:46:01 -04:00
Clark
b41bf66133 Disable "Edited" click listener for outgoing messages. 2024-06-05 15:46:01 -04:00
Clark
8bb3d71472 Fix thumbnail not being clickable on initial media receive. 2024-06-05 15:46:01 -04:00
Nicholas Tinsley
295d4b9466 Pin left GIF button to bottom of compose box. 2024-06-05 15:46:01 -04:00
Ahmed El herz
5e490376f4 Fix initial event not triggering onTouchEvent.
Fixes #13351
2024-06-05 15:46:01 -04:00
Greyson Parrelli
fa27531c00 Inline SVR2 feature flag. 2024-06-05 15:46:01 -04:00
Cody Henthorne
2737e5613c Use raw values for learned profile name event. 2024-06-05 15:46:01 -04:00
Greyson Parrelli
d84612ebf4 Revert "Validate full edit message payload."
This reverts commit 268b621667e3144fb3f07099d04aa5609387a5e6.
2024-06-05 15:46:00 -04:00
Greyson Parrelli
96165ad5a8 Fix username getting prematurely removed from recipient. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
19caef057e Update to AGP 8.4.1. 2024-06-05 15:46:00 -04:00
Clark
29cafb11eb Update proto and add payments export without tombstone. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
7e458bfde0 Validate full edit message payload. 2024-06-05 15:46:00 -04:00
Clark
2a3cb80217 Add ui wiring for archive thumbnail support. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
3d382ee15e Use extension functions instead of LibSignalNetwork class. 2024-06-05 15:46:00 -04:00
Clark
6069dfc6f8 Add a separate column for tracking thumbnail restore state. 2024-06-05 15:46:00 -04:00
Clark Chen
dee19ed94a Fix attachment table v231 migration consistency. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
905b0681f5 Update otpk/kpk tests. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
b6a4e1f145 Rewrite the AppDependencies system. 2024-06-05 15:46:00 -04:00
Alex Hart
a0131bf39b Fix db inconsistency. 2024-06-05 15:46:00 -04:00
Alex Hart
7ed77a00df Remove unused method from RecipientTable. 2024-06-05 15:46:00 -04:00
Alex Hart
887c173d8f Move camera flip and improve movement of some ui elements. 2024-06-05 15:46:00 -04:00
Cody Henthorne
6362da7a50 Refactor group state processing. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
1296365bed Upgrade to libsignal 0.47.0 2024-06-05 15:46:00 -04:00
Alex Hart
99ae7c5961 Add search view extension for incognito keyboards. 2024-06-05 15:46:00 -04:00
Clark
5c3ea712fe Add streaming video support for attachment files. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
bc5cb454bf Ship "instant" video playback. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
8a7c2c1e20 Rotate libsignal CDS feature flag. 2024-06-05 15:46:00 -04:00
Cody Henthorne
a81a675d59 Add Delete for Me sync support. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
1c66da7873 Update slow notification debugging info. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
afe3cd1098 Additional error handling for registration v2. 2024-06-05 15:46:00 -04:00
Alex Hart
4f3ee9ca1d Skip the contact links migration if contact permissions are disabled. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
7771aaa501 Sort the language list during build for determinism.
Addresses #13565.
2024-06-05 15:46:00 -04:00
Greyson Parrelli
5ad38c7960 Ensure archive data is copied when deduping. 2024-06-05 15:46:00 -04:00
Alex Hart
0fb1514da2 Consolidate subscription information and manage button to a single row. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
f37efd7e15 Add additional error handling for registration v2. 2024-06-05 15:46:00 -04:00
mtang-signal
1ae2464df1 Update remaining gallery permission UI. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
0425b70d31 Do not show unregistered contacts in search results. 2024-06-05 15:46:00 -04:00
Clark
7b0d3f36dc Ignore digest for downloading archived thumbnails. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
14b917dc7e Fix typo in query resulting in contacts not being unlinked. 2024-06-05 15:46:00 -04:00
Ehren Kret
6184cc0307 Migrate existing raw contacts to add video call links. 2024-06-05 15:46:00 -04:00
Nicholas Tinsley
870aa8e7b0 Fix country code sorting in reg v2. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
d88016669b Shorten groupId string patterns. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
a464b413d9 Use correct label in log. 2024-06-05 15:46:00 -04:00
Alex Hart
d719edf104 Rewrite in-app-payment flows to prepare for backups support. 2024-06-05 15:46:00 -04:00
mtang-signal
b36b00a11c Update camera permission UI for voice calls. 2024-06-05 15:46:00 -04:00
mtang-signal
a99db2b16e Update camera permission UI for usernames. 2024-06-05 15:46:00 -04:00
Greyson Parrelli
2744dec43a Switch to using dateSent for jump-to-calendar.
We use dateSent for date dividers, but were using dateReceived for
calendar date availability, which would occasionally result in a
mismatch. Switched to use the same thing we use for date dividers.
2024-06-05 15:45:59 -04:00
Greyson Parrelli
6f2cce1494 Add acknowledgements from libsigna/ringrtc. 2024-06-05 15:45:59 -04:00
Greyson Parrelli
689ee243aa Fix potential sqlite conflict in dlist on recipient remap. 2024-06-05 15:45:59 -04:00
Cody Henthorne
537fc0ef5c Update to Kotlin 1.9.20, AGP 8.4.0, and Gradle 8.6 2024-06-05 15:45:59 -04:00
Clark Chen
e647b31f29 Explicitly persist message backup tier. 2024-05-17 10:39:09 -04:00
Alex Hart
b59932cd88 Fix compilation error in contacts test app. 2024-05-17 09:33:22 -04:00
Nicholas Tinsley
cfb4377de3 Apply automated ktlint 1.2.1 formatting. 2024-05-17 09:33:22 -04:00
Nicholas Tinsley
e861c022da Disable new ktlint rules with preexisting violations. 2024-05-17 09:33:22 -04:00
Nicholas Tinsley
59006d3182 Upgrade ktlint to 1.2.1. 2024-05-17 09:33:22 -04:00
Nicholas Tinsley
503faea3a9 Support voice verification in registration v2. 2024-05-17 09:33:22 -04:00
Nicholas Tinsley
eb114de5c8 Bump version to 7.8.1 2024-05-16 15:50:48 -04:00
Nicholas Tinsley
1bf9695cff Update translations and other static files. 2024-05-16 15:45:51 -04:00
Clark
241bf065e8 Fix missing thumbnail_file column in media query. 2024-05-16 13:09:58 -04:00
Clark Chen
e0f3b35805 Fix missing archive_thumbnail_cdn column. 2024-05-16 12:30:33 -04:00
Nicholas Tinsley
5741dfc00b Bump version to 7.8.0 2024-05-16 10:24:48 -04:00
Nicholas Tinsley
ec430da772 Update translations and other static files. 2024-05-16 10:20:08 -04:00
Rashad Sookram
5e6d9434de Update to RingRTC v2.42.0 2024-05-16 10:16:10 -04:00
Clark
b72d586748 Add initial thumbnail restore for message backup. 2024-05-16 10:16:10 -04:00
Ehren Kret
757c0fd2ea create video call mimetype for raw contacts links 2024-05-16 10:16:10 -04:00
Nicholas Tinsley
c4e4eaf110 Remove lower hand confirmation dialog. 2024-05-16 10:16:10 -04:00
Nicholas Tinsley
f83275e246 Add customize button to in-call reaction picker. 2024-05-16 10:16:10 -04:00
Greyson Parrelli
d0340d39db Reset backupV2 credentials on 403. 2024-05-16 10:16:10 -04:00
Greyson Parrelli
227a279131 Make sure note to self is included in backupsV2. 2024-05-16 10:16:10 -04:00
mtang-signal
0465fdea62 Update contacts permission UI. 2024-05-16 10:16:10 -04:00
Nicholas Tinsley
13bd4a9c74 Update regv2 result field name. 2024-05-16 10:16:10 -04:00
Greyson Parrelli
f570f1f2c4 Initial test implementation of SVR3. 2024-05-15 15:55:22 -04:00
Nicholas Tinsley
68ced18ea1 Fleshed out session management in registration v2. 2024-05-15 15:55:22 -04:00
Greyson Parrelli
b4a8f01980 Include message timestamp in local send timings. 2024-05-15 15:55:22 -04:00
mtang-signal
c3c743fbb8 Update camera permission UI in media. 2024-05-15 15:55:22 -04:00
Adam Mork
b14eddefc9 Add payment enclave measurements for v6.0.0 2024-05-15 15:55:21 -04:00
Nicholas Tinsley
46638a1948 Bump version to 7.7.2 2024-05-15 15:54:11 -04:00
Nicholas Tinsley
5cee85fcdc Update translations and other static files. 2024-05-15 15:44:52 -04:00
mtang-signal
f97d7e3dfd Fix permissions ask in gallery. 2024-05-15 12:18:50 -07:00
Alex Hart
6da0ecf827 Bump version to 7.7.1 2024-05-10 22:54:45 -03:00
Alex Hart
9803550bba Update baseline profile. 2024-05-10 22:51:55 -03:00
Alex Hart
15284da4c5 Update translations and other static files. 2024-05-10 22:48:55 -03:00
Alex Hart
351c3219e4 Replace RxStore with MutableStateFlow for better lifecycle control. 2024-05-10 22:44:05 -03:00
Alex Hart
ab95dbbc77 Bump version to 7.7.0 2024-05-08 16:43:30 -03:00
Alex Hart
cc6cba45c6 Update baseline profile. 2024-05-08 16:36:44 -03:00
Alex Hart
ce37660df2 Update translations and other static files. 2024-05-08 16:34:17 -03:00
Nicholas Tinsley
ca14ed9b2c Allow for captcha solving for reg v2. 2024-05-08 16:30:53 -03:00
Clark
ba4cdea75d Add cellular backup toggle for message backup. 2024-05-08 16:30:53 -03:00
Clark
83c34dd4cc Integrate swapping backup tiers from backup settings. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
b6db3802d3 Set raised hand list to be distinct by RecipientID. 2024-05-08 16:30:53 -03:00
Greyson Parrelli
a9a19d3ae0 Add job to upload thumbnails to archive. 2024-05-08 16:30:53 -03:00
Alex Hart
52fb873b1b Specify vibrate attributes to resolve vibrate from background. 2024-05-08 16:30:53 -03:00
moiseev-signal
9a0bb243cd Implement a libsignal-net shadowing web socket. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
78bbab37fb Show missing FCM dialog in registration V2. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
9af73b1409 Allow initialization of registration V2 without FCM. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
9c5bb4aa17 Initial error handling for registration v2. 2024-05-08 16:30:53 -03:00
Clark
49ba83dda8 Integrate message backup frequency. 2024-05-08 16:30:53 -03:00
Clark
de3b0d4ca2 Integrate the backup size into backup settings. 2024-05-08 16:30:53 -03:00
fm-sys
b2efc42357 Add back ability to long press title bar to go to system contact.
Resolves #13372
2024-05-08 16:30:53 -03:00
Cody Henthorne
a71faf674d Cleanup group management code. 2024-05-08 16:30:53 -03:00
moiseev-signal
34faa9003f Upgrade to libsignal 0.46.0. 2024-05-08 16:30:53 -03:00
Clark
bc527a2bc1 Basic settings functionality for message backup. 2024-05-08 16:30:53 -03:00
Nicholas Tinsley
0a3f96935a Support device transfers in restore flow v2. 2024-05-08 16:30:53 -03:00
Alex Konradi
35232a3928 Unwrap ExecutionException from Future observable 2024-05-08 16:30:53 -03:00
Alex Hart
70d74e0bb1 Allow users who have disabled Contacts permission to hide system contacts. 2024-05-08 16:30:53 -03:00
Alex Hart
36c91a95e2 Check spannable intersect when moving between stories. 2024-05-08 16:30:53 -03:00
Greyson Parrelli
4600e38a2a Add partial index to improve unread count perf. 2024-05-08 16:30:52 -03:00
Alex Hart
55abd88a03 Implement better handling for call peeking when opening the calls tab. 2024-05-08 16:30:52 -03:00
mtang-signal
cd880b0879 Expand double tap area. 2024-05-08 16:30:52 -03:00
mtang-signal
bbae6d876f Avoid translating support email strings. 2024-05-08 16:30:52 -03:00
dalamsya50
48a0c5a5a9 Fix git error when running on GitHub Actions.
Fixes #13495
Resolves #13547
2024-05-08 16:30:52 -03:00
BenjaminMuslic
c261df41b0 Added automatic capitalization to profile name fields.
Resolves #13544
2024-05-08 16:30:52 -03:00
Greyson Parrelli
cc98eced27 Short-circuit query if list is empty. 2024-05-08 16:30:52 -03:00
moiseev-signal
452d5960e4 Add test and extra cleanup around usage of incremental mac. 2024-05-08 16:30:52 -03:00
mtang-signal
c95b180728 Update gallery permission UI 2024-05-08 16:30:52 -03:00
Greyson Parrelli
3c380d35fd Attempt to reduce impact of thread updates. 2024-05-08 16:30:52 -03:00
Nicholas Tinsley
41935120e5 DeviceTransferFragment Kotlin rewrite. 2024-05-01 16:45:36 -04:00
Alex Hart
03d8f72c41 Fix group collisions error. 2024-05-01 16:45:36 -04:00
Greyson Parrelli
ab9ecff4d4 Improve timing of query methods. 2024-05-01 16:45:36 -04:00
Alex Hart
e351a0b235 Correct flags for story replies. 2024-05-01 16:45:36 -04:00
Nicholas Tinsley
4a08de370a Fix issue with Mp4Writer with massive time scales. 2024-05-01 16:45:36 -04:00
Cody Henthorne
6d657b449c Convert and update Manage Storage Settings. 2024-05-01 16:45:36 -04:00
Clark
adef572abb Store group snapshot attributes in GroupAttributeBlobs. 2024-05-01 16:45:36 -04:00
Greyson Parrelli
d6f2039bd1 Update attachment cipher tests to use longer inputs. 2024-05-01 16:45:35 -04:00
Clark
1223c3c768 Add support for new backup calls proto and call links. 2024-05-01 16:45:35 -04:00
Greyson Parrelli
333fa22c96 Bump version to 7.6.2 2024-05-01 16:42:29 -04:00
Greyson Parrelli
76c04d8d6d Update translations and other static files. 2024-05-01 16:42:03 -04:00
Greyson Parrelli
c3070f2913 Revert "Expand double tap touch area."
This reverts commit 8c81e47737.
2024-05-01 16:33:33 -04:00
Nicholas Tinsley
234b3967ed Fix button crash in v1 PIN restore fragment. 2024-05-01 10:42:23 -04:00
Greyson Parrelli
89d420cda8 Bump version to 7.6.1 2024-04-30 16:42:44 -04:00
Greyson Parrelli
ced4ece5b8 Update translations and other static files. 2024-04-30 16:41:02 -04:00
mtang-signal
8c81e47737 Expand double tap touch area. 2024-04-30 16:29:33 -04:00
Cody Henthorne
5d15eef61d Improve translations with pluralized string resources. 2024-04-30 16:04:14 -04:00
Greyson Parrelli
8f3e62245f Fix some issues where views were accessed after being destroyed. 2024-04-30 15:22:57 -04:00
Greyson Parrelli
e4ab795c62 Fix stream reading error. 2024-04-30 15:22:57 -04:00
mtang-signal
e4d6f9240f Fix double tap layout warning. 2024-04-30 14:44:00 -04:00
Greyson Parrelli
cfaf40e605 Fix KeyValueDataSet tests. 2024-04-30 11:05:39 -07:00
Greyson Parrelli
bdcf2431e7 Bump version to 7.6.0 2024-04-29 22:04:56 -04:00
Greyson Parrelli
7241283be2 Update baseline profile. 2024-04-29 22:04:56 -04:00
Greyson Parrelli
dde2a8b63a Update translations and other static files. 2024-04-29 22:04:56 -04:00
Greyson Parrelli
f7763a5b82 Be more lenient around long-int conversion in SignalStore. 2024-04-29 22:04:47 -04:00
Greyson Parrelli
c6f4a01001 Hopeful fix for crash in SimpleProgressDialog. 2024-04-29 22:04:31 -04:00
Greyson Parrelli
95a6835988 Improve handling of backup initialization. 2024-04-29 19:26:06 -04:00
moiseev-signal
f9a8f447d2 Support proxy in connections managed by libsignal. 2024-04-29 19:26:06 -04:00
Nicholas Tinsley
d20f588802 Inline the group call reactions feature flag. 2024-04-29 19:25:59 -04:00
Nicholas Tinsley
f23476a4e9 Initial support for restoring backups and skipping SMS in registration v2. 2024-04-29 19:25:59 -04:00
mtang-signal
fd4864b3b1 Update microphone permission UI for calls. 2024-04-29 19:25:59 -04:00
mtang-signal
c5c0c432c4 Update microphone permission UI for voice messages. 2024-04-29 19:25:59 -04:00
Jim Gustafson
69c40a6835 Update to RingRTC v2.41.0 2024-04-29 19:25:59 -04:00
moiseev-signal
7ef7aa65e6 Upgrade to libsignal 0.45.1. 2024-04-29 19:25:59 -04:00
Greyson Parrelli
97c08f0d52 Add additional validations to incremental attachment streams. 2024-04-29 19:25:59 -04:00
mtang-signal
18e6c57e75 Update location permission UI. 2024-04-29 19:25:59 -04:00
mtang-signal
ffc1463cda Add double tap editing feature. 2024-04-29 19:25:59 -04:00
Clark
84e654efb2 Set archive transfer state when archive data is set. 2024-04-29 19:25:59 -04:00
Clark
d983265e08 Persist group state in backup. 2024-04-29 19:25:59 -04:00
Alex Hart
e60b32202e Improved missed call state handling. 2024-04-29 19:25:59 -04:00
moiseev-signal
95fbd7a31c Implement unauthenticated chat web socket connection via libsignal-net. 2024-04-29 19:25:59 -04:00
Nicholas Tinsley
00a91e32fc Multiple skin tones for reaction bursts. 2024-04-29 19:25:59 -04:00
Alex Hart
fa32b7a883 Fix coloring on outgoing calls. 2024-04-24 15:10:12 -03:00
Alex Hart
63e6f955ed Prevent getCallLinks from returning links without root keys. 2024-04-24 14:17:58 -03:00
Alex Hart
7dcb8a425a Handle joined sync message for call links. 2024-04-24 13:31:35 -03:00
Cody Henthorne
f35ce068f9 Change profile fetch REST fallback based on authentication error. 2024-04-24 11:41:31 -04:00
Nicholas Tinsley
881d231a93 Improve group call reactions UI when presented without raise hand.
This also dismisses the custom reaction picker when switching to PiP mode.
2024-04-24 10:13:56 -04:00
Alex Hart
293634c758 Send call link update sync message upon call link creation. 2024-04-24 10:48:00 -03:00
Greyson Parrelli
4134df3f35 Use archive-specific endpoint for attachment backfill. 2024-04-23 16:29:03 -04:00
Clark
f78a019c70 Use seconds instead of millis for redemption time. 2024-04-23 15:56:38 -04:00
Cody Henthorne
d561a1385c Fix extremely long emoji search crash. 2024-04-23 12:29:03 -04:00
moiseev-signal
9b5387e221 Upgrade to libsignal 0.45.0 2024-04-23 12:29:03 -04:00
Cody Henthorne
25b1a814fe Remove legacy keyword search flag from emoji search infra. 2024-04-23 12:29:03 -04:00
Clark
b043b6e458 Schedule message backups when enabled. 2024-04-23 12:29:03 -04:00
Clark
8a972d93e9 Actually use backup jitter in local backups. 2024-04-23 12:29:03 -04:00
Cody Henthorne
8fe66a14c5 Fix multi-window camera crash. 2024-04-23 12:29:03 -04:00
Clark
f82bd64c10 Copy inbound attachments to archive service. 2024-04-23 12:29:03 -04:00
Nicholas Tinsley
4bcab49539 Correct UnopinionatedResponseCodeHandler constant name. 2024-04-23 12:29:03 -04:00
mtang-signal
0f4618ab11 Remove link preview images from shared media. 2024-04-23 12:29:03 -04:00
Cody Henthorne
475ca50fab Fix missing local participant state changes in group calls bug. 2024-04-23 12:29:03 -04:00
Greyson Parrelli
a64a02fa0c Fix issue where structured contact name syncing was delayed. 2024-04-23 12:29:02 -04:00
Clark
f3669a5865 Fix message extra column not being restored properly. 2024-04-23 12:29:02 -04:00
Greyson Parrelli
34dbd11db0 Update file format for backupV2. 2024-04-23 12:29:02 -04:00
Nicholas Tinsley
2e7279c72f Only display "Processing" text on outgoing media. 2024-04-23 12:29:02 -04:00
Nicholas Tinsley
6ad72f00af Fix phone number formatter in Registration V2. 2024-04-23 12:29:02 -04:00
Alex Hart
b771a21518 Add screen for managing backup type. 2024-04-23 12:29:02 -04:00
Greyson Parrelli
04fb459acd Remove unused backup outputstream class. 2024-04-23 12:29:02 -04:00
Greyson Parrelli
690a68f0d0 Remove libweb submodule entirely. 2024-04-23 12:29:02 -04:00
Greyson Parrelli
f34ae8d118 Add padding to the gzipped backup output. 2024-04-23 12:29:02 -04:00
Cody Henthorne
da43ff1e95 Bump version to 7.5.2 2024-04-23 11:42:24 -04:00
Cody Henthorne
f053ebbd51 Update baseline profile. 2024-04-23 11:36:15 -04:00
Cody Henthorne
87606af29c Update translations and other static files. 2024-04-23 11:30:56 -04:00
Cody Henthorne
c811bdcffa Fix benchmark test messages. 2024-04-23 11:26:36 -04:00
Cody Henthorne
0536628da3 Stagger app wake ups due to analyze database alarm. 2024-04-23 10:44:09 -04:00
Nicholas Tinsley
1fa53cfcb8 Prevent crash on attachment delete while voice note system tone is playing. 2024-04-23 10:22:01 -04:00
Cody Henthorne
a9ea3854d2 Bump version to 7.5.1 2024-04-22 17:06:18 -04:00
Cody Henthorne
dc35261e00 Update translations and other static files. 2024-04-22 16:56:39 -04:00
Cody Henthorne
716bc1f5e7 Cleanup dangling domain reference. 2024-04-22 16:52:02 -04:00
Cody Henthorne
db27204084 Validate pni signature message. 2024-04-22 16:33:03 -04:00
Cody Henthorne
42aeceffe2 Revert full usage of ActiveCallManager. 2024-04-22 16:32:27 -04:00
Greyson Parrelli
03845eabaf Bump version to 7.5.0 2024-04-18 16:44:32 -04:00
Greyson Parrelli
62af9dad50 Update translations and other static files. 2024-04-18 16:43:51 -04:00
Cody Henthorne
ee58d47926 Cycle rx message sending flag. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
d74260b536 Improve network reliability. 2024-04-18 16:24:13 -04:00
Alex Hart
15d8a698c5 Add new name collision state management. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
62cf3feeaa Restore a Local Backup v2 2024-04-18 16:24:13 -04:00
Alex Hart
947ab7d48b Implement skeleton for backup sheets. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
a82b9ee25f Add a job to backfill attachment uploads to the archive service. 2024-04-18 16:24:13 -04:00
mtang-signal
1e4d96b7c4 Add camera permission check to group stories. 2024-04-18 16:24:13 -04:00
Alex Hart
735a8e680c Add backupSubscription field to configuration object. 2024-04-18 16:24:13 -04:00
Alex Hart
d9e9fe1d6a Move backups selection code to its own package. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
4bcd1df4f8 Expand account consistency checks. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
9762899272 Remove old thread remappings. 2024-04-18 16:24:13 -04:00
Alex Hart
ce1b73970c Implement BackupStatus widget. 2024-04-18 16:24:13 -04:00
Alex Hart
58282e589b Implement backups settings fragment. 2024-04-18 16:24:13 -04:00
mtang-signal
75bd113545 Fix missing send button for voice notes. 2024-04-18 16:24:13 -04:00
Cody Henthorne
7a6bd0e1f2 Revert "Remove vestigial call camera toggle button."
This reverts commit 7a9c01e6e5.
2024-04-18 16:24:13 -04:00
Greyson Parrelli
f673c4eb83 Remove sql language annotation (for now).
It's broken in newer versions of Android Studio. It doesn't seem to
allow partial-sql anymore, only fully-formed statements. Same with
roomsql.
2024-04-18 16:24:13 -04:00
Jim Gustafson
cbb04e8f0c Update to RingRTC v2.40.0 2024-04-18 16:24:13 -04:00
mtang-signal
cd03da54d5 Fix note to self message detail text. 2024-04-18 16:24:13 -04:00
Clark
5f31f5966c Update backup locator proto. 2024-04-18 16:24:13 -04:00
Clark
d8bbfe2678 Add archived media sync job. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
7a2d408ca2 Stop voice memo playback if the current item is deleted.
Fixes #13502.
2024-04-18 16:24:13 -04:00
Nicholas Tinsley
5e4dfcc65f Add translator notes for some strings. 2024-04-18 16:24:13 -04:00
Clark
7811e51b41 Add CDN number as parameter for read credential call. 2024-04-18 16:24:13 -04:00
Alex Konradi
9703a868e5 Request new ZKC-based auth credential. 2024-04-18 16:24:13 -04:00
Alex Hart
1b7784b01f Update call strings to align with new designs. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
a83abaca1d Order story viewer names alphabetically. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
29b3f09d8a Catch possible ISE at end of re-registration. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
d36b2a23f5 Hide irrelevant rows in self about sheet. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
8f1722c718 Update placeholder label for view once media. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
5416c3b8aa Improve play button display logic on video editor fragment. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
89eeae36c4 Fix signed int overflow in disappearing timer UI message. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
eec2685e67 Registration refactor initial scaffolding. 2024-04-18 16:24:13 -04:00
Clark
318b59a6b2 Do not fallback to REST for resumable upload spec on ratelimit. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
a2e0468cd9 Remove "lower hand" confirmation dialog. 2024-04-18 16:24:13 -04:00
Clark
689eacd618 Add initial support for backup and restore of message and media to staging.
Co-authored-by: Cody Henthorne <cody@signal.org>
2024-04-18 16:24:13 -04:00
tedgravlin
8617a074ad Update CLA link in PR template. 2024-04-18 16:24:12 -04:00
Greyson Parrelli
046b8da880 Add missing static IPs.
Fixes #13513
2024-04-18 16:24:12 -04:00
Clark Chen
34a36ddfea Bump version to 7.4.2 2024-04-15 16:32:09 -04:00
Clark Chen
9330448198 Update translations and other static files. 2024-04-15 16:24:09 -04:00
Clark Chen
b3336b4d84 Revert "Use existing libsignal proguard rules."
This reverts commit 2ce6ea9a2a.
2024-04-15 10:17:36 -04:00
Alex Hart
9553c94097 Bump version to 7.4.1 2024-04-12 16:38:43 -03:00
Alex Hart
c1845ae1c4 Update baseline profile. 2024-04-12 16:33:11 -03:00
Alex Hart
b6cc3852b0 Update translations and other static files. 2024-04-12 16:28:07 -03:00
Cody Henthorne
eefc86f27e Fix dangling call notification and remove active call manager flag. 2024-04-12 09:38:06 -04:00
Nicholas Tinsley
09404157aa Add processor information to debug log. 2024-04-11 16:09:33 -04:00
Alex Hart
abfd9f8f41 Add proper capitalization settings in nickname activity. 2024-04-11 10:35:47 -03:00
Bishal
e04381fd75 Add fix for missing play button when the audio is not sent in offline mode. 2024-04-11 10:31:12 -03:00
Alex Hart
30cc3ff9fc Bump version to 7.4.0 2024-04-10 16:31:53 -03:00
Alex Hart
6f5f299035 Update baseline profile. 2024-04-10 16:31:47 -03:00
Alex Hart
02eed02cb8 Update translations and other static files. 2024-04-10 16:29:24 -03:00
Greyson Parrelli
c1d29b5c39 Set internalUser=true for nightly builds. 2024-04-10 14:54:35 -04:00
Greyson Parrelli
db4442939d Remove Environment.IS_PNP 2024-04-10 14:52:59 -04:00
tedgravlin
6ece776382 Fix navbar color in multiple instances. 2024-04-10 14:29:58 -03:00
Alex Hart
0eda714755 Send recipients when sending group story sync. 2024-04-10 14:21:34 -03:00
Greyson Parrelli
831d099503 Inline the nicknames feature flag. 2024-04-10 13:18:01 -04:00
Alex Hart
fa23e4ca70 Convert members collection to set to avoid duplicate entries. 2024-04-10 13:45:46 -03:00
Greyson Parrelli
982f602178 Regularly analyze database tables to improve index usage. 2024-04-09 16:55:25 -04:00
Greyson Parrelli
713298109a Specify indexes for mention table queries. 2024-04-09 16:18:21 -04:00
Greyson Parrelli
8793981804 Add a log section for the database schema. 2024-04-09 16:18:21 -04:00
Greyson Parrelli
9bd4e9524c Convert MentionTable to kotlin. 2024-04-09 16:18:21 -04:00
Cody Henthorne
791dc2724f Attempt to fix bad notification for call service shutdown. 2024-04-09 16:18:21 -04:00
Cody Henthorne
ba3473c61a Fix scroll to message when bubble is under toolbar. 2024-04-09 16:18:21 -04:00
moiseev-signal
3ea194255d Add getUsername default method to CredentialsProvider 2024-04-09 16:18:21 -04:00
Cody Henthorne
ea081e981f Treat unregistered user during send as general failure. 2024-04-09 16:18:21 -04:00
Alex Konradi
2ce6ea9a2a Use existing libsignal proguard rules. 2024-04-09 16:18:20 -04:00
Alex Konradi
295c9310e9 Map libsignal CDSI errors to existing exceptions. 2024-04-09 16:18:20 -04:00
Greyson Parrelli
7447ed2eac Add the ability to jump to a specific date in search. 2024-04-09 16:18:20 -04:00
Cody Henthorne
d5bf16b91a Fix incorrect thread body adjustments containing media, mentions, and styling. 2024-04-09 16:18:06 -04:00
Cody Henthorne
76665c1f0d Prevent excessive video toggling in group calls due to server instability. 2024-04-09 16:18:06 -04:00
Cody Henthorne
dd28523b05 Transition full screen call UX to terminal state when call handled by linked device. 2024-04-09 16:18:06 -04:00
Cody Henthorne
16588c401e Reduce verbosity of WebRtcViewModel event logging during calls. 2024-04-09 16:18:06 -04:00
Greyson Parrelli
dbf8a7ca87 Rotate libsignal-net flag. 2024-04-09 16:18:06 -04:00
moiseev-signal
e92c76434e Upgrade to libsignal-client 0.44.0 2024-04-09 16:18:06 -04:00
Greyson Parrelli
7adb581271 Bump version to 7.3.1 2024-04-09 16:17:21 -04:00
Greyson Parrelli
869476a41b Update translations and other static files. 2024-04-09 16:16:47 -04:00
Greyson Parrelli
8daf1bca20 Improve handling of unknown groups. 2024-04-09 15:56:15 -04:00
Greyson Parrelli
d044b3c931 Remove most lazy properties from Recipient. 2024-04-09 15:02:36 -04:00
Cody Henthorne
0fcb19e1cc Fix group recipient resolve race that can cause unknown group recipients in live cache. 2024-04-09 14:59:47 -04:00
Nicholas Tinsley
2a6977da75 Nickname screen copy update. 2024-04-04 09:45:26 -04:00
Nicholas Tinsley
26bd435bf6 Update nickname delete dialog copy. 2024-04-03 16:48:26 -04:00
Greyson Parrelli
91f8d6075c Bump version to 7.3.0 2024-04-03 15:55:34 -04:00
Greyson Parrelli
9ed9a330f4 Update baseline profile. 2024-04-03 15:54:13 -04:00
Greyson Parrelli
8bbf6b790f Update translations and other static files. 2024-04-03 15:47:24 -04:00
Greyson Parrelli
a277e9b307 Fix compilation of benchmark build. 2024-04-03 15:47:24 -04:00
Cody Henthorne
f8e6bcf290 Add username_edit release note cta action. 2024-04-03 14:07:56 -04:00
Greyson Parrelli
3ba2b46bb0 Convert Recipient to kotlin. 2024-04-03 14:02:55 -04:00
Greyson Parrelli
b50eab230d Update strings for 'system contact' -> 'phone contact'. 2024-04-03 14:02:13 -04:00
Alex Hart
3f91824325 Fix bug preventing the review sheet from opening. 2024-04-03 14:02:13 -04:00
Alex Hart
879e05148b Fix database revocation for call links. 2024-04-03 14:02:13 -04:00
moiseev-signal
78e36b85d4 Make sure not more than one libsignal Network instance is ever created
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2024-04-03 14:02:13 -04:00
Alex Hart
544cc06f13 Add chevron to conversation heading. 2024-04-03 14:02:13 -04:00
Cody Henthorne
133b7ef3f1 Fix multiple exception crash in rx message send flow. 2024-04-03 14:02:13 -04:00
Cody Henthorne
08a407dc23 Prevent thread starvation during message sending. 2024-04-03 14:02:13 -04:00
Greyson Parrelli
6c697fad8b Stop reading the PNP capability. 2024-04-03 14:02:13 -04:00
Greyson Parrelli
c904a7aa97 Delete LegacyAttachmentUploadJob.
It's been over 4 months since it was replaced. That's beyond the 90 day
build expiration + 1 day job lifespan. Should be safe to remove.
2024-04-03 14:02:13 -04:00
Greyson Parrelli
ad131d7c65 Enqueue AccountConsistency check when prekey syncs fail. 2024-04-03 14:02:13 -04:00
Alex Hart
e12d2d1e98 Fix local pip movement when in RTL language. 2024-04-03 14:02:12 -04:00
adel-signal
f01e044662 Update to new calling turn info endpoint, add support for turn server ips.
Co-authored-by: Adel Lahlou <adel@signal.com>
2024-04-03 14:02:12 -04:00
Jim Gustafson
03d3ae7043 Update to RingRTC v2.39.3 2024-04-03 14:02:12 -04:00
Greyson Parrelli
6b60a22879 Bump version to 7.2.4 2024-04-03 13:32:38 -04:00
Greyson Parrelli
bbded8caa8 Update translations and other static files. 2024-04-03 13:32:08 -04:00
Greyson Parrelli
3a6352d2a3 Don't show profile name in parens if it's the same as display name. 2024-04-03 13:19:37 -04:00
Greyson Parrelli
8293d6bc4c Allow last-name-only nicknames to be saved. 2024-04-03 11:54:07 -04:00
Greyson Parrelli
56bdb28c2f Fix bug around entering text in the middle of a full note.
There's likely other weirdness, but this at least addresses the most
commond variation, where entering text in the middle of a full note
would start chopping stuff off the end.
2024-04-03 11:20:35 -04:00
Greyson Parrelli
b081fb1e13 Improve recipient shortname selection. 2024-04-03 10:45:47 -04:00
Greyson Parrelli
58c1f64dfe Allow familyName-only nicknames in storage service. 2024-04-03 10:44:04 -04:00
Greyson Parrelli
92b7147dcd Always take the remote nickname. 2024-04-03 10:39:43 -04:00
Greyson Parrelli
fa3a85c948 Bump version to 7.2.3 2024-04-02 15:30:07 -04:00
Greyson Parrelli
9da4513694 Update translations and other static files. 2024-04-02 15:29:14 -04:00
Greyson Parrelli
de520036a9 Allow last-name-only nicknames. 2024-04-02 15:19:44 -04:00
Greyson Parrelli
97ca15a1c0 Allow multi-line entry in note field. 2024-04-02 14:50:13 -04:00
Greyson Parrelli
713a34a5e7 Ensure that conversation count check is on background thread. 2024-04-02 14:36:07 -04:00
Alex Hart
d688280a30 Fix search for users without thread. 2024-04-02 15:27:57 -03:00
2159 changed files with 168465 additions and 58971 deletions

View File

@@ -6,4 +6,15 @@ ij_kotlin_allow_trailing_comma_on_call_site = false
ij_kotlin_allow_trailing_comma = false
ktlint_code_style = intellij_idea
twitter_compose_allowed_composition_locals=LocalExtendedColors
ktlint_standard_class-naming = disabled
ktlint_standard_class-naming = disabled
# below rules disabled during ktlint version migration because they were preexisting but should be corrected and re-enabled ASAP
ktlint_function_naming_ignore_when_annotated_with = Composable
ktlint_standard_property-naming = disabled
ktlint_standard_enum-wrapping = disabled
ktlint_standard_multiline-if-else = disabled
ktlint_standard_backing-property-naming = disabled
ktlint_standard_statement-wrapping = disabled
internal:ktlint-suppression = disabled
ktlint_standard_unnecessary-parentheses-before-trailing-lambda = disabled
ktlint_standard_value-parameter-comment = disabled

View File

@@ -2,7 +2,7 @@
### First time contributor checklist
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->
- [ ] I have read [how to contribute](https://github.com/signalapp/Signal-Android/blob/master/CONTRIBUTING.md) to this project
- [ ] I have signed the [Contributor License Agreement](https://whispersystems.org/cla/)
- [ ] I have signed the [Contributor License Agreement](https://signal.org/cla/)
### Contributor checklist
<!-- replace the empty checkboxes [ ] below with checked ones [x] accordingly -->

2
.gitignore vendored
View File

@@ -29,4 +29,4 @@ jni/libspeex/.deps/
pkcs11.password
dev.keystore
maps.key
local/
/local/

3
.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "libwebp"]
path = libwebp
url = https://github.com/webmproject/libwebp.git

View File

@@ -21,17 +21,10 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1403
val canonicalVersionName = "7.2.2"
val postFixSize = 100
val abiPostFix: Map<String, Int> = mapOf(
"universal" to 0,
"armeabi-v7a" to 1,
"arm64-v8a" to 2,
"x86" to 3,
"x86_64" to 4
)
val canonicalVersionCode = 1465
val canonicalVersionName = "7.18.2"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
val keystores: Map<String, Properties?> = mapOf("debug" to loadKeystoreProperties("keystore.debug.properties"))
@@ -61,6 +54,7 @@ val signalBuildToolsVersion: String by rootProject.extra
val signalCompileSdkVersion: String by rootProject.extra
val signalTargetSdkVersion: Int by rootProject.extra
val signalMinSdkVersion: Int by rootProject.extra
val signalNdkVersion: String by rootProject.extra
val signalJavaVersion: JavaVersion by rootProject.extra
val signalKotlinJvmTarget: String by rootProject.extra
@@ -79,7 +73,7 @@ wire {
}
ktlint {
version.set("0.49.1")
version.set("1.2.1")
}
android {
@@ -87,13 +81,17 @@ android {
buildToolsVersion = signalBuildToolsVersion
compileSdkVersion = signalCompileSdkVersion
ndkVersion = signalNdkVersion
flavorDimensions += listOf("distribution", "environment")
useLibrary("org.apache.http.legacy")
testBuildType = "instrumentation"
android.bundle.language.enableSplit = false
kotlinOptions {
jvmTarget = signalKotlinJvmTarget
freeCompilerArgs = listOf("-Xjvm-default=all")
}
keystores["debug"]?.let { properties ->
@@ -140,9 +138,27 @@ android {
targetCompatibility = signalJavaVersion
}
packagingOptions {
packaging {
jniLibs {
excludes += setOf(
"**/*.dylib",
"**/*.dll"
)
}
resources {
excludes += setOf("LICENSE.txt", "LICENSE", "NOTICE", "asm-license.txt", "META-INF/LICENSE", "META-INF/LICENSE.md", "META-INF/NOTICE", "META-INF/LICENSE-notice.md", "META-INF/proguard/androidx-annotations.pro", "libsignal_jni.dylib", "signal_jni.dll")
excludes += setOf(
"LICENSE.txt",
"LICENSE",
"NOTICE",
"asm-license.txt",
"META-INF/LICENSE",
"META-INF/LICENSE.md",
"META-INF/NOTICE",
"META-INF/LICENSE-notice.md",
"META-INF/proguard/androidx-annotations.pro",
"**/*.dylib",
"**/*.dll"
)
}
}
@@ -152,18 +168,16 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = "1.4.4"
kotlinCompilerExtensionVersion = "1.5.4"
}
defaultConfig {
versionCode = canonicalVersionCode * postFixSize
versionCode = (canonicalVersionCode * maxHotfixVersions) + currentHotfixVersion
versionName = canonicalVersionName
minSdk = signalMinSdkVersion
targetSdk = signalTargetSdkVersion
multiDexEnabled = true
vectorDrawables.useSupportLibrary = true
project.ext.set("archivesBaseName", "Signal")
@@ -178,7 +192,6 @@ android {
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3.signal.org\"")
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\"")
buildConfigField("String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"")
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\"")
buildConfigField("String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"")
buildConfigField("String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\"")
@@ -209,6 +222,7 @@ android {
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\"")
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"")
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.PRODUCTION")
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.INFO")
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"")
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"")
@@ -377,7 +391,6 @@ android {
buildConfigField("String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\"")
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3-staging.signal.org\"")
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
@@ -388,6 +401,7 @@ android {
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"")
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"")
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.STAGING")
buildConfigField("int", "LIBSIGNAL_LOG_LEVEL", "org.signal.libsignal.protocol.logging.SignalProtocolLogger.DEBUG")
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
@@ -407,7 +421,6 @@ android {
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
if (output.baseName.contains("nightly")) {
output.versionCodeOverride = canonicalVersionCode * postFixSize + 5
var tag = getCurrentGitTag()
if (!tag.isNullOrEmpty()) {
if (tag.startsWith("v")) {
@@ -421,14 +434,9 @@ android {
} else {
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
val abiName: String = output.getFilter("ABI") ?: "universal"
val postFix: Int = abiPostFix[abiName]!!
if (postFix >= postFixSize) {
throw AssertionError("postFix is too large")
if (currentHotfixVersion >= maxHotfixVersions) {
throw AssertionError("Hotfix version is too large!")
}
output.versionCodeOverride = canonicalVersionCode * postFixSize + postFix
}
}
}
@@ -437,6 +445,12 @@ android {
beforeVariants { variant ->
variant.enable = variant.name in selectableVariants
}
onVariants { variant ->
// Include the test-only library on debug builds.
if (variant.buildType != "instrumentation") {
variant.packaging.jniLibs.excludes.add("**/libsignal_jni_testing.so")
}
}
}
val releaseDir = "$projectDir/src/release/java"
@@ -488,7 +502,6 @@ dependencies {
implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.multidex)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.navigation.compose)
@@ -498,6 +511,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.reactivestreams.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
@@ -511,6 +525,7 @@ dependencies {
implementation(libs.androidx.profileinstaller)
implementation(libs.androidx.asynclayoutinflater)
implementation(libs.androidx.asynclayoutinflater.appcompat)
implementation(libs.androidx.emoji2)
implementation(libs.firebase.messaging) {
exclude(group = "com.google.firebase", module = "firebase-core")
exclude(group = "com.google.firebase", module = "firebase-analytics")
@@ -542,6 +557,7 @@ dependencies {
}
implementation(libs.stream)
implementation(libs.lottie)
implementation(libs.lottie.compose)
implementation(libs.signal.android.database.sqlcipher)
implementation(libs.androidx.sqlite)
implementation(libs.google.ez.vcard) {
@@ -553,11 +569,16 @@ dependencies {
implementation(libs.accompanist.permissions)
implementation(libs.kotlin.stdlib.jdk8)
implementation(libs.kotlin.reflect)
implementation(libs.kotlinx.coroutines.play.services)
implementation(libs.kotlinx.coroutines.rx3)
implementation(libs.jackson.module.kotlin)
implementation(libs.rxjava3.rxandroid)
implementation(libs.rxjava3.rxkotlin)
implementation(libs.rxdogtag)
"playImplementation"(project(":billing"))
"nightlyImplementation"(project(":billing"))
"spinnerImplementation"(project(":spinner"))
"canaryImplementation"(libs.square.leakcanary)
@@ -574,7 +595,6 @@ dependencies {
testImplementation(testLibs.robolectric.robolectric) {
exclude(group = "com.google.protobuf", module = "protobuf-java")
}
testImplementation(testLibs.robolectric.shadows.multidex)
testImplementation(testLibs.bouncycastle.bcprov.jdk15on) {
version {
strictly("1.70")
@@ -600,6 +620,7 @@ dependencies {
androidTestImplementation(testLibs.mockito.kotlin)
androidTestImplementation(testLibs.mockk.android)
androidTestImplementation(testLibs.square.okhttp.mockserver)
androidTestImplementation(testLibs.diff.utils)
androidTestUtil(testLibs.androidx.test.orchestrator)
}
@@ -714,7 +735,8 @@ fun Project.languageList(): List<String> {
.map { valuesFolderName -> valuesFolderName.replace("values-", "") }
.filter { valuesFolderName -> valuesFolderName != "values" }
.map { languageCode -> languageCode.replace("-r", "_") }
.distinct() + "en"
.distinct()
.sorted() + "en"
}
fun String.capitalize(): String {

View File

@@ -16,6 +16,10 @@
-keep class androidx.window.** { *; }
-keepclassmembers class * extends androidx.constraintlayout.motion.widget.Key {
public <init>();
}
# AGP generated dont warns
-dontwarn com.android.org.conscrypt.SSLParametersImpl
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@ import org.signal.core.util.logging.AndroidLogger
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger
@@ -21,8 +21,8 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
override fun initializeAppDependencies() {
val default = ApplicationDependencyProvider(this)
ApplicationDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
ApplicationDependencies.getDeadlockDetector().start()
AppDependencies.init(this, InstrumentationApplicationDependencyProvider(this, default))
AppDependencies.deadlockDetector.start()
}
override fun initializeLogging() {

View File

@@ -0,0 +1,377 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.github.difflib.DiffUtils
import com.github.difflib.UnifiedDiffUtils
import junit.framework.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.readFully
import org.signal.libsignal.messagebackup.ComparableBackup
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.kbs.MasterKey
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.ByteArrayInputStream
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class ArchiveImportExportTests {
companion object {
const val TAG = "ImportExport"
const val TESTS_FOLDER = "backupTests"
val SELF_ACI = ServiceId.ACI.from(UUID.fromString("00000000-0000-4000-8000-000000000001"))
val SELF_PNI = ServiceId.PNI.from(UUID.fromString("00000000-0000-4000-8000-000000000002"))
val SELF_E164 = "+10000000000"
val SELF_PROFILE_KEY: ByteArray = Base64.decode("YQKRq+3DQklInaOaMcmlzZnN0m/1hzLiaONX7gB12dg=")
val MASTER_KEY = Base64.decode("sHuBMP4ToZk4tcNU+S8eBUeCt8Am5EZnvuqTBJIR4Do")
}
// @Test
fun all() {
runTests()
}
@Test
fun temp() {
runTests { it == "chat_item_standard_message_formatted_text_03.binproto" }
}
// Passing
// @Test
fun accountData() {
runTests { it.startsWith("account_data_") }
}
@Test
fun adHocCall() {
runTests { it.startsWith("ad_hoc_call") }
}
// Passing
// @Test
fun chat() {
runTests { it.startsWith("chat_") && !it.contains("_item") }
}
// Passing
// @Test
fun chatItemContactMessage() {
runTests { it.startsWith("chat_item_contact_message_") }
}
// Passing
// @Test
fun chatItemExpirationTimerUpdate() {
runTests { it.startsWith("chat_item_expiration_timer_") }
}
// Passing
// @Test
fun chatItemGiftBadge() {
runTests { it.startsWith("chat_item_gift_badge_") }
}
@Test
fun chatItemGroupCallUpdate() {
runTests { it.startsWith("chat_item_group_call_update_") }
}
@Test
fun chatItemIndividualCallUpdate() {
runTests { it.startsWith("chat_item_individual_call_update_") }
}
// Passing
// @Test
fun chatItemLearnedProfileUpdate() {
runTests { it.startsWith("chat_item_learned_profile_update_") }
}
@Test
fun chatItemPaymentNotification() {
runTests { it.startsWith("chat_item_payment_notification_") }
}
// Passing
// @Test
fun chatItemProfileChangeUpdate() {
runTests { it.startsWith("chat_item_profile_change_update_") }
}
// Passing
// @Test
fun chatItemRemoteDelete() {
runTests { it.startsWith("chat_item_remote_delete_") }
}
// Passing
// @Test
fun chatItemSessionSwitchoverUpdate() {
runTests { it.startsWith("chat_item_session_switchover_update_") }
}
@Test
fun chatItemSimpleUpdates() {
runTests { it.startsWith("chat_item_simple_updates_") }
}
// Passing
// @Test
fun chatItemStandardMessageFormattedText() {
runTests { it.startsWith("chat_item_standard_message_formatted_text_") }
}
@Test
fun chatItemStandardMessageLongText() {
runTests { it.startsWith("chat_item_standard_message_long_text_") }
}
// Passing
// @Test
fun chatItemStandardMessageSpecialAttachments() {
runTests { it.startsWith("chat_item_standard_message_special_attachments_") }
}
// Passing
// @Test
fun chatItemStandardMessageStandardAttachments() {
runTests { it.startsWith("chat_item_standard_message_standard_attachments_") }
}
// Passing
// @Test
fun chatItemStandardMessageTextOnly() {
runTests { it.startsWith("chat_item_standard_message_text_only_") }
}
@Test
fun chatItemStandardMessageWithEdits() {
runTests { it.startsWith("chat_item_standard_message_with_edits_") }
}
@Test
fun chatItemStandardMessageWithQuote() {
runTests { it.startsWith("chat_item_standard_message_with_quote_") }
}
@Test
fun chatItemStickerMessage() {
runTests { it.startsWith("chat_item_sticker_message_") }
}
// Passing
// @Test
fun chatItemThreadMergeUpdate() {
runTests { it.startsWith("chat_item_thread_merge_update_") }
}
@Test
fun recipientCallLink() {
runTests { it.startsWith("recipient_call_link_") }
}
// Passing
// @Test
fun recipientContacts() {
runTests { it.startsWith("recipient_contacts_") }
}
// Passing
// @Test
fun recipientDistributionLists() {
runTests { it.startsWith("recipient_distribution_list_") }
}
// Passing
// @Test
fun recipientGroups() {
runTests { it.startsWith("recipient_groups_") }
}
private fun runTests(predicate: (String) -> Boolean = { true }) {
val testFiles = InstrumentationRegistry.getInstrumentation().context.resources.assets.list(TESTS_FOLDER)!!.filter(predicate)
val results: MutableList<TestResult> = mutableListOf()
Log.d(TAG, "About to run ${testFiles.size} tests.")
for (filename in testFiles) {
Log.d(TAG, "> $filename")
val startTime = System.currentTimeMillis()
val result = test(filename)
results += result
if (result is TestResult.Success) {
Log.d(TAG, " \uD83D\uDFE2 Passed in ${System.currentTimeMillis() - startTime} ms")
} else {
Log.d(TAG, " \uD83D\uDD34 Failed in ${System.currentTimeMillis() - startTime} ms")
}
}
results
.filterIsInstance<TestResult.Failure>()
.forEach {
Log.e(TAG, "Failure: ${it.name}\n${it.message}")
Log.e(TAG, "----------------------------------")
Log.e(TAG, "----------------------------------")
Log.e(TAG, "----------------------------------")
}
if (results.any { it is TestResult.Failure }) {
val successCount = results.count { it is TestResult.Success }
val failingTestNames = results.filterIsInstance<TestResult.Failure>().joinToString(separator = "\n") { " \uD83D\uDD34 ${it.name}" }
val message = "Some tests failed! Only $successCount/${results.size} passed. Failure details are above. Failing tests:\n$failingTestNames"
Log.d(TAG, message)
throw AssertionError("Some tests failed!")
} else {
Log.d(TAG, "All ${results.size} tests passed!")
}
}
private fun test(filename: String): TestResult {
resetAllData()
val inputFileBytes: ByteArray = InstrumentationRegistry.getInstrumentation().context.resources.assets.open("$TESTS_FOLDER/$filename").readFully(true)
val importResult = import(inputFileBytes)
assertTrue(importResult is ImportResult.Success)
val success = importResult as ImportResult.Success
val generatedBackupData = BackupRepository.debugExport(plaintext = true, currentTime = success.backupTime)
checkEquivalent(filename, inputFileBytes, generatedBackupData)?.let { return it }
return TestResult.Success(filename)
}
private fun resetAllData() {
// Need to delete these first to prevent foreign key crash
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.ListTable.TABLE_NAME}")
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.MembershipTable.TABLE_NAME}")
SqlUtil.getAllTables(SignalDatabase.rawDatabase)
.filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB
.sorted()
.forEach { table ->
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
SqlUtil.resetAutoIncrementValue(SignalDatabase.rawDatabase, table)
}
AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.clearSelf()
RecipientId.clearCache()
KeyValueDatabase.getInstance(AppDependencies.application).clear()
SignalStore.resetCache()
SignalStore.svr.setMasterKey(MasterKey(MASTER_KEY), "1234")
SignalStore.account.setE164(SELF_E164)
SignalStore.account.setAci(SELF_ACI)
SignalStore.account.setPni(SELF_PNI)
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
SignalStore.backup.backupTier = MessageBackupTier.PAID
}
private fun import(importData: ByteArray): ImportResult {
return BackupRepository.import(
length = importData.size.toLong(),
inputStreamFactory = { ByteArrayInputStream(importData) },
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY)),
plaintext = true
)
}
private fun assertPassesValidator(testName: String, generatedBackupData: ByteArray): TestResult.Failure? {
try {
BackupRepository.validate(
length = generatedBackupData.size.toLong(),
inputStreamFactory = { ByteArrayInputStream(generatedBackupData) },
selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, ProfileKey(SELF_PROFILE_KEY))
)
} catch (e: Exception) {
return TestResult.Failure(testName, "Generated backup failed validation: ${e.message}")
}
return null
}
private fun checkEquivalent(testName: String, import: ByteArray, export: ByteArray): TestResult.Failure? {
val importComparable = try {
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, import.inputStream(), import.size.toLong())
} catch (e: Exception) {
return TestResult.Failure(testName, "Imported backup hit a validation error: ${e.message}")
}
val exportComparable = try {
ComparableBackup.readUnencrypted(MessageBackup.Purpose.REMOTE_BACKUP, export.inputStream(), import.size.toLong())
} catch (e: Exception) {
return TestResult.Failure(testName, "Exported backup hit a validation error: ${e.message}")
}
if (importComparable.unknownFieldMessages.isNotEmpty()) {
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}")
}
if (exportComparable.unknownFieldMessages.isNotEmpty()) {
return TestResult.Failure(testName, "Imported backup contains unknown fields: ${importComparable.unknownFieldMessages}")
}
val canonicalImport = importComparable.comparableString
val canonicalExport = exportComparable.comparableString
if (canonicalImport != canonicalExport) {
val importLines = canonicalImport.lines()
val exportLines = canonicalExport.lines()
val patch = DiffUtils.diff(importLines, exportLines)
val diff = UnifiedDiffUtils.generateUnifiedDiff("Import", "Export", importLines, patch, 3).joinToString(separator = "\n")
val importFrames = import.toFrames()
val exportFrames = export.toFrames()
val importGroupFramesByMasterKey = importFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey }
val exportGroupFramesByMasterKey = exportFrames.mapNotNull { it.recipient?.group }.associateBy { it.masterKey }
val groupErrorMessage = StringBuilder()
for ((importKey, importValue) in importGroupFramesByMasterKey) {
if (exportGroupFramesByMasterKey[importKey]?.let { it.snapshot != importValue.snapshot } == true) {
groupErrorMessage.append("[$importKey] Snapshot mismatch.\nImport:\n${importValue}\n\nExport:\n${exportGroupFramesByMasterKey[importKey]}\n\n")
}
}
return TestResult.Failure(testName, "Imported backup does not match exported backup. Diff:\n$diff\n$groupErrorMessage")
}
return null
}
fun ByteArray.toFrames(): List<Frame> {
return PlainTextBackupReader(this.inputStream(), this.size.toLong()).use { it.asSequence().toList() }
}
private sealed class TestResult(val name: String) {
class Success(name: String) : TestResult(name)
class Failure(name: String, val message: String) : TestResult(name)
}
}

View File

@@ -1,675 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import android.content.ContentValues
import android.database.Cursor
import androidx.core.content.contentValuesOf
import net.zetetic.database.sqlcipher.SQLiteDatabase
import org.junit.Before
import org.junit.Test
import org.signal.core.util.Hex
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
import org.signal.core.util.readToList
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireBlob
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.toInt
import org.signal.core.util.withinTransaction
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.EmojiSearchTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.subscription.Subscriber
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.io.ByteArrayInputStream
import java.util.UUID
import kotlin.random.Random
typealias DatabaseData = Map<String, List<Map<String, Any?>>>
class BackupTest {
companion object {
val SELF_ACI = ACI.from(UUID.fromString("77770000-b477-4f35-a824-d92987a63641"))
val SELF_PNI = PNI.from(UUID.fromString("77771111-b014-41fb-bf73-05cb2ec52910"))
const val SELF_E164 = "+10000000000"
val SELF_PROFILE_KEY = ProfileKey(Random.nextBytes(32))
val ALICE_ACI = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
val ALICE_PNI = PNI.from(UUID.fromString("aaaa1111-c960-4f6c-8385-671ad2ffb999"))
val ALICE_E164 = "+12222222222"
/** Columns that we don't need to check equality of */
private val IGNORED_COLUMNS: Map<String, Set<String>> = mapOf(
RecipientTable.TABLE_NAME to setOf(RecipientTable.STORAGE_SERVICE_ID),
MessageTable.TABLE_NAME to setOf(MessageTable.FROM_DEVICE_ID)
)
/** Tables we don't need to check equality of */
private val IGNORED_TABLES: Set<String> = setOf(
EmojiSearchTable.TABLE_NAME,
"sqlite_sequence",
"message_fts_data",
"message_fts_idx",
"message_fts_docsize"
)
}
@Before
fun setup() {
SignalStore.account().setE164(SELF_E164)
SignalStore.account().setAci(SELF_ACI)
SignalStore.account().setPni(SELF_PNI)
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
}
@Test
fun emptyDatabase() {
backupTest { }
}
@Test
fun noteToSelf() {
backupTest {
individualChat(aci = SELF_ACI, givenName = "Note to Self") {
standardMessage(outgoing = true, body = "A")
standardMessage(outgoing = true, body = "B")
standardMessage(outgoing = true, body = "C")
}
}
}
@Test
fun individualChat() {
backupTest {
individualChat(aci = ALICE_ACI, givenName = "Alice") {
val m1 = standardMessage(outgoing = true, body = "Outgoing 1")
val m2 = standardMessage(outgoing = false, body = "Incoming 1", read = true)
standardMessage(outgoing = true, body = "Outgoing 2", quotes = m2)
standardMessage(outgoing = false, body = "Incoming 2", quotes = m1, quoteTargetMissing = true, read = false)
standardMessage(outgoing = true, body = "Outgoing 3, with mention", randomMention = true)
standardMessage(outgoing = false, body = "Incoming 3, with style", read = false, randomStyling = true)
remoteDeletedMessage(outgoing = true)
remoteDeletedMessage(outgoing = false)
}
}
}
@Test
fun individualRecipients() {
backupTest {
// Comprehensive example
individualRecipient(
aci = ALICE_ACI,
pni = ALICE_PNI,
e164 = ALICE_E164,
givenName = "Alice",
familyName = "Smith",
username = "alice.99",
hidden = false,
registeredState = RecipientTable.RegisteredState.REGISTERED,
profileKey = ProfileKey(Random.nextBytes(32)),
profileSharing = true,
hideStory = false
)
// Trying to get coverage of all the various values
individualRecipient(aci = ACI.from(UUID.randomUUID()), registeredState = RecipientTable.RegisteredState.NOT_REGISTERED)
individualRecipient(aci = ACI.from(UUID.randomUUID()), registeredState = RecipientTable.RegisteredState.UNKNOWN)
individualRecipient(pni = PNI.from(UUID.randomUUID()))
individualRecipient(e164 = "+15551234567")
individualRecipient(aci = ACI.from(UUID.randomUUID()), givenName = "Bob")
individualRecipient(aci = ACI.from(UUID.randomUUID()), familyName = "Smith")
individualRecipient(aci = ACI.from(UUID.randomUUID()), profileSharing = false)
individualRecipient(aci = ACI.from(UUID.randomUUID()), hideStory = true)
individualRecipient(aci = ACI.from(UUID.randomUUID()), hidden = true)
}
}
@Test
fun individualCallLogs() {
backupTest {
val aliceId = individualRecipient(
aci = ALICE_ACI,
pni = ALICE_PNI,
e164 = ALICE_E164,
givenName = "Alice",
familyName = "Smith",
username = "alice.99",
hidden = false,
registeredState = RecipientTable.RegisteredState.REGISTERED,
profileKey = ProfileKey(Random.nextBytes(32)),
profileSharing = true,
hideStory = false
)
insertOneToOneCallVariations(1, 1, aliceId)
}
}
private fun insertOneToOneCallVariations(callId: Long, timestamp: Long, id: RecipientId): Long {
val directions = arrayOf(CallTable.Direction.INCOMING, CallTable.Direction.OUTGOING)
val callTypes = arrayOf(CallTable.Type.AUDIO_CALL, CallTable.Type.VIDEO_CALL)
val events = arrayOf(
CallTable.Event.MISSED,
CallTable.Event.OUTGOING_RING,
CallTable.Event.ONGOING,
CallTable.Event.ACCEPTED,
CallTable.Event.NOT_ACCEPTED
)
var callTimestamp: Long = timestamp
var currentCallId = callId
for (direction in directions) {
for (event in events) {
for (type in callTypes) {
insertOneToOneCall(callId = currentCallId, callTimestamp, id, type, direction, event)
callTimestamp++
currentCallId++
}
}
}
return currentCallId
}
private fun insertOneToOneCall(callId: Long, timestamp: Long, peer: RecipientId, type: CallTable.Type, direction: CallTable.Direction, event: CallTable.Event) {
val messageType: Long = CallTable.Call.getMessageType(type, direction, event)
SignalDatabase.rawDatabase.withinTransaction {
val recipient = Recipient.resolved(peer)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipient)
val outgoing = direction == CallTable.Direction.OUTGOING
val messageValues = contentValuesOf(
MessageTable.FROM_RECIPIENT_ID to if (outgoing) Recipient.self().id.serialize() else peer.serialize(),
MessageTable.FROM_DEVICE_ID to 1,
MessageTable.TO_RECIPIENT_ID to if (outgoing) peer.serialize() else Recipient.self().id.serialize(),
MessageTable.DATE_RECEIVED to timestamp,
MessageTable.DATE_SENT to timestamp,
MessageTable.READ to 1,
MessageTable.TYPE to messageType,
MessageTable.THREAD_ID to threadId
)
val messageId = SignalDatabase.rawDatabase.insert(MessageTable.TABLE_NAME, null, messageValues)
val values = contentValuesOf(
CallTable.CALL_ID to callId,
CallTable.MESSAGE_ID to messageId,
CallTable.PEER to peer.serialize(),
CallTable.TYPE to CallTable.Type.serialize(type),
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
CallTable.EVENT to CallTable.Event.serialize(event),
CallTable.TIMESTAMP to timestamp
)
SignalDatabase.rawDatabase.insert(CallTable.TABLE_NAME, null, values)
SignalDatabase.threads.update(threadId, true)
}
}
@Test
fun accountData() {
val context = ApplicationDependencies.getApplication()
backupTest(validateKeyValue = true) {
val self = Recipient.self()
// TODO note-to-self archived
// TODO note-to-self unread
SignalStore.account().setAci(SELF_ACI)
SignalStore.account().setPni(SELF_PNI)
SignalStore.account().setE164(SELF_E164)
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
SignalDatabase.recipients.setProfileKey(self.id, ProfileKey(Random.nextBytes(32)))
SignalDatabase.recipients.setProfileName(self.id, ProfileName.fromParts("Peter", "Parker"))
SignalDatabase.recipients.setProfileAvatar(self.id, "https://example.com/")
SignalStore.donationsValues().markUserManuallyCancelled()
SignalStore.donationsValues().setSubscriber(Subscriber(SubscriberId.generate(), "USD"))
SignalStore.donationsValues().setDisplayBadgesOnProfile(false)
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
SignalStore.settings().isLinkPreviewsEnabled = false
SignalStore.settings().isPreferSystemContactPhotos = true
SignalStore.settings().universalExpireTimer = 42
SignalStore.settings().setKeepMutedChatsArchived(true)
SignalStore.storyValues().viewedReceiptsEnabled = false
SignalStore.storyValues().userHasViewedOnboardingStory = true
SignalStore.storyValues().isFeatureDisabled = false
SignalStore.storyValues().userHasBeenNotifiedAboutStories = true
SignalStore.storyValues().userHasSeenGroupStoryEducationSheet = true
SignalStore.emojiValues().reactions = listOf("a", "b", "c")
TextSecurePreferences.setTypingIndicatorsEnabled(context, false)
TextSecurePreferences.setReadReceiptsEnabled(context, false)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, true)
}
// Have to check TextSecurePreferences ourselves, since they're not in a database
TextSecurePreferences.isTypingIndicatorsEnabled(context) assertIs false
TextSecurePreferences.isReadReceiptsEnabled(context) assertIs false
TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context) assertIs true
}
/**
* Sets up the database, then executes your setup code, then compares snapshots of the database
* before an after an import to ensure that no data was lost/changed.
*
* @param validateKeyValue If true, this will also validate the KeyValueDatabase. You only want to do this if you
* intend on setting most of the values. Otherwise stuff tends to not match since values are lazily written.
*/
private fun backupTest(validateKeyValue: Boolean = false, content: () -> Unit) {
// Under normal circumstances, My Story ends up being the first recipient in the table, and is added automatically.
// This screws with the tests by offsetting all the recipientIds in the initial state.
// Easiest way to get around this is to make the DB a true clean slate by clearing everything.
// (We only really need to clear Recipient/dlists, but doing everything to be consistent.)
SignalDatabase.distributionLists.clearAllDataForBackupRestore()
SignalDatabase.recipients.clearAllDataForBackupRestore()
SignalDatabase.messages.clearAllDataForBackupRestore()
SignalDatabase.threads.clearAllDataForBackupRestore()
// Again, for comparison purposes, because we always import self first, we want to ensure it's the first item
// in the table when we export.
individualRecipient(
aci = SELF_ACI,
pni = SELF_PNI,
e164 = SELF_E164,
profileKey = SELF_PROFILE_KEY,
profileSharing = true
)
content()
val startingMainData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
val startingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
val exported: ByteArray = BackupRepository.export()
BackupRepository.import(length = exported.size.toLong(), inputStreamFactory = { ByteArrayInputStream(exported) }, selfData = BackupRepository.SelfData(SELF_ACI, SELF_PNI, SELF_E164, SELF_PROFILE_KEY))
val endingData: DatabaseData = SignalDatabase.rawDatabase.readAllContents()
val endingKeyValueData: DatabaseData = if (validateKeyValue) SignalDatabase.rawDatabase.readAllContents() else emptyMap()
assertDatabaseMatches(startingMainData, endingData)
assertDatabaseMatches(startingKeyValueData, endingKeyValueData)
}
private fun individualChat(aci: ACI, givenName: String, familyName: String? = null, init: IndividualChatCreator.() -> Unit) {
val recipientId = individualRecipient(aci = aci, givenName = givenName, familyName = familyName, profileSharing = true)
val threadId: Long = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
IndividualChatCreator(SignalDatabase.rawDatabase, recipientId, threadId).init()
SignalDatabase.threads.update(threadId, false)
}
private fun individualRecipient(
aci: ACI? = null,
pni: PNI? = null,
e164: String? = null,
givenName: String? = null,
familyName: String? = null,
username: String? = null,
hidden: Boolean = false,
registeredState: RecipientTable.RegisteredState = RecipientTable.RegisteredState.UNKNOWN,
profileKey: ProfileKey? = null,
profileSharing: Boolean = false,
hideStory: Boolean = false
): RecipientId {
check(aci != null || pni != null || e164 != null)
val recipientId = SignalDatabase.recipients.getAndPossiblyMerge(aci, pni, e164, pniVerified = true, changeSelf = true)
if (givenName != null || familyName != null) {
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts(givenName, familyName))
}
if (username != null) {
SignalDatabase.recipients.setUsername(recipientId, username)
}
if (registeredState == RecipientTable.RegisteredState.REGISTERED) {
SignalDatabase.recipients.markRegistered(recipientId, aci ?: pni!!)
} else if (registeredState == RecipientTable.RegisteredState.NOT_REGISTERED) {
SignalDatabase.recipients.markUnregistered(recipientId)
}
if (profileKey != null) {
SignalDatabase.recipients.setProfileKey(recipientId, profileKey)
}
SignalDatabase.recipients.setProfileSharing(recipientId, profileSharing)
SignalDatabase.recipients.setHideStory(recipientId, hideStory)
if (hidden) {
SignalDatabase.recipients.markHidden(recipientId)
}
return recipientId
}
private inner class IndividualChatCreator(
private val db: SQLiteDatabase,
private val recipientId: RecipientId,
private val threadId: Long
) {
fun standardMessage(
outgoing: Boolean,
sentTimestamp: Long = System.currentTimeMillis(),
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
serverTimestamp: Long = sentTimestamp,
body: String? = null,
read: Boolean = true,
quotes: Long? = null,
quoteTargetMissing: Boolean = false,
randomMention: Boolean = false,
randomStyling: Boolean = false
): Long {
return db.insertMessage(
from = if (outgoing) Recipient.self().id else recipientId,
to = if (outgoing) recipientId else Recipient.self().id,
outgoing = outgoing,
threadId = threadId,
sentTimestamp = sentTimestamp,
receivedTimestamp = receivedTimestamp,
serverTimestamp = serverTimestamp,
body = body,
read = read,
quotes = quotes,
quoteTargetMissing = quoteTargetMissing,
randomMention = randomMention,
randomStyling = randomStyling
)
}
fun remoteDeletedMessage(
outgoing: Boolean,
sentTimestamp: Long = System.currentTimeMillis(),
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
serverTimestamp: Long = sentTimestamp
): Long {
return db.insertMessage(
from = if (outgoing) Recipient.self().id else recipientId,
to = if (outgoing) recipientId else Recipient.self().id,
outgoing = outgoing,
threadId = threadId,
sentTimestamp = sentTimestamp,
receivedTimestamp = receivedTimestamp,
serverTimestamp = serverTimestamp,
remoteDeleted = true
)
}
}
private fun SQLiteDatabase.insertMessage(
from: RecipientId,
to: RecipientId,
outgoing: Boolean,
threadId: Long,
sentTimestamp: Long = System.currentTimeMillis(),
receivedTimestamp: Long = if (outgoing) sentTimestamp else sentTimestamp + 1,
serverTimestamp: Long = sentTimestamp,
body: String? = null,
read: Boolean = true,
quotes: Long? = null,
quoteTargetMissing: Boolean = false,
randomMention: Boolean = false,
randomStyling: Boolean = false,
remoteDeleted: Boolean = false
): Long {
val type = if (outgoing) {
MessageTypes.BASE_SENT_TYPE
} else {
MessageTypes.BASE_INBOX_TYPE
} or MessageTypes.SECURE_MESSAGE_BIT or MessageTypes.PUSH_MESSAGE_BIT
val contentValues = ContentValues()
contentValues.put(MessageTable.DATE_SENT, sentTimestamp)
contentValues.put(MessageTable.DATE_RECEIVED, receivedTimestamp)
contentValues.put(MessageTable.FROM_RECIPIENT_ID, from.serialize())
contentValues.put(MessageTable.TO_RECIPIENT_ID, to.serialize())
contentValues.put(MessageTable.THREAD_ID, threadId)
contentValues.put(MessageTable.BODY, body)
contentValues.put(MessageTable.TYPE, type)
contentValues.put(MessageTable.READ, if (read) 1 else 0)
if (!outgoing) {
contentValues.put(MessageTable.DATE_SERVER, serverTimestamp)
}
if (remoteDeleted) {
contentValues.put(MessageTable.REMOTE_DELETED, 1)
return this
.insertInto(MessageTable.TABLE_NAME)
.values(contentValues)
.run()
}
if (quotes != null) {
val quoteDetails = this.getQuoteDetailsFor(quotes)
contentValues.put(MessageTable.QUOTE_ID, if (quoteTargetMissing) MessageTable.QUOTE_TARGET_MISSING_ID else quoteDetails.quotedSentTimestamp)
contentValues.put(MessageTable.QUOTE_AUTHOR, quoteDetails.authorId.serialize())
contentValues.put(MessageTable.QUOTE_BODY, quoteDetails.body)
contentValues.put(MessageTable.QUOTE_BODY_RANGES, quoteDetails.bodyRanges)
contentValues.put(MessageTable.QUOTE_TYPE, quoteDetails.type)
contentValues.put(MessageTable.QUOTE_MISSING, quoteTargetMissing.toInt())
}
if (body != null && (randomMention || randomStyling)) {
val ranges: MutableList<BodyRangeList.BodyRange> = mutableListOf()
if (randomMention) {
ranges += BodyRangeList.BodyRange(
start = 0,
length = Random.nextInt(body.length),
mentionUuid = if (outgoing) Recipient.resolved(to).requireAci().toString() else Recipient.resolved(from).requireAci().toString()
)
}
if (randomStyling) {
ranges += BodyRangeList.BodyRange(
start = 0,
length = Random.nextInt(body.length),
style = BodyRangeList.BodyRange.Style.fromValue(Random.nextInt(BodyRangeList.BodyRange.Style.values().size))
)
}
contentValues.put(MessageTable.MESSAGE_RANGES, BodyRangeList(ranges = ranges).encode())
}
return this
.insertInto(MessageTable.TABLE_NAME)
.values(contentValues)
.run()
}
private fun assertDatabaseMatches(expected: DatabaseData, actual: DatabaseData) {
assert(expected.keys.size == actual.keys.size) { "Mismatched table count! Expected: ${expected.keys} || Actual: ${actual.keys}" }
assert(expected.keys.containsAll(actual.keys)) { "Table names differ! Expected: ${expected.keys} || Actual: ${actual.keys}" }
val tablesToCheck = expected.keys.filter { !IGNORED_TABLES.contains(it) }
for (table in tablesToCheck) {
val expectedTable: List<Map<String, Any?>> = expected[table]!!
val actualTable: List<Map<String, Any?>> = actual[table]!!
assert(expectedTable.size == actualTable.size) { "Mismatched number of rows for table '$table'! Expected: ${expectedTable.size} || Actual: ${actualTable.size}\n $actualTable" }
val expectedFiltered: List<Map<String, Any?>> = expectedTable.withoutExcludedColumns(IGNORED_COLUMNS[table])
val actualFiltered: List<Map<String, Any?>> = actualTable.withoutExcludedColumns(IGNORED_COLUMNS[table])
assert(contentEquals(expectedFiltered, actualFiltered)) { "Data did not match for table '$table'!\n${prettyDiff(expectedFiltered, actualFiltered)}" }
}
}
private fun contentEquals(expectedRows: List<Map<String, Any?>>, actualRows: List<Map<String, Any?>>): Boolean {
if (expectedRows == actualRows) {
return true
}
assert(expectedRows.size == actualRows.size)
for (i in expectedRows.indices) {
val expectedRow = expectedRows[i]
val actualRow = actualRows[i]
for (key in expectedRow.keys) {
val expectedValue = expectedRow[key]
val actualValue = actualRow[key]
if (!contentEquals(expectedValue, actualValue)) {
return false
}
}
}
return true
}
private fun contentEquals(lhs: Any?, rhs: Any?): Boolean {
return if (lhs is ByteArray && rhs is ByteArray) {
lhs.contentEquals(rhs)
} else {
lhs == rhs
}
}
private fun prettyDiff(expectedRows: List<Map<String, Any?>>, actualRows: List<Map<String, Any?>>): String {
val builder = StringBuilder()
assert(expectedRows.size == actualRows.size)
for (i in expectedRows.indices) {
val expectedRow = expectedRows[i]
val actualRow = actualRows[i]
var describedRow = false
for (key in expectedRow.keys) {
val expectedValue = expectedRow[key]
val actualValue = actualRow[key]
if (!contentEquals(expectedValue, actualValue)) {
if (!describedRow) {
builder.append("-- ROW ${i + 1}\n")
describedRow = true
}
builder.append("  [$key] Expected: ${expectedValue.prettyPrint()} || Actual: ${actualValue.prettyPrint()} \n")
}
}
if (describedRow) {
builder.append("\n")
builder.append("Expected: $expectedRow\n")
builder.append("Actual: $actualRow\n")
}
}
return builder.toString()
}
private fun Any?.prettyPrint(): String {
return when (this) {
is ByteArray -> "Bytes(${Hex.toString(this)})"
else -> this.toString()
}
}
private fun List<Map<String, Any?>>.withoutExcludedColumns(ignored: Set<String>?): List<Map<String, Any?>> {
return if (ignored != null) {
this.map { row ->
row.filterKeys { !ignored.contains(it) }
}
} else {
this
}
}
private fun SQLiteDatabase.getQuoteDetailsFor(messageId: Long): QuoteDetails {
return this
.select(
MessageTable.DATE_SENT,
MessageTable.FROM_RECIPIENT_ID,
MessageTable.BODY,
MessageTable.MESSAGE_RANGES
)
.from(MessageTable.TABLE_NAME)
.where("${MessageTable.ID} = ?", messageId)
.run()
.readToSingleObject { cursor ->
QuoteDetails(
quotedSentTimestamp = cursor.requireLong(MessageTable.DATE_SENT),
authorId = RecipientId.from(cursor.requireLong(MessageTable.FROM_RECIPIENT_ID)),
body = cursor.requireString(MessageTable.BODY),
bodyRanges = cursor.requireBlob(MessageTable.MESSAGE_RANGES),
type = QuoteModel.Type.NORMAL.code
)
}!!
}
private fun SQLiteDatabase.readAllContents(): DatabaseData {
return SqlUtil.getAllTables(this).associateWith { table -> this.getAllTableData(table) }
}
private fun SQLiteDatabase.getAllTableData(table: String): List<Map<String, Any?>> {
return this
.select()
.from(table)
.run()
.readToList { cursor ->
val map: MutableMap<String, Any?> = mutableMapOf()
for (i in 0 until cursor.columnCount) {
val column = cursor.getColumnName(i)
when (cursor.getType(i)) {
Cursor.FIELD_TYPE_INTEGER -> map[column] = cursor.getInt(i)
Cursor.FIELD_TYPE_FLOAT -> map[column] = cursor.getFloat(i)
Cursor.FIELD_TYPE_STRING -> map[column] = cursor.getString(i)
Cursor.FIELD_TYPE_BLOB -> map[column] = cursor.getBlob(i)
Cursor.FIELD_TYPE_NULL -> map[column] = null
}
}
map
}
}
private data class QuoteDetails(
val quotedSentTimestamp: Long,
val authorId: RecipientId,
val body: String?,
val bodyRanges: ByteArray?,
val type: Int
)
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.testing.SignalFlakyTest
import org.thoughtcrime.securesms.testing.SignalFlakyTestRule
@RunWith(AndroidJUnit4::class)
class FlakyTestAnnotationTest {
@get:Rule
val flakyTestRule = SignalFlakyTestRule()
companion object {
private var count = 0
}
@SignalFlakyTest
@Test
fun purposelyFlaky() {
count++
assertEquals(3, count)
}
}

View File

@@ -1,393 +0,0 @@
package org.thoughtcrime.securesms.components.settings.app.changenumber
import androidx.lifecycle.SavedStateHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import okhttp3.mockwebserver.MockResponse
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.registration.VerifyAccountRepository
import org.thoughtcrime.securesms.registration.VerifyResponseProcessor
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.MockProvider
import org.thoughtcrime.securesms.testing.Post
import org.thoughtcrime.securesms.testing.Put
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsNull
import org.thoughtcrime.securesms.testing.assertIsSize
import org.thoughtcrime.securesms.testing.connectionFailure
import org.thoughtcrime.securesms.testing.failure
import org.thoughtcrime.securesms.testing.parsedRequestBody
import org.thoughtcrime.securesms.testing.success
import org.thoughtcrime.securesms.testing.timeout
import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.internal.push.MismatchedDevices
import org.whispersystems.signalservice.internal.push.PreKeyState
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class ChangeNumberViewModelTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var viewModel: ChangeNumberViewModel
@Before
fun setUp() {
ThreadUtil.runOnMainSync {
viewModel = ChangeNumberViewModel(
localNumber = harness.self.requireE164(),
changeNumberRepository = ChangeNumberRepository(),
savedState = SavedStateHandle(),
password = SignalStore.account().servicePassword!!,
verifyAccountRepository = VerifyAccountRepository(harness.application)
)
viewModel.setNewCountry(1)
viewModel.setNewNationalNumber("5555550102")
}
}
@After
fun tearDown() {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@Test
fun testChangeNumber_givenOnlyPrimaryAndNoRegLock() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
/**
* If we encounter a server error, this means the server ack our request and rejected it. In this
* case we know the change *did not* take on the server and can reset to a clean state.
*/
@Test
fun testChangeNumber_givenServerFailedApiCall() {
// GIVEN
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { MockResponse().failure(500) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs true
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
/**
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
* number on the server side. We have to do a whoami call to query the server for our details and then
* respond accordingly.
*
* In this case, the whoami is our old details, so we can know the change *did not* take on the server
* and can reset to a clean state.
*/
@Test
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToServer() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { MockResponse().connectionFailure() },
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, oldPni, oldE164)) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().isChangeNumberLocked assertIs false
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
/**
* If we encounter a non-server error like a timeout or bad SSL, we do not know the state of our change
* number on the server side. We have to do a whoami call to query the server for our details and then
* respond accordingly.
*
* In this case, the whoami is our new details, so we can know the change *did* take on the server
* and need to keep the app in a locked state. The test then uses the ChangeNumberLockActivity to unlock
* and apply the pending state after confirming the change on the server.
*/
@Test
@FlakyTest
@Ignore("Test sometimes requires manual intervention to continue.")
fun testChangeNumber_givenNetworkFailedApiCallEnRouteToClient() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val oldPni = Recipient.self().requirePni()
val oldE164 = Recipient.self().requireE164()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
MockResponse().timeout()
},
Get("/v1/accounts/whoami") { MockResponse().success(MockProvider.createWhoAmIResponse(aci, newPni, "+15555550102")) },
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
val processor: VerifyResponseProcessor = viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet()
// THEN
processor.isServerSentError() assertIs false
Recipient.self().requireE164() assertIs oldE164
Recipient.self().requirePni() assertIs oldPni
SignalStore.misc().isChangeNumberLocked assertIs true
SignalStore.misc().pendingChangeNumberMetadata.assertIsNotNull()
// WHEN AGAIN Processing lock
val scenario = harness.launchActivity<ChangeNumberLockActivity>()
scenario.onActivity {}
ThreadUtil.sleep(500)
// THEN AGAIN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenOnlyPrimaryAndRegistrationLock() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
processor.registrationLock() assertIs true
Recipient.self().requirePni() assertIsNot newPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenMismatchedDevicesOnFirstCall() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Get("/v1/devices") { MockResponse().success(MockProvider.primaryOnlyDeviceList) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.deviceMessages.isEmpty()) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2)
extraDevices = emptyList()
}
)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Get("/v2/keys/$aci/2") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
@Test
fun testChangeNumber_givenRegLockAndMismatchedDevicesOnFirstTwoCalls() {
// GIVEN
val aci = Recipient.self().requireServiceId()
val newPni = PNI.from(UUID.randomUUID())
lateinit var changeNumberRequest: ChangePhoneNumberRequest
lateinit var setPreKeysRequest: PreKeyState
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Post("/v1/verification/session") { MockResponse().success(MockProvider.sessionMetadataJson.copy(verified = false)) },
Put("/v1/verification/session/${MockProvider.sessionMetadataJson.id}/code") { MockResponse().success(MockProvider.sessionMetadataJson) },
Put("/v2/accounts/number") { r ->
changeNumberRequest = r.parsedRequestBody()
if (changeNumberRequest.registrationLock.isNullOrEmpty()) {
MockResponse().failure(423, MockProvider.lockedFailure)
} else if (changeNumberRequest.deviceMessages.isEmpty()) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2)
extraDevices = emptyList()
}
)
} else if (changeNumberRequest.deviceMessages.size == 1) {
MockResponse().failure(
409,
MismatchedDevices().apply {
missingDevices = listOf(2, 3)
extraDevices = emptyList()
}
)
} else {
MockResponse().success(MockProvider.createVerifyAccountResponse(aci, newPni))
}
},
Get("/v2/keys/$aci/2") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 2))
},
Get("/v2/keys/$aci/3") {
MockResponse().success(MockProvider.createPreKeyResponse(deviceId = 3))
},
Put("/v2/keys") { r ->
setPreKeysRequest = r.parsedRequestBody()
MockResponse().success()
},
Get("/v1/certificate/delivery") { MockResponse().success(MockProvider.senderCertificate) }
)
// WHEN
viewModel.requestVerificationCode(VerifyAccountRepository.Mode.SMS_WITHOUT_LISTENER, null, null).blockingGet().resultOrThrow
viewModel.verifyCodeWithoutRegistrationLock("123456").blockingGet().also { processor ->
processor.registrationLock() assertIs true
Recipient.self().requirePni() assertIsNot newPni
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
viewModel.verifyCodeAndRegisterAccountWithRegistrationLock("pin").blockingGet().resultOrThrow
// THEN
assertSuccess(newPni, changeNumberRequest, setPreKeysRequest)
}
private fun assertSuccess(newPni: ServiceId, changeNumberRequest: ChangePhoneNumberRequest, setPreKeysRequest: PreKeyState) {
val pniProtocolStore = ApplicationDependencies.getProtocolStore().pni()
val pniMetadataStore = SignalStore.account().pniPreKeys
Recipient.self().requireE164() assertIs "+15555550102"
Recipient.self().requirePni() assertIs newPni
SignalStore.account().pniRegistrationId assertIs changeNumberRequest.pniRegistrationIds["1"]!!
SignalStore.account().pniIdentityKey.publicKey assertIs changeNumberRequest.pniIdentityKey
pniMetadataStore.activeSignedPreKeyId assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.keyId
val activeSignedPreKey: SignedPreKeyRecord = pniProtocolStore.loadSignedPreKey(pniMetadataStore.activeSignedPreKeyId)
activeSignedPreKey.keyPair.publicKey assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.publicKey
activeSignedPreKey.signature assertIs changeNumberRequest.devicePniSignedPrekeys["1"]!!.signature
setPreKeysRequest.signedPreKey.publicKey assertIs activeSignedPreKey.keyPair.publicKey
setPreKeysRequest.preKeys assertIsSize 100
SignalStore.misc().pendingChangeNumberMetadata.assertIsNull()
}
}

View File

@@ -0,0 +1,133 @@
package org.thoughtcrime.securesms.components.settings.app.subscription.donate
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isSelected
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.Delete
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.success
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.math.BigDecimal
import java.util.Currency
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
@Ignore("Test fails on small screens, requires scrolling.")
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class CheckoutFlowActivityTest__RecurringDonations {
@get:Rule
val harness = SignalActivityRule(othersCount = 10)
private val intent = CheckoutFlowActivity.createIntent(InstrumentationRegistry.getInstrumentation().targetContext, InAppPaymentType.RECURRING_DONATION)
@Test
fun givenRecurringDonations_whenILoadScreen_thenIExpectMonthlySelected() {
ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withId(R.id.monthly)).check(matches(isSelected()))
}
@Test
fun givenNoCurrentDonation_whenILoadScreen_thenIExpectContinueButton() {
ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withText("Continue")).check(matches(isDisplayed()))
}
@Test
fun givenACurrentDonation_whenILoadScreen_thenIExpectUpgradeButton() {
initialiseConfigurationResponse()
initialiseActiveSubscription()
ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withText(R.string.SubscribeFragment__update_subscription)).check(matches(isDisplayed()))
onView(withText(R.string.SubscribeFragment__cancel_subscription)).check(matches(isDisplayed()))
}
@Test
fun givenACurrentDonation_whenIPressCancel_thenIExpectCancellationDialog() {
initialiseConfigurationResponse()
initialiseActiveSubscription()
ActivityScenario.launch<CheckoutFlowActivity>(intent)
onView(withText(R.string.SubscribeFragment__cancel_subscription)).check(matches(isDisplayed()))
onView(withText(R.string.SubscribeFragment__cancel_subscription)).perform(ViewActions.click())
onView(withText(R.string.SubscribeFragment__confirm_cancellation)).check(matches(isDisplayed()))
onView(withText(R.string.SubscribeFragment__confirm)).perform(ViewActions.click())
onView(withText(R.string.StripePaymentInProgressFragment__cancelling)).check(matches(isDisplayed()))
}
private fun initialiseConfigurationResponse() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/configuration") {
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
assets.open("inAppPaymentsTests/configuration.json").use { stream ->
MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
}
}
)
}
private fun initialiseActiveSubscription() {
val currency = Currency.getInstance("USD")
val subscriber = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = currency,
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.CARD
)
InAppPaymentsRepository.setSubscriber(subscriber)
SignalStore.inAppPayments.setSubscriberCurrency(currency, subscriber.type)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
MockResponse().success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
true,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"active",
"STRIPE",
"CARD",
false
),
null
)
)
},
Delete("/v1/subscription/${subscriber.subscriberId.serialize()}") {
Thread.sleep(10000)
MockResponse().success()
}
)
}
}

View File

@@ -7,6 +7,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ThreadUtil
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.conversation.v2.ConversationActivity
import org.thoughtcrime.securesms.database.MessageType
@@ -15,7 +16,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
@@ -137,7 +137,7 @@ class ConversationItemPreviewer {
private fun attachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
Cdn.CDN_3.cdnNumber,
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
@@ -154,7 +154,8 @@ class ConversationItemPreviewer {
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
System.currentTimeMillis(),
null
)
}
}

View File

@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet
@@ -64,7 +64,7 @@ class SafetyNumberChangeDialogPreviewer {
scenario.onActivity { conversationActivity ->
SafetyNumberBottomSheet
.forIdentityRecordsAndDestinations(
identityRecords = ApplicationDependencies.getProtocolStore().aci().identities().getIdentityRecords(othersRecipients).identityRecords,
identityRecords = AppDependencies.protocolStore.aci().identities().getIdentityRecords(othersRecipients).identityRecords,
destinations = listOf(ContactSearchKey.RecipientSearchKey(myStoryRecipientId, true))
)
.show(conversationActivity.supportFragmentManager)

View File

@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
import android.net.Uri
import android.view.View
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestManager
import io.mockk.mockk
@@ -203,8 +204,8 @@ class V2ConversationItemShapeTest {
private val colorizer = Colorizer()
override val lifecycleOwner: LifecycleOwner = mockk(relaxed = true)
override val displayMode: ConversationItemDisplayMode = ConversationItemDisplayMode.Standard
override val clickListener: ConversationAdapter.ItemClickListener = FakeConversationItemClickListener
override val selectedItems: Set<MultiselectPart> = emptySet()
override val isMessageRequestAccepted: Boolean = true
@@ -289,6 +290,8 @@ class V2ConversationItemShapeTest {
override fun onChangeNumberUpdateContact(recipient: Recipient) = Unit
override fun onChangeProfileNameUpdateContact(recipient: Recipient) = Unit
override fun onCallToAction(action: String) = Unit
override fun onDonateClicked() = Unit
@@ -313,7 +316,7 @@ class V2ConversationItemShapeTest {
override fun goToMediaPreview(parent: ConversationItem?, sharedElement: View?, args: MediaIntentFactory.MediaPreviewArgs?) = Unit
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) = Unit
override fun onShowGroupDescriptionClicked(groupName: String, description: String, shouldLinkifyWebLinks: Boolean) = Unit
@@ -328,5 +331,8 @@ class V2ConversationItemShapeTest {
override fun onReportSpamLearnMoreClicked() = Unit
override fun onMessageRequestAcceptOptionsClicked() = Unit
override fun onItemDoubleClick(item: MultiselectPart) = Unit
override fun onPaymentTombstoneClicked() = Unit
}
}

View File

@@ -1,15 +1,23 @@
package org.thoughtcrime.securesms.database
import android.content.Context
import android.net.Uri
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.copyTo
import org.signal.core.util.readFully
import org.signal.core.util.stream.NullOutputStream
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.mms.MediaStream
import org.thoughtcrime.securesms.mms.SentMediaQuality
@@ -17,6 +25,15 @@ import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream
import org.whispersystems.signalservice.api.crypto.AttachmentCipherOutputStream
import org.whispersystems.signalservice.api.crypto.NoCipherOutputStream
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.util.Optional
@RunWith(AndroidJUnit4::class)
@@ -163,6 +180,91 @@ class AttachmentTableTest {
highInfo.file.exists() assertIs true
}
@Test
fun finalizeAttachmentAfterDownload_fixDigestOnNonZeroPadding() {
// Insert attachment metadata for badly-padded attachment
val plaintext = byteArrayOf(1, 2, 3, 4)
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val badlyPaddedPlaintext = PaddingInputStream(plaintext.inputStream(), plaintext.size.toLong()).readFully().also { it[it.size - 1] = 0x42 }
val badlyPaddedCiphertext = encryptPrePaddedBytes(badlyPaddedPlaintext, key, iv)
val badlyPaddedDigest = getDigest(badlyPaddedCiphertext)
val cipherFile = getTempFile()
cipherFile.writeBytes(badlyPaddedCiphertext)
val mmsId = -1L
val attachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(mmsId, listOf(createAttachmentPointer(key, badlyPaddedDigest, plaintext.size)), emptyList()).values.first()
// Give data to attachment table
val cipherInputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintext.size.toLong(), key, badlyPaddedDigest, null, 4, false)
SignalDatabase.attachments.finalizeAttachmentAfterDownload(mmsId, attachmentId, cipherInputStream, iv)
// Verify the digest has been updated to the properly padded one
val properlyPaddedPlaintext = PaddingInputStream(plaintext.inputStream(), plaintext.size.toLong()).readFully()
val properlyPaddedCiphertext = encryptPrePaddedBytes(properlyPaddedPlaintext, key, iv)
val properlyPaddedDigest = getDigest(properlyPaddedCiphertext)
val newDigest = SignalDatabase.attachments.getAttachment(attachmentId)!!.remoteDigest!!
assertArrayEquals(properlyPaddedDigest, newDigest)
}
@Test
fun finalizeAttachmentAfterDownload_leaveDigestAloneForAllZeroPadding() {
// Insert attachment metadata for properly-padded attachment
val plaintext = byteArrayOf(1, 2, 3, 4)
val key = Util.getSecretBytes(64)
val iv = Util.getSecretBytes(16)
val paddedPlaintext = PaddingInputStream(plaintext.inputStream(), plaintext.size.toLong()).readFully()
val ciphertext = encryptPrePaddedBytes(paddedPlaintext, key, iv)
val digest = getDigest(ciphertext)
val cipherFile = getTempFile()
cipherFile.writeBytes(ciphertext)
val mmsId = -1L
val attachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(mmsId, listOf(createAttachmentPointer(key, digest, plaintext.size)), emptyList()).values.first()
// Give data to attachment table
val cipherInputStream = AttachmentCipherInputStream.createForAttachment(cipherFile, plaintext.size.toLong(), key, digest, null, 4, false)
SignalDatabase.attachments.finalizeAttachmentAfterDownload(mmsId, attachmentId, cipherInputStream, iv)
// Verify the digest hasn't changed
val newDigest = SignalDatabase.attachments.getAttachment(attachmentId)!!.remoteDigest!!
assertArrayEquals(digest, newDigest)
}
private fun createAttachmentPointer(key: ByteArray, digest: ByteArray, size: Int): Attachment {
return PointerAttachment.forPointer(
pointer = Optional.of(
SignalServiceAttachmentPointer(
cdnNumber = 3,
remoteId = SignalServiceAttachmentRemoteId.V4("asdf"),
contentType = MediaUtil.IMAGE_JPEG,
key = key,
size = Optional.of(size),
preview = Optional.empty(),
width = 2,
height = 2,
digest = Optional.of(digest),
incrementalDigest = Optional.empty(),
incrementalMacChunkSize = 0,
fileName = Optional.of("file.jpg"),
voiceNote = false,
isBorderless = false,
isGif = false,
caption = Optional.empty(),
blurHash = Optional.empty(),
uploadTimestamp = 0,
uuid = null
)
)
).get()
}
private fun createAttachment(id: Long, uri: Uri, transformProperties: AttachmentTable.TransformProperties): UriAttachment {
return UriAttachmentBuilder.build(
id,
@@ -179,4 +281,24 @@ class AttachmentTableTest {
private fun createMediaStream(byteArray: ByteArray): MediaStream {
return MediaStream(byteArray.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
}
private fun getDigest(ciphertext: ByteArray): ByteArray {
val digestStream = NoCipherOutputStream(NullOutputStream)
ciphertext.inputStream().copyTo(digestStream)
return digestStream.transmittedDigest
}
private fun encryptPrePaddedBytes(plaintext: ByteArray, key: ByteArray, iv: ByteArray): ByteArray {
val outputStream = ByteArrayOutputStream()
val cipherStream = AttachmentCipherOutputStream(key, iv, outputStream)
plaintext.inputStream().copyTo(cipherStream)
return outputStream.toByteArray()
}
private fun getTempFile(): File {
val dir = InstrumentationRegistry.getInstrumentation().targetContext.getDir("temp", Context.MODE_PRIVATE)
dir.mkdir()
return File.createTempFile("transfer", ".mms", dir)
}
}

View File

@@ -14,7 +14,8 @@ import org.junit.runner.RunWith
import org.signal.core.util.Base64
import org.signal.core.util.update
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaName
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.MediaStream
@@ -24,6 +25,10 @@ import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
import org.whispersystems.signalservice.api.backup.MediaId
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.File
import java.util.UUID
@@ -46,9 +51,9 @@ class AttachmentTableTest_deduping {
@Before
fun setUp() {
SignalStore.account().setAci(ServiceId.ACI.from(UUID.randomUUID()))
SignalStore.account().setPni(ServiceId.PNI.from(UUID.randomUUID()))
SignalStore.account().setE164("+15558675309")
SignalStore.account.setAci(ServiceId.ACI.from(UUID.randomUUID()))
SignalStore.account.setPni(ServiceId.PNI.from(UUID.randomUUID()))
SignalStore.account.setE164("+15558675309")
SignalDatabase.attachments.deleteAllAttachments()
}
@@ -106,15 +111,6 @@ class AttachmentTableTest_deduping {
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Non-matching mp4 fast start
test {
val id1 = insertWithData(DATA_A, TransformProperties(mp4FastStart = true))
val id2 = insertWithData(DATA_A, TransformProperties(mp4FastStart = false))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
}
/**
@@ -194,6 +190,8 @@ class AttachmentTableTest_deduping {
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Mimics sending two files at once. Ensures all fields are kept in sync as we compress and upload.
@@ -219,6 +217,7 @@ class AttachmentTableTest_deduping {
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Re-use the upload when uploaded recently
@@ -233,6 +232,7 @@ class AttachmentTableTest_deduping {
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
}
@@ -252,6 +252,16 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id2, true)
assertDoesNotHaveRemoteFields(id2)
assertArchiveFieldsMatch(id1, id2)
upload(id2)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
assertRemoteFieldsMatch(id1, id2)
}
// This isn't so much "desirable behavior" as it is documenting how things work.
@@ -281,6 +291,7 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// This represents what would happen if you edited a video, sent it, then forwarded it. We should match, skip transform, and skip upload.
@@ -296,6 +307,7 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// This represents what would happen if you edited a video, sent it, then forwarded it, but *edited the forwarded video*. We should not dedupe.
@@ -326,6 +338,7 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// This represents what would happen if you sent an image using high quality, then forwarded it using standard quality.
@@ -342,6 +355,7 @@ class AttachmentTableTest_deduping {
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Make sure that files marked as unhashable are all updated together
@@ -456,6 +470,7 @@ class AttachmentTableTest_deduping {
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
// Making sure things work for quotes of videos, which have trickier transform properties
@@ -469,6 +484,7 @@ class AttachmentTableTest_deduping {
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
}
@@ -646,7 +662,17 @@ class AttachmentTableTest_deduping {
}
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createPointerAttachment(attachmentId, uploadTimestamp), uploadTimestamp)
SignalDatabase.attachments.createKeyIvIfNecessary(attachmentId)
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createUploadResult(attachmentId, uploadTimestamp))
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
SignalDatabase.attachments.setArchiveData(
attachmentId = attachmentId,
archiveCdn = Cdn.CDN_3.cdnNumber,
archiveMediaName = attachment.getMediaName().name,
archiveThumbnailMediaId = MediaId(Util.getSecretBytes(15)).encode(),
archiveMediaId = MediaId(Util.getSecretBytes(15)).encode()
)
}
fun delete(attachmentId: AttachmentId) {
@@ -739,10 +765,20 @@ class AttachmentTableTest_deduping {
assertEquals(lhsAttachment.remoteLocation, rhsAttachment.remoteLocation)
assertEquals(lhsAttachment.remoteKey, rhsAttachment.remoteKey)
assertArrayEquals(lhsAttachment.remoteIv, rhsAttachment.remoteIv)
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
assertEquals(lhsAttachment.cdnNumber, rhsAttachment.cdnNumber)
assertEquals(lhsAttachment.cdn.cdnNumber, rhsAttachment.cdn.cdnNumber)
}
fun assertArchiveFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
assertEquals(lhsAttachment.archiveCdn, rhsAttachment.archiveCdn)
assertEquals(lhsAttachment.archiveMediaName, rhsAttachment.archiveMediaName)
assertEquals(lhsAttachment.archiveMediaId, rhsAttachment.archiveMediaId)
}
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
@@ -751,7 +787,7 @@ class AttachmentTableTest_deduping {
assertNull(databaseAttachment.remoteLocation)
assertNull(databaseAttachment.remoteDigest)
assertNull(databaseAttachment.remoteKey)
assertEquals(0, databaseAttachment.cdnNumber)
assertEquals(0, databaseAttachment.cdn.cdnNumber)
}
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
@@ -763,35 +799,19 @@ class AttachmentTableTest_deduping {
return MediaStream(this.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
}
private fun createPointerAttachment(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): PointerAttachment {
val location = "somewhere-${Random.nextLong()}"
val key = "somekey-${Random.nextLong()}"
val digest = Random.nextBytes(32)
val incrementalDigest = Random.nextBytes(16)
private fun createUploadResult(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): AttachmentUploadResult {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
return PointerAttachment(
"image/jpeg",
AttachmentTable.TRANSFER_PROGRESS_DONE,
databaseAttachment.size, // size
null,
3, // cdnNumber
location,
key,
digest,
incrementalDigest,
5, // incrementalMacChunkSize
null,
databaseAttachment.voiceNote,
databaseAttachment.borderless,
databaseAttachment.videoGif,
databaseAttachment.width,
databaseAttachment.height,
uploadTimestamp,
databaseAttachment.caption,
databaseAttachment.stickerLocator,
databaseAttachment.blurHash
return AttachmentUploadResult(
remoteId = SignalServiceAttachmentRemoteId.V4("somewhere-${Random.nextLong()}"),
cdnNumber = Cdn.CDN_3.cdnNumber,
key = databaseAttachment.remoteKey?.let { Base64.decode(it) } ?: Util.getSecretBytes(64),
iv = databaseAttachment.remoteIv ?: Util.getSecretBytes(16),
digest = Random.nextBytes(32),
incrementalDigest = Random.nextBytes(16),
incrementalDigestChunkSize = 5,
uploadTimestamp = uploadTimestamp,
dataSize = databaseAttachment.size
)
}
}

View File

@@ -86,7 +86,8 @@ class CallLinkTableTest {
linkKeyBytes = roomId,
adminPassBytes = null
),
state = SignalCallLinkState()
state = SignalCallLinkState(),
deletionTimestamp = 0L
)
)

View File

@@ -56,7 +56,7 @@ class CallTableTest {
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
SignalDatabase.calls.deleteGroupCall(call!!)
SignalDatabase.calls.markCallDeletedFromSyncEvent(call!!)
val deletedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
val oldestDeletionTimestamp = SignalDatabase.calls.getOldestDeletionTimestamp()
@@ -69,9 +69,10 @@ class CallTableTest {
@Test
fun givenNoPreExistingEvent_whenIDeleteGroupCall_thenIInsertAndMarkCallDeleted() {
val callId = 1L
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
callId,
groupRecipientId,
CallTable.Type.GROUP_CALL,
CallTable.Direction.OUTGOING,
System.currentTimeMillis()
)
@@ -438,11 +439,12 @@ class CallTableTest {
@Test
fun givenADeletedCallEvent_whenIReceiveARingUpdate_thenIIgnoreTheRingUpdate() {
val callId = 1L
SignalDatabase.calls.insertDeletedGroupCallFromSyncEvent(
SignalDatabase.calls.insertDeletedCallFromSyncEvent(
callId = callId,
recipientId = groupRecipientId,
direction = CallTable.Direction.INCOMING,
timestamp = System.currentTimeMillis()
timestamp = System.currentTimeMillis(),
type = CallTable.Type.GROUP_CALL
)
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(

View File

@@ -15,7 +15,7 @@ import org.signal.core.util.getIndexes
import org.signal.core.util.readToList
import org.signal.core.util.requireNonNullString
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.testing.SignalActivityRule
/**
@@ -30,7 +30,7 @@ class DatabaseConsistencyTest {
@Test
fun testUpgradeConsistency() {
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
val testHelper = InMemoryTestHelper(ApplicationDependencies.getApplication()).also {
val testHelper = InMemoryTestHelper(AppDependencies.application).also {
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
}

View File

@@ -8,7 +8,7 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
@@ -26,7 +26,7 @@ class DatabaseObserverTest {
@Before
fun setup() {
db = SignalDatabase.instance!!.signalWritableDatabase
observer = ApplicationDependencies.getDatabaseObserver()
observer = AppDependencies.databaseObserver
}
@Test

View File

@@ -25,15 +25,6 @@ class DistributionListTablesTest {
Assert.assertNotNull(id)
}
@Test
fun createList_whenNameConflict_failToInsert() {
val id: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNotNull(id)
val id2: DistributionListId? = distributionDatabase.createList("test", recipientList(1, 2, 3))
Assert.assertNull(id2)
}
@Test
fun getList_returnCorrectList() {
createRecipients(3)

View File

@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.database
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
@@ -75,21 +74,6 @@ class GroupTableTest {
assertEquals(2, groups.size)
}
@Test
fun givenGroups_whenIQueryGroupsByMembership_thenIExpectBothGroups() {
insertPushGroup()
insertMmsGroup(members = listOf(harness.others[1]))
val groups = groupTable.queryGroupsByMembership(
setOf(harness.self.id, harness.others[1]),
includeInactive = false,
excludeV1 = false,
excludeMms = false
)
assertEquals(2, groups.cursor?.count)
}
@Test
fun givenGroups_whenIGetGroups_thenIExpectBothGroups() {
insertPushGroup()
@@ -181,72 +165,10 @@ class GroupTableTest {
assertFalse(actual)
}
@Test
fun givenAGroup_whenIUpdateMembers_thenIExpectUpdatedMembers() {
val v2Group = insertPushGroup()
groupTable.updateMembers(v2Group, listOf(harness.self.id, harness.others[1]))
val groupRecord = groupTable.getGroup(v2Group)
assertEquals(setOf(harness.self.id, harness.others[1]), groupRecord.get().members.toSet())
}
@Test
fun givenAnMmsGroup_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val members: List<RecipientId> = listOf(harness.self.id, harness.others[0])
val other = insertMmsGroup(members + listOf(harness.others[1]))
val mmsGroup = insertMmsGroup(members)
val actual = groupTable.getOrCreateMmsGroupForMembers(members.toSet())
assertNotEquals(other, actual)
assertEquals(mmsGroup, actual)
}
@Test
fun givenMultipleMmsGroups_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1])
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2])
val group1: GroupId = insertMmsGroup(group1Members)
val group2: GroupId = insertMmsGroup(group2Members)
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.toSet())
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.toSet())
assertEquals(group1, group1Result)
assertEquals(group2, group2Result)
assertNotEquals(group1Result, group2Result)
}
@Test
fun givenMultipleMmsGroupsWithDifferentMemberOrders_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val group1Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[1], harness.others[2]).shuffled()
val group2Members: List<RecipientId> = listOf(harness.self.id, harness.others[0], harness.others[2], harness.others[3]).shuffled()
val group1: GroupId = insertMmsGroup(group1Members)
val group2: GroupId = insertMmsGroup(group2Members)
val group1Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group1Members.shuffled().toSet())
val group2Result: GroupId = groupTable.getOrCreateMmsGroupForMembers(group2Members.shuffled().toSet())
assertEquals(group1, group1Result)
assertEquals(group2, group2Result)
assertNotEquals(group1Result, group2Result)
}
@Test
fun givenMmsGroupWithOneMember_whenIGetOrCreateMmsGroup_thenIExpectMyMmsGroup() {
val groupMembers: List<RecipientId> = listOf(harness.self.id)
val group: GroupId = insertMmsGroup(groupMembers)
val groupResult: GroupId = groupTable.getOrCreateMmsGroupForMembers(groupMembers.toSet())
assertEquals(group, groupResult)
}
@Test
fun givenTwoGroupsWithoutMembers_whenIQueryThem_thenIExpectEach() {
val g1 = insertPushGroup(listOf())
val g2 = insertPushGroup(listOf())
val g1 = insertPushGroup(members = emptyList())
val g2 = insertPushGroup(members = emptyList())
val gr1 = groupTable.getGroup(g1)
val gr2 = groupTable.getGroup(g2)
@@ -273,6 +195,85 @@ class GroupTableTest {
assertEquals(groups[0].id, groupInCommon)
}
@Test
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForTheSharedToken_thenIExpectBothGroups() {
insertPushGroup("Group Alice")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(2, it.cursor?.count)
val firstGroup = it.getNext()
val secondGroup = it.getNext()
assertEquals("Group Alice", firstGroup?.title)
assertEquals("Group Bob", secondGroup?.title)
}
}
@Test
fun givenTwoGroupsWithANameThatSharesAToken_whenISearchForAnUnsharedToken_thenIExpectOneGroup() {
insertPushGroup("Group Alice")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Alice",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(1, it.cursor?.count)
val firstGroup = it.getNext()
assertEquals("Group Alice", firstGroup?.title)
}
}
@Test
fun givenAGroupWithThreeTokens_whenISearchForTheFirstAndLastToken_thenIExpectThatGroup() {
insertPushGroup("Group & Alice")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group Alice",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(1, it.cursor?.count)
val firstGroup = it.getNext()
assertEquals("Group & Alice", firstGroup?.title)
}
}
@Test
fun givenTwoGroupsWithSharedTokens_whenISearchForAnExactMatch_thenIExpectThatGroupFirst() {
insertPushGroup("Group Alice Bob")
insertPushGroup("Group Bob")
SignalDatabase.groups.queryGroupsByTitle(
inputQuery = "Group Bob",
includeInactive = false,
excludeV1 = false,
excludeMms = false
).use {
assertEquals(2, it.cursor?.count)
val firstGroup = it.getNext()
val second = it.getNext()
assertEquals("Group Bob", firstGroup?.title)
assertEquals("Group Alice Bob", second?.title)
}
}
private fun insertThread(groupId: GroupId): Long {
val groupRecipient = SignalDatabase.recipients.getByGroupId(groupId).get()
return SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(groupRecipient))
@@ -292,6 +293,7 @@ class GroupTableTest {
}
private fun insertPushGroup(
title: String = "Test Group",
members: List<DecryptedMember> = listOf(
DecryptedMember.Builder()
.aciBytes(harness.self.requireAci().toByteString())
@@ -307,11 +309,12 @@ class GroupTableTest {
): GroupId {
val groupMasterKey = GroupMasterKey(Random.nextBytes(GroupMasterKey.SIZE))
val decryptedGroupState = DecryptedGroup.Builder()
.title(title)
.members(members)
.revision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState)!!
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
}
private fun insertPushGroupWithSelfAndOthers(others: List<RecipientId>): GroupId {
@@ -336,6 +339,6 @@ class GroupTableTest {
.revision(0)
.build()
return groupTable.create(groupMasterKey, decryptedGroupState)!!
return groupTable.create(groupMasterKey, decryptedGroupState, null)!!
}
}

View File

@@ -159,7 +159,7 @@ class KyberPreKeyTableTest {
val count = SignalDatabase.rawDatabase
.update(KyberPreKeyTable.TABLE_NAME)
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
assertEquals(1, count)
@@ -169,8 +169,15 @@ class KyberPreKeyTableTest {
return SignalDatabase.rawDatabase
.select(KyberPreKeyTable.STALE_TIMESTAMP)
.from(KyberPreKeyTable.TABLE_NAME)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
}
private fun ServiceId.toAccountId(): String {
return when (this) {
is ACI -> this.toString()
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
}
}
}

View File

@@ -12,12 +12,12 @@ import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.updateAll
import org.thoughtcrime.securesms.crash.CrashConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.testing.assertIs
class LogDatabaseTest {
private val db: LogDatabase = LogDatabase.getInstance(ApplicationDependencies.getApplication())
private val db: LogDatabase = LogDatabase.getInstance(AppDependencies.application)
@Test
fun crashTable_matchesNamePattern() {

View File

@@ -30,8 +30,8 @@ class MessageTableTest_gifts {
mms.deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
}

View File

@@ -40,14 +40,14 @@ class MmsTableTest_stories {
mms.deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
myStory = Recipient.resolved(SignalDatabase.recipients.getOrInsertFromDistributionListId(DistributionListId.MY_STORY))
recipients = (0 until 5).map { SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID())) }
releaseChannelRecipient = Recipient.resolved(SignalDatabase.recipients.insertReleaseChannelRecipient())
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelRecipient.id)
SignalStore.releaseChannel.setReleaseChannelRecipientId(releaseChannelRecipient.id)
}
@Test

View File

@@ -0,0 +1,239 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedMember
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIsSize
@RunWith(AndroidJUnit4::class)
class NameCollisionTablesTest {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
private lateinit var charlie: RecipientId
@Before
fun setUp() {
alice = setUpRecipient(harness.others[0])
bob = setUpRecipient(harness.others[1])
charlie = setUpRecipient(harness.others[2])
}
@Test
fun givenAUserWithAThreadIdButNoConflicts_whenIGetCollisionsForThreadRecipient_thenIExpectNoCollisions() {
val threadRecipientId = alice
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(threadRecipientId))
val actual = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(threadRecipientId)
actual assertIsSize 0
}
@Test
fun givenTwoUsers_whenOneChangesTheirProfileNameToMatchTheOther_thenIExpectANameCollision() {
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
actualAlice assertIsSize 2
actualBob assertIsSize 2
}
@Test
fun givenTwoUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectNoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
actualAlice assertIsSize 0
actualBob assertIsSize 0
}
@Test
fun givenThreeUsersWithANameCollisions_whenOneChangesToADifferentName_thenIExpectTwoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
setProfileName(charlie, ProfileName.fromParts("Bob", "Android"))
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
val actualBob = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
val actualCharlie = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(charlie)
actualAlice assertIsSize 0
actualBob assertIsSize 2
actualCharlie assertIsSize 2
}
@Test
fun givenTwoUsersWithADismissedNameCollision_whenOneChangesToADifferentNameAndBack_thenIExpectANameCollision() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
setProfileName(alice, ProfileName.fromParts("Alice", "Android"))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
val actualAlice = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
actualAlice assertIsSize 2
}
@Test
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
actualCollisions assertIsSize 0
}
@Test
fun givenADismissedNameCollisionForAliceThatIUpdate_whenIGetNameCollisionsForAlice_thenIExpectNoNameCollisions() {
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(alice)
actualCollisions assertIsSize 0
}
@Test
fun givenADismissedNameCollisionForAlice_whenIGetNameCollisionsForBob_thenIExpectANameCollisionWithTwoEntries() {
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
setProfileName(alice, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(alice)
val actualCollisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(bob)
actualCollisions assertIsSize 2
}
@Test
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
collisions assertIsSize 2
}
@Test
fun givenAGroupWithAliceAndBobWithDismissedCollision_whenIInsertNameChangeMessageForAlice_thenIExpectAGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Bob", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
SignalDatabase.nameCollisions.markCollisionsForThreadRecipientDismissed(info.recipientId)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Bob Android", "Alice Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
collisions assertIsSize 0
}
@Test
fun givenAGroupWithAliceAndBob_whenIInsertNameChangeMessageForAliceWithMismatch_thenIExpectNoGroupNameCollision() {
val alice = Recipient.resolved(alice)
val bob = Recipient.resolved(bob)
val info = createGroup()
setProfileName(alice.id, ProfileName.fromParts("Alice", "Android"))
setProfileName(bob.id, ProfileName.fromParts("Bob", "Android"))
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(info.recipientId))
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "Alice Android", "Bob Android")
val collisions = SignalDatabase.nameCollisions.getCollisionsForThreadRecipientId(info.recipientId)
collisions assertIsSize 0
}
private fun setUpRecipient(recipientId: RecipientId): RecipientId {
SignalDatabase.recipients.setProfileSharing(recipientId, false)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(recipientId, false)
MmsHelper.insert(
threadId = threadId,
message = IncomingMessage(
type = MessageType.NORMAL,
from = recipientId,
groupId = null,
body = "hi",
sentTimeMillis = 100L,
receivedTimeMillis = 200L,
serverTimeMillis = 100L,
isUnidentified = true
)
)
return recipientId
}
private fun setProfileName(recipientId: RecipientId, name: ProfileName) {
SignalDatabase.recipients.setProfileName(recipientId, name)
SignalDatabase.nameCollisions.handleIndividualNameCollision(recipientId)
}
private fun createGroup(): GroupTestingUtils.TestGroupInfo {
return GroupTestingUtils.insertGroup(
revision = 0,
DecryptedMember(
aciBytes = harness.self.requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
),
DecryptedMember(
aciBytes = Recipient.resolved(alice).requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
),
DecryptedMember(
aciBytes = Recipient.resolved(bob).requireAci().toByteString(),
role = Member.Role.ADMINISTRATOR
)
)
}
}

View File

@@ -120,7 +120,7 @@ class OneTimePreKeyTableTest {
val count = SignalDatabase.rawDatabase
.update(OneTimePreKeyTable.TABLE_NAME)
.values(OneTimePreKeyTable.STALE_TIMESTAMP to staleTime)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
assertEquals(1, count)
@@ -130,8 +130,15 @@ class OneTimePreKeyTableTest {
return SignalDatabase.rawDatabase
.select(OneTimePreKeyTable.STALE_TIMESTAMP)
.from(OneTimePreKeyTable.TABLE_NAME)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account)
.where("${OneTimePreKeyTable.ACCOUNT_ID} = ? AND ${OneTimePreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
.readToSingleObject { it.requireLongOrNull(OneTimePreKeyTable.STALE_TIMESTAMP) }
}
private fun ServiceId.toAccountId(): String {
return when (this) {
is ACI -> this.toString()
is PNI -> OneTimePreKeyTable.PNI_ACCOUNT_ID
}
}
}

View File

@@ -9,7 +9,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
import org.thoughtcrime.securesms.storage.StorageSyncModels
@@ -28,7 +28,7 @@ class RecipientTableTest_applyStorageSyncContactUpdate {
@Test
fun insertMessageOnVerifiedToDefault() {
// GIVEN
val identities = ApplicationDependencies.getProtocolStore().aci().identities()
val identities = AppDependencies.protocolStore.aci().identities()
val other = Recipient.resolved(harness.others[0])
MmsHelper.insert(recipient = other)

View File

@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMessage
@@ -53,9 +53,9 @@ class RecipientTableTest_getAndPossiblyMerge {
@Before
fun setup() {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
SignalStore.account.setE164(E164_SELF)
SignalStore.account.setAci(ACI_SELF)
SignalStore.account.setPni(PNI_SELF)
}
@Test
@@ -1103,8 +1103,8 @@ class RecipientTableTest_getAndPossiblyMerge {
init {
// Need to delete these first to prevent foreign key crash
SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list")
SignalDatabase.rawDatabase.execSQL("DELETE FROM distribution_list_member")
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.ListTable.TABLE_NAME}")
SignalDatabase.rawDatabase.execSQL("DELETE FROM ${DistributionListTables.MembershipTable.TABLE_NAME}")
SqlUtil.getAllTables(SignalDatabase.rawDatabase)
.filterNot { it.contains("sqlite") || it.contains("fts") || it.startsWith("emoji_search_") } // If we delete these we'll corrupt the DB
@@ -1113,8 +1113,8 @@ class RecipientTableTest_getAndPossiblyMerge {
SignalDatabase.rawDatabase.execSQL("DELETE FROM $table")
}
ApplicationDependencies.getRecipientCache().clear()
ApplicationDependencies.getRecipientCache().clearSelf()
AppDependencies.recipientCache.clear()
AppDependencies.recipientCache.clearSelf()
RecipientId.clearCache()
}

View File

@@ -4,9 +4,11 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.testing.SignalFlakyTestRule
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicBoolean
@@ -18,6 +20,9 @@ class SQLiteDatabaseTest {
private lateinit var db: SQLiteDatabase
@get:Rule
val flakyTestRule = SignalFlakyTestRule()
@Before
fun setup() {
db = SignalDatabase.instance!!.signalWritableDatabase
@@ -181,7 +186,8 @@ class SQLiteDatabaseTest {
assertTrue(hasRun2.get())
}
@Test
// @SignalFlakyTest
// @Test
fun runPostSuccessfulTransaction_runsAfterMainTransactionInNestedTransaction() {
val hasRun1 = AtomicBoolean(false)
val hasRun2 = AtomicBoolean(false)

View File

@@ -10,7 +10,9 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Hex
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.database.model.databaseprotos.addMember
import org.thoughtcrime.securesms.database.model.databaseprotos.addRequestingMember
import org.thoughtcrime.securesms.database.model.databaseprotos.deleteRequestingMember
@@ -44,8 +46,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
recipients = SignalDatabase.recipients
sms = SignalDatabase.messages
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
alice = recipients.getOrInsertFromServiceId(aliceServiceId)
bob = recipients.getOrInsertFromServiceId(bobServiceId)
@@ -286,11 +288,18 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
private fun groupUpdateMessage(sender: RecipientId, groupContext: DecryptedGroupV2Context): IncomingMessage {
wallClock++
val updateDescription = GV2UpdateDescription(
gv2ChangeDescription = groupContext,
groupChangeUpdate = GroupsV2UpdateMessageConverter.translateDecryptedChangeUpdate(SignalStore.account.getServiceIds(), groupContext)
)
return IncomingMessage.groupUpdate(
from = sender,
timestamp = wallClock,
groupId = groupId,
groupContext = groupContext,
update = updateDescription,
isGroupAdd = false,
serverGuid = null
)
}

View File

@@ -5,6 +5,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.stickers.StickerLocator
import java.util.UUID
object UriAttachmentBuilder {
fun build(
@@ -22,23 +23,28 @@ object UriAttachmentBuilder {
stickerLocator: StickerLocator? = null,
blurHash: BlurHash? = null,
audioHash: AudioHash? = null,
transformProperties: AttachmentTable.TransformProperties? = null
transformProperties: AttachmentTable.TransformProperties? = null,
uuid: UUID? = UUID.randomUUID()
): UriAttachment {
return UriAttachment(
uri,
contentType,
transferState,
size,
fileName,
voiceNote,
borderless,
videoGif,
quote,
caption,
stickerLocator,
blurHash,
audioHash,
transformProperties
dataUri = uri,
contentType = contentType,
transferState = transferState,
size = size,
width = 0,
height = 0,
fileName = fileName,
fastPreflightId = null,
voiceNote = voiceNote,
borderless = borderless,
videoGif = videoGif,
quote = quote,
caption = caption,
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = audioHash,
transformProperties = transformProperties,
uuid = uuid
)
}
}

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toDecimalValue
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.FiatValue
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.testing.assertIs
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.math.BigDecimal
import java.util.Currency
@RunWith(AndroidJUnit4::class)
class FixInAppCurrencyIfAbleTest {
@get:Rule
val harness = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
@Test
fun givenNoSubscribers_whenIMigrate_thenIDoNothing() {
migrate()
}
@Test
fun givenASubscriberButNoPayment_whenIMigrate_thenIDoNothing() {
val subscriber = insertSubscriber("USD")
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndMismatchedPayment_whenIMigrate_thenIDoNothing() {
val subscriber = insertSubscriber("USD")
val otherSubscriber = insertSubscriber("EUR")
insertPayment(otherSubscriber)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndPaymentWithNoSubscriber_whenIMigrate_thenDoNothing() {
val subscriber = insertSubscriber("USD")
insertPayment(null)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs ""
}
@Test
fun givenASubscriberAndMatchingPayment_whenIMigrate_thenUpdateCurrencyCode() {
val subscriber = insertSubscriber("USD")
insertPayment(subscriber)
clearCurrencyCode(subscriber)
migrate()
getCurrencyCode(subscriber) assertIs "USD"
}
@Test
fun givenASupercededSubscriber_whenIMigrate_thenIDoNothing() {
val oldSubscriber = insertSubscriber("USD")
insertPayment(oldSubscriber)
clearCurrencyCode(oldSubscriber)
insertSubscriber("USD")
migrate()
}
private fun migrate() {
V236_FixInAppSubscriberCurrencyIfAble.migrate(
context = InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application,
db = SignalDatabase.rawDatabase,
oldVersion = 0,
newVersion = 0
)
}
private fun insertSubscriber(currencyCode: String): InAppPaymentSubscriberRecord {
val record = InAppPaymentSubscriberRecord(
subscriberId = SubscriberId.generate(),
currency = Currency.getInstance(currencyCode),
type = InAppPaymentSubscriberRecord.Type.DONATION,
requiresCancel = false,
paymentMethodType = InAppPaymentData.PaymentMethodType.PAYPAL
)
SignalDatabase.inAppPaymentSubscribers.insertOrReplace(record)
return record
}
private fun clearCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord) {
SignalDatabase.rawDatabase.update(InAppPaymentSubscriberTable.TABLE_NAME)
.values(InAppPaymentSubscriberTable.CURRENCY_CODE to "")
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
.run()
}
private fun getCurrencyCode(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord): String {
return SignalDatabase.rawDatabase.select(InAppPaymentSubscriberTable.CURRENCY_CODE)
.from(InAppPaymentSubscriberTable.TABLE_NAME)
.where("${InAppPaymentSubscriberTable.SUBSCRIBER_ID} = ?", inAppPaymentSubscriberRecord.subscriberId.serialize())
.run()
.readToSingleObject { it.requireNonNullString(InAppPaymentSubscriberTable.CURRENCY_CODE) }!!
}
private fun insertPayment(inAppPaymentSubscriberRecord: InAppPaymentSubscriberRecord?): InAppPaymentTable.InAppPayment {
val id = SignalDatabase.inAppPayments.insert(
type = InAppPaymentType.RECURRING_DONATION,
state = InAppPaymentTable.State.END,
subscriberId = inAppPaymentSubscriberRecord?.subscriberId,
endOfPeriod = null,
inAppPaymentData = InAppPaymentData(
amount = FiatValue(
currencyCode = inAppPaymentSubscriberRecord?.currency?.currencyCode ?: "USD",
amount = BigDecimal.ONE.toDecimalValue()
),
level = 200,
paymentMethodType = inAppPaymentSubscriberRecord?.paymentMethodType ?: InAppPaymentData.PaymentMethodType.UNKNOWN
)
)
return SignalDatabase.inAppPayments.getById(id)!!
}
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.dependencies
import android.app.Application
import io.mockk.spyk
import okhttp3.ConnectionSpec
import okhttp3.Response
import okhttp3.WebSocket
@@ -23,6 +24,9 @@ import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.Verb
import org.thoughtcrime.securesms.testing.runSync
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.SignalServiceDataStore
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.SignalWebSocket
import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
@@ -30,6 +34,7 @@ import org.whispersystems.signalservice.internal.configuration.SignalServiceConf
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl
import org.whispersystems.signalservice.internal.configuration.SignalStorageUrl
import org.whispersystems.signalservice.internal.configuration.SignalSvr2Url
import org.whispersystems.signalservice.internal.push.PushServiceSocket
import java.util.Optional
/**
@@ -37,12 +42,13 @@ import java.util.Optional
*
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess].
*/
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : AppDependencies.Provider by default {
private val serviceTrustStore: TrustStore
private val uncensoredConfiguration: SignalServiceConfiguration
private val serviceNetworkAccessMock: SignalServiceNetworkAccess
private val recipientCache: LiveRecipientCache
private var signalServiceMessageSender: SignalServiceMessageSender? = null
init {
runSync {
@@ -53,18 +59,21 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
Get("/v1/websocket/?login=") {
MockResponse().success().withWebSocketUpgrade(mockIdentifiedWebSocket)
},
Get("/v1/websocket", { !it.path.contains("login") }) {
Get("/v1/websocket", {
val path = it.path
return@Get path == null || !path.contains("login")
}) {
MockResponse().success().withWebSocketUpgrade(object : WebSocketListener() {})
}
)
}
webServer.setDispatcher(object : Dispatcher() {
webServer.dispatcher = object : Dispatcher() {
override fun dispatch(request: RecordedRequest): MockResponse {
val handler = handlers.firstOrNull { it.requestPredicate(request) }
return handler?.responseFactory?.invoke(request) ?: MockResponse().setResponseCode(500)
}
})
}
serviceTrustStore = SignalServiceTrustStore(application)
uncensoredConfiguration = SignalServiceConfiguration(
@@ -101,6 +110,17 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
return recipientCache
}
override fun provideSignalServiceMessageSender(
signalWebSocket: SignalWebSocket,
protocolStore: SignalServiceDataStore,
pushServiceSocket: PushServiceSocket
): SignalServiceMessageSender {
if (signalServiceMessageSender == null) {
signalServiceMessageSender = spyk(objToCopy = default.provideSignalServiceMessageSender(signalWebSocket, protocolStore, pushServiceSocket))
}
return signalServiceMessageSender!!
}
class MockWebSocket : WebSocketListener() {
private val TAG = "MockWebSocket"

View File

@@ -16,7 +16,7 @@ import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.providers.BlobProvider
@@ -38,7 +38,7 @@ class AttachmentCompressionJobTest {
StreamUtil.readFully(it)
}
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(ApplicationDependencies.getApplication())
val blob = BlobProvider.getInstance().forData(imageBytes).createForSingleSessionOnDisk(AppDependencies.application)
val firstPreUpload = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
val firstDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(firstPreUpload)
@@ -51,12 +51,12 @@ class AttachmentCompressionJobTest {
val secondJobLatch = CountDownLatch(1)
val jobThread = Thread {
firstCompressionJob.setContext(ApplicationDependencies.getApplication())
firstCompressionJob.setContext(AppDependencies.application)
firstJobResult = firstCompressionJob.run()
secondJobLatch.await()
secondCompressionJob!!.setContext(ApplicationDependencies.getApplication())
secondCompressionJob!!.setContext(AppDependencies.application)
secondJobResult = secondCompressionJob!!.run()
}

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.jobs
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.EventTimer
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.JobDatabase.Companion.getInstance
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JobManager
import org.thoughtcrime.securesms.jobmanager.JobMigrator
import org.thoughtcrime.securesms.jobmanager.JobTracker
import org.thoughtcrime.securesms.util.TextSecurePreferences
import java.util.concurrent.CountDownLatch
import kotlin.random.Random
@Ignore("This is just for testing performance, not correctness, and they can therefore take a long time. Run them manually when you need to.")
@RunWith(AndroidJUnit4::class)
class JobManagerPerformanceTests {
companion object {
val TAG = Log.tag(JobManagerPerformanceTests::class.java)
}
@Test
fun testPerformance_singleQueue() {
runTest("singleQueue", 2000) { TestJob(queue = "queue") }
}
@Test
fun testPerformance_fourQueues() {
runTest("fourQueues", 2000) { TestJob(queue = "queue-${Random.nextInt(1, 5)}") }
}
@Test
fun testPerformance_noQueues() {
runTest("noQueues", 2000) { TestJob(queue = null) }
}
private fun runTest(name: String, count: Int, jobCreator: () -> TestJob) {
val context = AppDependencies.application
val jobManager = testJobManager(context)
jobManager.beginJobLoop()
val eventTimer = EventTimer()
val latch = CountDownLatch(count)
var seenStart = false
jobManager.addListener({ it.factoryKey == TestJob.KEY }) { _, state ->
if (!seenStart && state == JobTracker.JobState.RUNNING) {
// Adding the jobs can take a while (and runs on a background thread), so we want to reset the timer the first time we see a job run so the first job
// doesn't have a skewed time
eventTimer.reset()
seenStart = true
}
if (state.isComplete) {
eventTimer.emit("job")
latch.countDown()
if (latch.count % 100 == 0L) {
Log.d(TAG, "[$name] Finished ${count - latch.count}/$count jobs")
}
}
}
Log.i(TAG, "[$name] Adding jobs...")
jobManager.addAll((1..count).map { jobCreator() })
Log.i(TAG, "[$name] Waiting for jobs to complete...")
latch.await()
Log.i(TAG, "[$name] Jobs complete!")
Log.i(TAG, eventTimer.stop().summary)
}
private fun testJobManager(context: Application): JobManager {
val config = JobManager.Configuration.Builder()
.setJobFactories(
JobManagerFactories.getJobFactories(context) + mapOf(
TestJob.KEY to TestJob.Factory()
)
)
.setConstraintFactories(JobManagerFactories.getConstraintFactories(context))
.setConstraintObservers(JobManagerFactories.getConstraintObservers(context))
.setJobStorage(FastJobStorage(getInstance(context)))
.setJobMigrator(JobMigrator(TextSecurePreferences.getJobManagerVersion(context), JobManager.CURRENT_VERSION, JobManagerFactories.getJobMigrations(context)))
.build()
return JobManager(context, config)
}
private class TestJob(params: Parameters) : Job(params) {
companion object {
const val KEY = "test"
}
constructor(queue: String?) : this(Parameters.Builder().setQueue(queue).build())
override fun serialize(): ByteArray? = null
override fun getFactoryKey(): String = KEY
override fun run(): Result = Result.success()
override fun onFailure() = Unit
class Factory : Job.Factory<TestJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): TestJob {
return TestJob(parameters)
}
}
}
}

View File

@@ -77,7 +77,7 @@ class EditMessageSyncProcessorTest {
.build()
).build()
).build()
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0)
SignalDatabase.recipients.setExpireMessages(toRecipient.id, content.dataMessage?.expireTimer ?: 0, content.dataMessage?.expireTimerVersion ?: 1)
val syncTextMessage = TestMessage(
envelope = MessageContentFuzzer.envelope(originalTimestamp),
content = syncContent,
@@ -112,7 +112,7 @@ class EditMessageSyncProcessorTest {
testResult.runSync(listOf(syncTextMessage, syncEditMessage))
SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000)
SignalDatabase.recipients.setExpireMessages(toRecipient.id, (content.dataMessage?.expireTimer ?: 0) / 1000, content.dataMessage?.expireTimerVersion ?: 1)
val originalTextMessage = OutgoingMessage(
threadRecipient = toRecipient,
sentTimeMillis = originalTimestamp,

View File

@@ -0,0 +1,290 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import android.net.Uri
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.UriAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.UriAttachmentBuilder
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.MediaUtil
import java.util.UUID
import kotlin.random.Random
/**
* Makes inserting messages through the "normal" code paths simpler. Mostly focused on incoming messages.
*/
class MessageHelper(private val harness: SignalActivityRule, var startTime: Long = System.currentTimeMillis()) {
val alice: RecipientId = harness.others[0]
val bob: RecipientId = harness.others[1]
val group: GroupTestingUtils.TestGroupInfo = harness.group!!
val processor: MessageContentProcessor = MessageContentProcessor(harness.context)
init {
val threadIdSlot = slot<Long>()
mockkStatic(ThreadUpdateJob::class)
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
SignalDatabase.threads.update(threadIdSlot.captured, false)
}
}
fun tearDown() {
unmockkStatic(ThreadUpdateJob::class)
}
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = sender, timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null,
allowExpireTimeChanges = false
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun outgoingText(conversationId: RecipientId = alice, successfulSend: Boolean = true, updateMessage: ((OutgoingMessage) -> OutgoingMessage)? = null): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
val threadRecipient = Recipient.resolved(conversationId)
val message = OutgoingMessage(
threadRecipient = threadRecipient,
body = MessageContentFuzzer.string(),
sentTimeMillis = messageData.timestamp,
isUrgent = true,
isSecure = true
).let { updateMessage?.invoke(it) ?: it }
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
if (successfulSend) {
SignalDatabase.messages.markAsSent(messageId, true)
}
return messageData.copy(messageId = messageId)
}
fun outgoingMessage(conversationId: RecipientId = alice, updateMessage: OutgoingMessage.() -> OutgoingMessage): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
val threadRecipient = Recipient.resolved(conversationId)
val message = OutgoingMessage(
threadRecipient = threadRecipient,
sentTimeMillis = messageData.timestamp,
isUrgent = true,
isSecure = true
).apply { updateMessage() }
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(threadRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(message, threadId, false, null)
return messageData.copy(messageId = messageId)
}
fun outgoingAttachment(data: ByteArray, uuid: UUID? = UUID.randomUUID()): Attachment {
val uri: Uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
val attachment: UriAttachment = UriAttachmentBuilder.build(
id = Random.nextLong(),
uri = uri,
contentType = MediaUtil.IMAGE_JPEG,
transformProperties = AttachmentTable.TransformProperties(),
uuid = uuid
)
return attachment
}
fun outgoingGroupChange(): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = harness.self.id, timestamp = startTime)
val groupRecipient = Recipient.resolved(group.recipientId)
val decryptedGroupV2Context = DecryptedGroupV2Context(
context = group.groupV2Context,
groupState = SignalDatabase.groups.getGroup(group.groupId).get().requireV2GroupProperties().decryptedGroup
)
val updateDescription = GV2UpdateDescription.Builder()
.gv2ChangeDescription(decryptedGroupV2Context)
.groupChangeUpdate(GroupsV2UpdateMessageConverter.translateDecryptedChange(SignalStore.account.getServiceIds(), decryptedGroupV2Context))
.build()
val outgoingMessage = OutgoingMessage.groupUpdateMessage(groupRecipient, updateDescription, startTime)
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(groupRecipient)
val messageId = SignalDatabase.messages.insertMessageOutbox(outgoingMessage, threadId, false, null)
SignalDatabase.messages.markAsSent(messageId, true)
return messageData.copy(messageId = messageId)
}
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = sender, timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzStickerMediaMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime = nextStartTime()
val messageData = MessageData(author = sender, timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.editTextMessage(
targetTimestamp = targetTimestamp,
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
).dataMessage!!
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeMessage(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeMessage(deletes.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeConversation(vararg deletes: MessageContentFuzzer.DeleteForMeSync): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeConversation(deletes.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeLocalOnlyConversation(vararg conversations: RecipientId): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeLocalOnlyConversation(conversations.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): MessageData {
startTime = nextStartTime()
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncDeleteForMeAttachment(conversationId, message, uuid, digest, plainTextHash),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
/**
* Get the next "sentTimestamp" for current + [nextMessageOffset]th message. Useful for early message processing and future message timestamps.
*/
fun nextStartTime(nextMessageOffset: Int = 1): Long {
return startTime + 1000 * nextMessageOffset
}
data class MessageData(
val author: RecipientId = RecipientId.UNKNOWN,
val serverGuid: UUID = UUID.randomUUID(),
val timestamp: Long,
val messageId: Long = -1L
)
}

View File

@@ -16,7 +16,7 @@ import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.Curve
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.AliceClient
@@ -55,8 +55,8 @@ class MessageProcessingPerformanceTest {
@Before
fun setup() {
mockkStatic(UnidentifiedAccessUtil::class)
every { UnidentifiedAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
mockkStatic(SealedSenderAccessUtil::class)
every { SealedSenderAccessUtil.getCertificateValidator() } returns FakeClientHelpers.noOpCertificateValidator
mockkObject(MessageContentProcessor)
every { MessageContentProcessor.create(harness.application) } returns TimingMessageContentProcessor(harness.application)
@@ -64,7 +64,7 @@ class MessageProcessingPerformanceTest {
@After
fun after() {
unmockkStatic(UnidentifiedAccessUtil::class)
unmockkStatic(SealedSenderAccessUtil::class)
unmockkStatic(MessageContentProcessor::class)
}

View File

@@ -6,23 +6,14 @@
package org.thoughtcrime.securesms.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
@@ -31,43 +22,28 @@ class SyncMessageProcessorTest_readSyncs {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
private lateinit var group: GroupTestingUtils.TestGroupInfo
private lateinit var processor: MessageContentProcessor
private lateinit var messageHelper: MessageHelper
@Before
fun setUp() {
alice = harness.others[0]
bob = harness.others[1]
group = harness.group!!
processor = MessageContentProcessor(harness.context)
val threadIdSlot = slot<Long>()
mockkStatic(ThreadUpdateJob::class)
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
SignalDatabase.threads.update(threadIdSlot.captured, false)
}
messageHelper = MessageHelper(harness)
}
@After
fun tearDown() {
unmockkStatic(ThreadUpdateJob::class)
messageHelper.tearDown()
}
@Test
fun handleSynchronizeReadMessage() {
val messageHelper = MessageHelper()
val message1Timestamp = messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(alice to message1Timestamp, alice to message2Timestamp)
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
@@ -75,16 +51,14 @@ class SyncMessageProcessorTest_readSyncs {
@Test
fun handleSynchronizeReadMessageMissingTimestamp() {
val messageHelper = MessageHelper()
messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(alice to message2Timestamp)
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
@@ -92,21 +66,19 @@ class SyncMessageProcessorTest_readSyncs {
@Test
fun handleSynchronizeReadWithEdits() {
val messageHelper = MessageHelper()
val message1Timestamp = messageHelper.incomingText().timestamp
messageHelper.syncReadMessage(alice to message1Timestamp)
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp
val message2Timestamp = messageHelper.incomingMedia().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(alice to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
@@ -114,112 +86,22 @@ class SyncMessageProcessorTest_readSyncs {
@Test
fun handleSynchronizeReadWithEditsInGroup() {
val messageHelper = MessageHelper()
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
val message1Timestamp = messageHelper.incomingText(sender = alice, destination = group.recipientId).timestamp
messageHelper.syncReadMessage(messageHelper.alice to message1Timestamp)
messageHelper.syncReadMessage(alice to message1Timestamp)
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = alice, destination = group.recipientId).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = alice, destination = group.recipientId).timestamp
val message2Timestamp = messageHelper.incomingMedia(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
val message2Timestamp = messageHelper.incomingMedia(sender = bob, destination = group.recipientId).timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(group.recipientId)!!
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(bob to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
messageHelper.syncReadMessage(messageHelper.bob to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
private inner class MessageHelper(var startTime: Long = System.currentTimeMillis()) {
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzStickerMediaMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.editTextMessage(
targetTimestamp = targetTimestamp,
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
).dataMessage!!
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
}
private data class MessageData(val serverGuid: UUID = UUID.randomUUID(), val timestamp: Long)
}

View File

@@ -0,0 +1,730 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.Matchers.greaterThan
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.MessageContentFuzzer.DeleteForMeSync
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assert
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNot
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsSize
import org.thoughtcrime.securesms.util.IdentityUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class SyncMessageProcessorTest_synchronizeDeleteForMe {
companion object {
private val TAG = "SyncDeleteForMeTest"
}
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var messageHelper: MessageHelper
@Before
fun setUp() {
messageHelper = MessageHelper(harness)
}
@After
fun tearDown() {
messageHelper.tearDown()
}
@Test
fun singleMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText().timestamp
messageHelper.incomingText()
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun singleOutgoingMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.outgoingText().timestamp
messageHelper.incomingText()
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, harness.self.id to message1Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun singleGroupMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 3
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
}
@Test
fun multipleGroupMessageDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp
messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId)
val message3Timestamp = messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 3
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.group.recipientId, messageHelper.alice to message1Timestamp, messageHelper.bob to message3Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun allMessagesDelete() {
// GIVEN
val message1Timestamp = messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to message1Timestamp, messageHelper.alice to message2Timestamp)
)
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 0
val threadRecord = SignalDatabase.threads.getThreadRecord(threadId)
threadRecord assertIs null
}
@Test
fun earlyMessagesDelete() {
// GIVEN
messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
// WHEN
val nextTextMessageTimestamp = messageHelper.nextStartTime(2)
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to nextTextMessageTimestamp)
)
messageHelper.incomingText()
// THEN
messageCount = SignalDatabase.messages.getMessageCountForThread(threadId)
messageCount assertIs 1
}
@Test
fun multipleConversationMessagesDelete() {
// GIVEN
messageHelper.incomingText(sender = messageHelper.alice)
val aliceMessage2 = messageHelper.incomingText(sender = messageHelper.alice).timestamp
messageHelper.incomingText(sender = messageHelper.bob)
val bobMessage2 = messageHelper.incomingText(sender = messageHelper.bob).timestamp
val aliceThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
aliceMessageCount assertIs 2
val bobThreadId = SignalDatabase.threads.getThreadIdFor(messageHelper.bob)!!
var bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
bobMessageCount assertIs 2
// WHEN
messageHelper.syncDeleteForMeMessage(
DeleteForMeSync(conversationId = messageHelper.alice, messageHelper.alice to aliceMessage2),
DeleteForMeSync(conversationId = messageHelper.bob, messageHelper.bob to bobMessage2)
)
// THEN
aliceMessageCount = SignalDatabase.messages.getMessageCountForThread(aliceThreadId)
aliceMessageCount assertIs 1
bobMessageCount = SignalDatabase.messages.getMessageCountForThread(bobThreadId)
bobMessageCount assertIs 1
}
@Test
fun singleConversationDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.alice,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = true
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
@Test
fun singleConversationNoRecentsFoundDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
val randomFutureMessages = (1..5).map {
messageHelper.alice to messageHelper.nextStartTime(it)
}
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, isFullDelete = true)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
harness.inMemoryLogger.flush()
harness.inMemoryLogger.entries().filter { it.message?.contains("Unable to find most recent received at timestamp") == true }.size assertIs 1
}
@Test
fun singleConversationNoRecentsFoundNonExpiringRecentsFoundDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
val nonExpiringMessages = messages.takeLast(5).map { it.recipientId to it.timetamp }
val randomFutureMessages = (1..5).map {
messageHelper.alice to messageHelper.nextStartTime(it)
}
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(conversationId = messageHelper.alice, randomFutureMessages, nonExpiringMessages, true)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
harness.inMemoryLogger.flush()
harness.inMemoryLogger.entries().filter { it.message?.contains("Using backup non-expiring messages") == true }.size assertIs 1
}
@Test
fun localOnlyRemainingAfterConversationDeleteWithFullDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
Log.v(TAG, "Adding normal messages")
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val alice = Recipient.resolved(messageHelper.alice)
Log.v(TAG, "Adding identity message")
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
Log.v(TAG, "Adding profile message")
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
Log.v(TAG, "Adding call message")
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
// WHEN
Log.v(TAG, "Processing sync message")
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.alice,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = true
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
@Test
fun localOnlyRemainingAfterConversationDeleteWithoutFullDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val alice = Recipient.resolved(messageHelper.alice)
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 23
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.alice,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = false
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 3
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
}
@Test
fun groupConversationDelete() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 50) {
messages += when (i % 3) {
1 -> MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText(sender = messageHelper.alice, destination = messageHelper.group.recipientId).timestamp)
2 -> MessageTable.SyncMessageId(messageHelper.bob, messageHelper.incomingText(sender = messageHelper.bob, destination = messageHelper.group.recipientId).timestamp)
else -> MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(messageHelper.group.recipientId).timestamp)
}
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(
conversationId = messageHelper.group.recipientId,
messages = messages.takeLast(5).map { it.recipientId to it.timetamp },
isFullDelete = true
)
)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
@Test
fun multipleConversationDelete() {
// GIVEN
val allMessages = mapOf<RecipientId, MutableList<MessageTable.SyncMessageId>>(
messageHelper.alice to mutableListOf(),
messageHelper.bob to mutableListOf()
)
allMessages.forEach { (conversation, messages) ->
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(conversation, messageHelper.incomingText(sender = conversation).timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText(conversationId = conversation).timestamp)
}
}
val threadIds = allMessages.keys.map { SignalDatabase.threads.getThreadIdFor(it)!! }
threadIds.forEach { SignalDatabase.messages.getMessageCountForThread(it) assertIs 20 }
// WHEN
messageHelper.syncDeleteForMeConversation(
DeleteForMeSync(conversationId = messageHelper.alice, allMessages[messageHelper.alice]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true),
DeleteForMeSync(conversationId = messageHelper.bob, allMessages[messageHelper.bob]!!.takeLast(5).map { it.recipientId to it.timetamp }, isFullDelete = true)
)
// THEN
threadIds.forEach {
SignalDatabase.messages.getMessageCountForThread(it) assertIs 0
SignalDatabase.threads.getThreadRecord(it) assertIs null
}
}
@Test
fun singleLocalOnlyConversation() {
// GIVEN
val alice = Recipient.resolved(messageHelper.alice)
// Insert placeholder message to prevent early thread update deletes
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
// Cleanup and confirm setup
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assert greaterThan(0)
// WHEN
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
// THEN
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
}
@Ignore("counts are consistent for some reason")
@Test
fun multipleLocalOnlyConversation() {
// GIVEN
val alice = Recipient.resolved(messageHelper.alice)
// Insert placeholder messages in group and alice thread to prevent early thread update deletes
val groupPlaceholderMessage = messageHelper.outgoingText(conversationId = messageHelper.group.recipientId).messageId
val oneToOnePlaceHolderMessage = messageHelper.outgoingText().messageId
val aliceThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.alice, isGroup = false)
val groupThreadId = SignalDatabase.threads.getOrCreateThreadIdFor(messageHelper.group.recipientId, isGroup = true)
// Identity changes
IdentityUtil.markIdentityVerified(harness.context, alice, true, true)
IdentityUtil.markIdentityVerified(harness.context, alice, false, true)
IdentityUtil.markIdentityVerified(harness.context, alice, true, false)
IdentityUtil.markIdentityVerified(harness.context, alice, false, false)
IdentityUtil.markIdentityUpdate(harness.context, alice.id)
// Calls
SignalDatabase.calls.insertOneToOneCall(1, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.OUTGOING, CallTable.Event.ACCEPTED)
SignalDatabase.calls.insertOneToOneCall(2, System.currentTimeMillis(), alice.id, CallTable.Type.VIDEO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED)
SignalDatabase.calls.insertOneToOneCall(3, System.currentTimeMillis(), alice.id, CallTable.Type.AUDIO_CALL, CallTable.Direction.INCOMING, CallTable.Event.MISSED_NOTIFICATION_PROFILE)
SignalDatabase.calls.insertAcceptedGroupCall(4, messageHelper.group.recipientId, CallTable.Direction.INCOMING, System.currentTimeMillis())
SignalDatabase.calls.insertDeclinedGroupCall(5, messageHelper.group.recipientId, System.currentTimeMillis())
// Detected changes
SignalDatabase.messages.insertProfileNameChangeMessages(alice, "new name", "previous name")
SignalDatabase.messages.insertLearnedProfileNameChangeMessage(alice, null, "username.42")
SignalDatabase.messages.insertNumberChangeMessages(alice.id)
SignalDatabase.messages.insertSmsExportMessage(alice.id, SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!)
SignalDatabase.messages.insertSessionSwitchoverEvent(alice.id, aliceThreadId, SessionSwitchoverEvent())
// Sent failed
SignalDatabase.messages.markAsSending(messageHelper.outgoingText().messageId)
SignalDatabase.messages.markAsSentFailed(messageHelper.outgoingText().messageId)
messageHelper.outgoingText().let {
SignalDatabase.messages.markAsSending(it.messageId)
SignalDatabase.messages.markAsRateLimited(it.messageId)
}
// Group change
messageHelper.outgoingGroupChange()
// Cleanup and confirm setup
SignalDatabase.messages.deleteMessage(messageId = oneToOnePlaceHolderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
SignalDatabase.messages.deleteMessage(messageId = groupPlaceholderMessage, threadId = aliceThreadId, notify = false, updateThread = false)
SignalDatabase.rawDatabase.withinTransaction {
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 16
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 10
}
// WHEN
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice, messageHelper.group.recipientId)
// THEN
SignalDatabase.messages.getMessageCountForThread(aliceThreadId) assertIs 0
SignalDatabase.threads.getThreadRecord(aliceThreadId) assertIs null
SignalDatabase.messages.getMessageCountForThread(groupThreadId) assertIs 0
SignalDatabase.threads.getThreadRecord(groupThreadId) assertIs null
}
@Test
fun singleLocalOnlyConversationHasAddressable() {
// GIVEN
val messages = mutableListOf<MessageTable.SyncMessageId>()
for (i in 0 until 10) {
messages += MessageTable.SyncMessageId(messageHelper.alice, messageHelper.incomingText().timestamp)
messages += MessageTable.SyncMessageId(harness.self.id, messageHelper.outgoingText().timestamp)
}
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
// WHEN
messageHelper.syncDeleteForMeLocalOnlyConversation(messageHelper.alice)
// THEN
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 20
SignalDatabase.threads.getThreadRecord(threadId).assertIsNotNull()
harness.inMemoryLogger.flush()
harness.inMemoryLogger.entries().filter { it.message?.contains("Thread is not local only") == true }.size assertIs 1
}
@Test
fun singleAttachmentDeletes() {
// GIVEN
val message1 = messageHelper.outgoingText { message ->
message.copy(
attachments = listOf(
messageHelper.outgoingAttachment(byteArrayOf(1, 2, 3)),
messageHelper.outgoingAttachment(byteArrayOf(2, 3, 4), null),
messageHelper.outgoingAttachment(byteArrayOf(5, 6, 7), null),
messageHelper.outgoingAttachment(byteArrayOf(10, 11, 12))
)
)
}
var attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
attachments assertIsSize 4
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
// Has all three
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[0].attachmentId,
uploadResult = attachments[0].toUploadResult(
digest = byteArrayOf(attachments[0].attachmentId.id.toByte()),
uploadTimestamp = message1.timestamp + 1
)
)
// Missing uuid and digest
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[1].attachmentId,
uploadResult = attachments[1].toUploadResult(uploadTimestamp = message1.timestamp + 1)
)
// Missing uuid and plain text
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[2].attachmentId,
uploadResult = attachments[2].toUploadResult(
digest = byteArrayOf(attachments[2].attachmentId.id.toByte()),
uploadTimestamp = message1.timestamp + 1
)
)
SignalDatabase.rawDatabase.update(AttachmentTable.TABLE_NAME).values(AttachmentTable.DATA_HASH_END to null).where("${AttachmentTable.ID} = ?", attachments[2].attachmentId).run()
// Different has all three
SignalDatabase.attachments.finalizeAttachmentAfterUpload(
id = attachments[3].attachmentId,
uploadResult = attachments[3].toUploadResult(
digest = byteArrayOf(attachments[3].attachmentId.id.toByte()),
uploadTimestamp = message1.timestamp + 1
)
)
attachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
// WHEN
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[0].uuid,
attachments[0].remoteDigest,
attachments[0].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
var updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 3
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[0].attachmentId }
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[1].uuid,
attachments[1].remoteDigest,
attachments[1].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 2
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[1].attachmentId }
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[2].uuid,
attachments[2].remoteDigest,
attachments[2].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 1
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 1
updatedAttachments.forEach { it.attachmentId assertIsNot attachments[2].attachmentId }
messageHelper.syncDeleteForMeAttachment(
conversationId = messageHelper.alice,
message = message1.author to message1.timestamp,
attachments[3].uuid,
attachments[3].remoteDigest,
attachments[3].dataHash
)
SignalDatabase.messages.getMessageCountForThread(threadId) assertIs 0
updatedAttachments = SignalDatabase.attachments.getAttachmentsForMessage(message1.messageId)
updatedAttachments assertIsSize 0
SignalDatabase.threads.getThreadRecord(threadId) assertIs null
}
private fun DatabaseAttachment.copy(
uuid: UUID? = this.uuid,
digest: ByteArray? = this.remoteDigest
): Attachment {
return DatabaseAttachment(
attachmentId = this.attachmentId,
mmsId = this.mmsId,
hasData = this.hasData,
hasThumbnail = false,
hasArchiveThumbnail = false,
contentType = this.contentType,
transferProgress = this.transferState,
size = this.size,
fileName = this.fileName,
cdn = this.cdn,
location = this.remoteLocation,
key = this.remoteKey,
iv = this.remoteIv,
digest = digest,
incrementalDigest = this.incrementalDigest,
incrementalMacChunkSize = this.incrementalMacChunkSize,
fastPreflightId = this.fastPreflightId,
voiceNote = this.voiceNote,
borderless = this.borderless,
videoGif = this.videoGif,
width = this.width,
height = this.height,
quote = this.quote,
caption = this.caption,
stickerLocator = this.stickerLocator,
blurHash = this.blurHash,
audioHash = this.audioHash,
transformProperties = this.transformProperties,
displayOrder = this.displayOrder,
uploadTimestamp = this.uploadTimestamp,
dataHash = this.dataHash,
archiveCdn = this.archiveCdn,
archiveMediaName = this.archiveMediaName,
archiveMediaId = this.archiveMediaId,
thumbnailRestoreState = this.thumbnailRestoreState,
archiveTransferState = this.archiveTransferState,
uuid = uuid
)
}
private fun Attachment.toUploadResult(
digest: ByteArray = this.remoteDigest ?: byteArrayOf(),
uploadTimestamp: Long = this.uploadTimestamp
): AttachmentUploadResult {
return AttachmentUploadResult(
remoteId = SignalServiceAttachmentRemoteId.V4(this.remoteLocation ?: "some-location"),
cdnNumber = this.cdn.cdnNumber,
key = this.remoteKey?.let { Base64.decode(it) } ?: Util.getSecretBytes(64),
iv = this.remoteIv ?: Util.getSecretBytes(16),
digest = digest,
incrementalDigest = this.incrementalDigest,
incrementalDigestChunkSize = this.incrementalMacChunkSize,
dataSize = this.size,
uploadTimestamp = uploadTimestamp
)
}
}

View File

@@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.migrations
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.count
import org.signal.core.util.readToSingleInt
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.assertIs
import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.util.Currency
@RunWith(AndroidJUnit4::class)
class SubscriberIdMigrationJobTest {
private val testSubject = SubscriberIdMigrationJob()
@Test
fun givenNoSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectNoDatabaseEntries() {
testSubject.run()
val actual = SignalDatabase.inAppPaymentSubscribers.readableDatabase.count()
.from(InAppPaymentSubscriberTable.TABLE_NAME)
.run()
.readToSingleInt()
actual assertIs 0
}
@Test
fun givenUSDSubscriber_whenIRunSubscriberIdMigrationJob_thenIExpectASingleEntry() {
val subscriberId = SubscriberId.generate()
SignalStore.inAppPayments.setSubscriberCurrency(Currency.getInstance("USD"), InAppPaymentSubscriberRecord.Type.DONATION)
SignalStore.inAppPayments.setSubscriber("USD", subscriberId)
SignalStore.inAppPayments.setSubscriptionPaymentSourceType(PaymentSourceType.PayPal)
SignalStore.inAppPayments.shouldCancelSubscriptionBeforeNextSubscribeAttempt = true
testSubject.run()
val actual = SignalDatabase.inAppPaymentSubscribers.getByCurrencyCode("USD", InAppPaymentSubscriberRecord.Type.DONATION)
actual.assertIsNotNull()
actual!!.subscriberId.bytes assertIs subscriberId.bytes
actual.paymentMethodType assertIs InAppPaymentData.PaymentMethodType.PAYPAL
actual.requiresCancel assertIs true
actual.currency assertIs Currency.getInstance("USD")
actual.type assertIs InAppPaymentSubscriberRecord.Type.DONATION
}
}

View File

@@ -90,10 +90,10 @@ class SafetyNumberBottomSheetRepositoryTest {
subjectUnderTest.removeFromStories(toRemove, listOf(destinationKey)).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(3)
testSubscriber.awaitCount(2)
// THEN
testSubscriber.assertValueAt(2) { map ->
testSubscriber.assertValueAt(1) { map ->
assertMatch(
map,
mapOf(
@@ -116,10 +116,10 @@ class SafetyNumberBottomSheetRepositoryTest {
subjectUnderTest.removeAllFromStory(distributionListMembers, distributionList).subscribe()
testSubscriber.request(1)
testScheduler.triggerActions()
testSubscriber.awaitCount(3)
testSubscriber.awaitCount(2)
// THEN
testSubscriber.assertValueAt(2) { map ->
testSubscriber.assertValueAt(1) { map ->
assertMatch(map, mapOf())
}
}

View File

@@ -24,9 +24,9 @@ class ContactRecordProcessorTest {
@Before
fun setup() {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
SignalStore.account.setE164(E164_SELF)
SignalStore.account.setAci(ACI_SELF)
SignalStore.account.setPni(PNI_SELF)
}
@Test

View File

@@ -3,8 +3,7 @@ package org.thoughtcrime.securesms.testing
import org.signal.core.util.logging.Log
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.messages.protocol.BufferedProtocolStore
import org.thoughtcrime.securesms.recipients.Recipient
@@ -30,14 +29,14 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
uuid = serviceId.rawUuid,
e164 = e164,
deviceId = 1,
identityKey = SignalStore.account().aciIdentityKey.publicKey.publicKey,
identityKey = SignalStore.account.aciIdentityKey.publicKey.publicKey,
expires = 31337
)
fun process(envelope: Envelope, serverDeliveredTimestamp: Long) {
val start = System.currentTimeMillis()
val bufferedStore = BufferedProtocolStore.create()
ApplicationDependencies.getIncomingMessageObserver()
AppDependencies.incomingMessageObserver
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
?.mapNotNull { it.run() }
?.forEach { it.enqueue() }
@@ -48,9 +47,9 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
}
fun encrypt(now: Long, destination: Recipient): Envelope {
return ApplicationDependencies.getSignalServiceMessageSender().getEncryptedMessage(
return AppDependencies.signalServiceMessageSender.getEncryptedMessage(
SignalServiceAddress(destination.requireServiceId(), destination.requireE164()),
FakeClientHelpers.getTargetUnidentifiedAccess(ProfileKeyUtil.getSelfProfileKey(), ProfileKey(destination.profileKey), aliceSenderCertificate),
FakeClientHelpers.getSealedSenderAccess(ProfileKey(destination.profileKey), aliceSenderCertificate),
1,
FakeClientHelpers.encryptedTextMessage(now),
false

View File

@@ -17,7 +17,7 @@ import org.signal.libsignal.protocol.state.SignedPreKeyRecord
import org.signal.libsignal.protocol.util.KeyHelper
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil
import org.thoughtcrime.securesms.crypto.SealedSenderAccessUtil
import org.thoughtcrime.securesms.database.OneTimePreKeyTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SignedPreKeyTable
@@ -25,14 +25,13 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.FakeClientHelpers.toEnvelope
import org.whispersystems.signalservice.api.SignalServiceAccountDataStore
import org.whispersystems.signalservice.api.SignalSessionLock
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher
import org.whispersystems.signalservice.api.crypto.SignalSessionBuilder
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.push.DistributionId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.push.Envelope
import java.util.Optional
import java.util.UUID
import java.util.concurrent.locks.ReentrantLock
@@ -75,12 +74,12 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
}
fun decrypt(envelope: Envelope, serverDeliveredTimestamp: Long) {
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, UnidentifiedAccessUtil.getCertificateValidator())
val cipher = SignalServiceCipher(serviceAddress, 1, aciStore, sessionLock, SealedSenderAccessUtil.getCertificateValidator())
cipher.decrypt(envelope, serverDeliveredTimestamp)
}
private fun getAliceServiceId(): ServiceId {
return SignalStore.account().requireAci()
return SignalStore.account.requireAci()
}
private fun getAlicePreKeyBundle(): PreKeyBundle {
@@ -103,7 +102,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
val selfSignedPreKeyRecord = SignalDatabase.signedPreKeys.get(getAliceServiceId(), selfSignedPreKeyId)!!
return PreKeyBundle(
SignalStore.account().registrationId,
SignalStore.account.registrationId,
1,
selfPreKeyId,
selfPreKeyRecord.keyPair.publicKey,
@@ -115,19 +114,19 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
}
private fun getAliceProtocolAddress(): SignalProtocolAddress {
return SignalProtocolAddress(SignalStore.account().requireAci().toString(), 1)
return SignalProtocolAddress(SignalStore.account.requireAci().toString(), 1)
}
private fun getAlicePublicKey(): IdentityKey {
return SignalStore.account().aciIdentityKey.publicKey
return SignalStore.account.aciIdentityKey.publicKey
}
private fun getAliceProfileKey(): ProfileKey {
return ProfileKeyUtil.getSelfProfileKey()
}
private fun getAliceUnidentifiedAccess(): Optional<UnidentifiedAccess> {
return FakeClientHelpers.getTargetUnidentifiedAccess(profileKey, getAliceProfileKey(), senderCertificate)
private fun getAliceUnidentifiedAccess(): SealedSenderAccess? {
return FakeClientHelpers.getSealedSenderAccess(getAliceProfileKey(), senderCertificate)
}
private class BobSignalServiceAccountDataStore(private val registrationId: Int, private val identityKeyPair: IdentityKeyPair) : SignalServiceAccountDataStore {
@@ -139,10 +138,12 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean = true
override fun loadSession(address: SignalProtocolAddress?): SessionRecord = aliceSessionRecord ?: SessionRecord()
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): Boolean = false
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) { aliceSessionRecord = record }
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) {
aliceSessionRecord = record
}
override fun getSubDeviceSessions(name: String?): List<Int> = emptyList()
override fun containsSession(address: SignalProtocolAddress?): Boolean = aliceSessionRecord != null
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account().aciIdentityKey.publicKey
override fun getIdentity(address: SignalProtocolAddress?): IdentityKey = SignalStore.account.aciIdentityKey.publicKey
override fun loadPreKey(preKeyId: Int): PreKeyRecord = throw UnsupportedOperationException()
override fun storePreKey(preKeyId: Int, record: PreKeyRecord?) = throw UnsupportedOperationException()
override fun containsPreKey(preKeyId: Int): Boolean = throw UnsupportedOperationException()

View File

@@ -14,8 +14,8 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.whispersystems.signalservice.api.crypto.ContentHint
import org.whispersystems.signalservice.api.crypto.EnvelopeContent
import org.whispersystems.signalservice.api.crypto.SealedSenderAccess
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccess
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
@@ -46,11 +46,10 @@ object FakeClientHelpers {
}
}
fun getTargetUnidentifiedAccess(myProfileKey: ProfileKey, theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): Optional<UnidentifiedAccess> {
val selfUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(myProfileKey)
val themUnidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey)
fun getSealedSenderAccess(theirProfileKey: ProfileKey, senderCertificate: SenderCertificate): SealedSenderAccess? {
val themUnidentifiedAccessKey = UnidentifiedAccess(UnidentifiedAccess.deriveAccessKeyFrom(theirProfileKey), senderCertificate.serialized, false)
return UnidentifiedAccessPair(UnidentifiedAccess(selfUnidentifiedAccessKey, senderCertificate.serialized, false), UnidentifiedAccess(themUnidentifiedAccessKey, senderCertificate.serialized, false)).targetUnidentifiedAccess
return SealedSenderAccess.forIndividual(themUnidentifiedAccessKey)
}
fun encryptedTextMessage(now: Long, message: String = "Test body message"): EnvelopeContent {

View File

@@ -33,7 +33,7 @@ object GroupTestingUtils {
.title(MessageContentFuzzer.string())
.build()
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState)!!
val groupId = SignalDatabase.groups.create(groupMasterKey, decryptedGroupState, null)!!
val groupRecipientId = SignalDatabase.recipients.getOrInsertFromGroupId(groupId)
SignalDatabase.recipients.setProfileSharing(groupRecipientId, true)

View File

@@ -2,12 +2,15 @@ package org.thoughtcrime.securesms.testing
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.buildWith
import org.thoughtcrime.securesms.messages.TestMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.internal.push.AttachmentPointer
import org.whispersystems.signalservice.internal.push.BodyRange
import org.whispersystems.signalservice.internal.push.Content
@@ -61,13 +64,13 @@ object MessageContentFuzzer {
* - An expire timer value
* - Bold style body ranges
*/
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null, allowExpireTimeChanges: Boolean = true): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
timestamp = sentTimestamp
body = string()
if (random.nextBoolean()) {
if (allowExpireTimeChanges && random.nextBoolean()) {
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
}
if (random.nextBoolean()) {
@@ -150,6 +153,121 @@ object MessageContentFuzzer {
).build()
}
fun syncDeleteForMeMessage(allDeletes: List<DeleteForMeSync>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
messageDeletes = allDeletes.map { (conversationId, conversationDeletes) ->
val conversation = Recipient.resolved(conversationId)
SyncMessage.DeleteForMe.MessageDeletes(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
},
messages = conversationDeletes.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
}
)
}
)
)
).build()
}
fun syncDeleteForMeConversation(allDeletes: List<DeleteForMeSync>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
conversationDeletes = allDeletes.map { delete ->
val conversation = Recipient.resolved(delete.conversationId)
SyncMessage.DeleteForMe.ConversationDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
},
mostRecentMessages = delete.messages.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
},
mostRecentNonExpiringMessages = delete.nonExpiringMessages.map { (author, timestamp) ->
SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(author).requireAci().toString(),
sentTimestamp = timestamp
)
},
isFullDelete = delete.isFullDelete
)
}
)
)
).build()
}
fun syncDeleteForMeLocalOnlyConversation(conversations: List<RecipientId>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
localOnlyConversationDeletes = conversations.map { conversationId ->
val conversation = Recipient.resolved(conversationId)
SyncMessage.DeleteForMe.LocalOnlyConversationDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
}
)
}
)
)
).build()
}
fun syncDeleteForMeAttachment(conversationId: RecipientId, message: Pair<RecipientId, Long>, uuid: UUID?, digest: ByteArray?, plainTextHash: String?): Content {
val conversation = Recipient.resolved(conversationId)
return Content
.Builder()
.syncMessage(
SyncMessage(
deleteForMe = SyncMessage.DeleteForMe(
attachmentDeletes = listOf(
SyncMessage.DeleteForMe.AttachmentDelete(
conversation = if (conversation.isGroup) {
SyncMessage.DeleteForMe.ConversationIdentifier(threadGroupId = conversation.requireGroupId().decodedId.toByteString())
} else {
SyncMessage.DeleteForMe.ConversationIdentifier(threadServiceId = conversation.requireAci().toString())
},
targetMessage = SyncMessage.DeleteForMe.AddressableMessage(
authorServiceId = Recipient.resolved(message.first).requireAci().toString(),
sentTimestamp = message.second
),
uuid = uuid?.let { UuidUtil.toByteString(it) },
fallbackDigest = digest?.toByteString(),
fallbackPlaintextHash = plainTextHash?.let { Base64.decodeOrNull(it)?.toByteString() }
)
)
)
)
).build()
}
/**
* Create a random media message that may be:
* - A text body
@@ -278,7 +396,7 @@ object MessageContentFuzzer {
caption = string(allowNullString = true)
blurHash = string()
uploadTimestamp = random.nextLong()
cdnNumber = 1
cdnNumber = 2
build()
}
@@ -290,4 +408,14 @@ object MessageContentFuzzer {
fun fuzzServerDeliveredTimestamp(envelopeTimestamp: Long): Long {
return envelopeTimestamp + 10
}
data class DeleteForMeSync(
val conversationId: RecipientId,
val messages: List<Pair<RecipientId, Long>>,
val nonExpiringMessages: List<Pair<RecipientId, Long>> = emptyList(),
val isFullDelete: Boolean = true,
val attachments: List<Pair<Long, AttachmentTable.SyncAttachmentId>> = emptyList()
) {
constructor(conversationId: RecipientId, vararg messages: Pair<RecipientId, Long>) : this(conversationId, messages.toList())
}
}

View File

@@ -31,7 +31,7 @@ object MockProvider {
val lockedFailure = PushServiceSocket.RegistrationLockFailure().apply {
svr1Credentials = AuthCredentials.create("username", "password")
svr2Credentials = null
svr2Credentials = AuthCredentials.create("username", "password")
}
val primaryOnlyDeviceList = DeviceInfoList().apply {
@@ -68,7 +68,7 @@ object MockProvider {
}
}
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account().aciIdentityKey, deviceId: Int): PreKeyResponse {
fun createPreKeyResponse(identity: IdentityKeyPair = SignalStore.account.aciIdentityKey, deviceId: Int): PreKeyResponse {
val signedPreKeyRecord = PreKeyUtil.generateSignedPreKey(SecureRandom().nextInt(Medium.MAX_VALUE), identity.privateKey)
val oneTimePreKey = PreKeyRecord(SecureRandom().nextInt(Medium.MAX_VALUE), Curve.generateKeyPair())

View File

@@ -55,5 +55,5 @@ inline fun <reified T> RecordedRequest.parsedRequestBody(): T {
}
private fun defaultRequestPredicate(verb: String, path: String, predicate: RequestPredicate = { true }): RequestPredicate = { request ->
request.method == verb && request.path.startsWith("/$path") && predicate(request)
request.method == verb && request.path?.startsWith("/$path") == true && predicate(request)
}

View File

@@ -8,6 +8,7 @@ import android.content.SharedPreferences
import android.preference.PreferenceManager
import androidx.test.core.app.ActivityScenario
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockResponse
import org.junit.rules.ExternalResource
import org.signal.libsignal.protocol.IdentityKey
@@ -19,24 +20,21 @@ import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.RegistrationRepository
import org.thoughtcrime.securesms.registration.RegistrationUtil
import org.thoughtcrime.securesms.registration.VerifyResponse
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil
import org.thoughtcrime.securesms.registration.data.RegistrationData
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.util.UUID
/**
@@ -47,7 +45,8 @@ import java.util.UUID
*/
class SignalActivityRule(private val othersCount: Int = 4, private val createGroup: Boolean = false) : ExternalResource() {
val application: Application = ApplicationDependencies.getApplication()
val application: Application = AppDependencies.application
private val TEST_E164 = "+15555550101"
lateinit var context: Context
private set
@@ -90,40 +89,40 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
preferences.edit().putBoolean("passphrase_initialized", true).commit()
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
val registrationRepository = RegistrationRepository(application)
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(Put("/v2/keys") { MockResponse().success() })
val response: ServiceResponse<VerifyResponse> = registrationRepository.registerAccount(
RegistrationData(
runBlocking {
val registrationData = RegistrationData(
code = "123123",
e164 = "+15555550101",
e164 = TEST_E164,
password = Util.getSecret(18),
registrationId = registrationRepository.registrationId,
profileKey = registrationRepository.getProfileKey("+15555550101"),
registrationId = RegistrationRepository.getRegistrationId(),
profileKey = RegistrationRepository.getProfileKey(TEST_E164),
fcmToken = null,
pniRegistrationId = registrationRepository.pniRegistrationId,
pniRegistrationId = RegistrationRepository.getPniRegistrationId(),
recoveryPassword = "asdfasdfasdfasdf"
),
VerifyResponse(
verifyAccountResponse = VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
)
val remoteResult = RegistrationRepository.AccountRegistrationResult(
uuid = UUID.randomUUID().toString(),
pni = UUID.randomUUID().toString(),
storageCapable = false,
number = TEST_E164,
masterKey = null,
pin = null,
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
),
false
).blockingGet()
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
)
val localRegistrationData = LocalRegistrationMetadataUtil.createLocalRegistrationMetadata(SignalStore.account.aciIdentityKey, SignalStore.account.pniIdentityKey, registrationData, remoteResult, false)
RegistrationRepository.registerAccountLocally(application, localRegistrationData)
}
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
SignalStore.svr().optOut()
SignalStore.svr.optOut()
RegistrationUtil.maybeMarkRegistrationComplete()
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
SignalStore.settings().isMessageNotificationsEnabled = false
SignalStore.settings.isMessageNotificationsEnabled = false
return Recipient.self()
}
@@ -141,11 +140,11 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, false, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
others += recipientId
othersKeys += otherIdentity
}
@@ -158,14 +157,14 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
}
fun changeIdentityKey(recipient: Recipient, identityKey: IdentityKey = IdentityKeyUtil.generateIdentityKeyPair().publicKey) {
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0), identityKey)
}
fun getIdentity(recipient: Recipient): IdentityKey {
return ApplicationDependencies.getProtocolStore().aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
return AppDependencies.protocolStore.aci().identities().getIdentity(SignalProtocolAddress(recipient.requireServiceId().toString(), 0))
}
fun setVerified(recipient: Recipient, status: IdentityTable.VerifiedStatus) {
ApplicationDependencies.getProtocolStore().aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
AppDependencies.protocolStore.aci().identities().setVerified(recipient.id, getIdentity(recipient), IdentityTable.VerifiedStatus.VERIFIED)
}
}

View File

@@ -26,8 +26,8 @@ class SignalDatabaseRule(
override fun starting(description: Description?) {
deleteAllThreads()
SignalStore.account().setAci(localAci)
SignalStore.account().setPni(localPni)
SignalStore.account.setAci(localAci)
SignalStore.account.setPni(localPni)
}
override fun finished(description: Description?) {

View File

@@ -0,0 +1,9 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.testing
@Retention(AnnotationRetention.RUNTIME)
annotation class SignalFlakyTest(val allowedAttempts: Int = 3)

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.testing
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import org.signal.core.util.logging.Log
/**
* A JUnit rule that retries tests annotated with [SignalFlakyTest] before considering them to be a failure.
* As the name implies, this is useful for known-flaky tests.
*/
class SignalFlakyTestRule : TestRule {
override fun apply(base: Statement, description: Description): Statement {
val flakyAnnotation = description.getAnnotation(SignalFlakyTest::class.java)
return if (flakyAnnotation != null) {
FlakyStatement(
base = base,
description = description,
allowedAttempts = flakyAnnotation.allowedAttempts
)
} else {
base
}
}
private class FlakyStatement(private val base: Statement, private val description: Description, private val allowedAttempts: Int) : Statement() {
override fun evaluate() {
var attemptsRemaining = allowedAttempts
while (attemptsRemaining > 0) {
try {
base.evaluate()
return
} catch (t: Throwable) {
attemptsRemaining--
if (attemptsRemaining <= 0) {
throw t
}
Log.w(description.testClass.simpleName, "[${description.methodName}] Flaky test failed! $attemptsRemaining attempt(s) remaining.", t)
}
}
}
}
}

View File

@@ -1,13 +1,14 @@
package org.thoughtcrime.securesms.testing
import android.database.Cursor
import android.util.Base64
import org.hamcrest.Matcher
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.hasSize
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.not
import org.hamcrest.Matchers.notNullValue
import org.hamcrest.Matchers.nullValue
import org.signal.core.util.Hex
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.select
@@ -56,33 +57,41 @@ infix fun <E, T : Collection<E>> T.assertIsSize(expected: Int) {
assertThat(this, hasSize(expected))
}
infix fun <T : Any> T.assert(matcher: Matcher<T>) {
assertThat(this, matcher)
}
fun CountDownLatch.awaitFor(duration: Duration) {
if (!await(duration.inWholeMilliseconds, TimeUnit.MILLISECONDS)) {
throw TimeoutException("Latch await took longer than ${duration.inWholeMilliseconds}ms")
}
}
fun dumpTableToLogs(tag: String = "TestUtils", table: String) {
dumpTable(table).forEach { Log.d(tag, it.toString()) }
fun dumpTableToLogs(tag: String = "TestUtils", table: String, columns: Set<String>? = null) {
dumpTable(table, columns).forEach { Log.d(tag, it.toString()) }
}
fun dumpTable(table: String): List<List<Pair<String, String?>>> {
fun dumpTable(table: String, columns: Set<String>?): List<List<Pair<String, String?>>> {
return SignalDatabase.rawDatabase
.select()
.from(table)
.run()
.readToList { cursor ->
val map: List<Pair<String, String?>> = cursor.columnNames.map { column ->
val index = cursor.getColumnIndex(column)
var data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
}
val map: List<Pair<String, String?>> = cursor.columnNames.mapNotNull { column ->
if (columns == null || columns.contains(column)) {
val index = cursor.getColumnIndex(column)
var data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Hex.toStringCondensed(cursor.getBlob(index))
else -> cursor.getString(index)
}
if (table == MessageTable.TABLE_NAME && column == MessageTable.TYPE) {
data = MessageTableTestUtils.typeColumnToString(cursor.getLong(index))
}
column to data
column to data
} else {
null
}
}
map
}

View File

@@ -1,11 +0,0 @@
package org.thoughtcrime.securesms.util;
/**
* A class that allows us to inject feature flags during tests.
*/
public final class FeatureFlagsAccessor {
public static void forceValue(String key, Object value) {
FeatureFlags.FORCED_VALUES.put(key, value);
}
}

View File

@@ -61,7 +61,7 @@ object MessageTableTestUtils {
isProfileChange:${type == MessageTypes.PROFILE_CHANGE_TYPE}
isGroupV1MigrationEvent:${type == MessageTypes.GV1_MIGRATION_TYPE}
isChangeNumber:${type == MessageTypes.CHANGE_NUMBER_TYPE}
isBoostRequest:${type == MessageTypes.BOOST_REQUEST_TYPE}
isDonationChannelDonationRequest:${type == MessageTypes.RELEASE_CHANNEL_DONATION_REQUEST_TYPE}
isThreadMerge:${type == MessageTypes.THREAD_MERGE_TYPE}
isSmsExport:${type == MessageTypes.SMS_EXPORT_TYPE}
isGroupV2LeaveOnly:${type and MessageTypes.GROUP_V2_LEAVE_BITS == MessageTypes.GROUP_V2_LEAVE_BITS}

View File

@@ -1,40 +0,0 @@
package org.signal.benchmark
import android.content.Context
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.util.FeatureFlags
import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.account.PreKeyUpload
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import java.io.IOException
import java.util.Optional
class DummyAccountManagerFactory : AccountManagerFactory() {
override fun createAuthenticated(context: Context, aci: ACI, pni: PNI, number: String, deviceId: Int, password: String): SignalServiceAccountManager {
return DummyAccountManager(
ApplicationDependencies.getSignalServiceNetworkAccess().getConfiguration(number),
aci,
pni,
number,
deviceId,
password,
BuildConfig.SIGNAL_AGENT,
FeatureFlags.okHttpAutomaticRetry(),
FeatureFlags.groupLimits().hardLimit
)
}
private class DummyAccountManager(configuration: SignalServiceConfiguration?, aci: ACI?, pni: PNI?, e164: String?, deviceId: Int, password: String?, signalAgent: String?, automaticNetworkRetry: Boolean, maxGroupSize: Int) : SignalServiceAccountManager(configuration, aci, pni, e164, deviceId, password, signalAgent, automaticNetworkRetry, maxGroupSize) {
@Throws(IOException::class)
override fun setGcmId(gcmRegistrationId: Optional<String>) {
}
@Throws(IOException::class)
override fun setPreKeys(preKeyUpload: PreKeyUpload) {
}
}
}

View File

@@ -1,5 +1,6 @@
package org.signal.benchmark.setup
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.MessageType
@@ -9,7 +10,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
@@ -144,7 +144,7 @@ object TestMessages {
}
private fun imageAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
Cdn.S3.cdnNumber,
SignalServiceAttachmentRemoteId.from(""),
"image/webp",
null,
@@ -161,13 +161,14 @@ object TestMessages {
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
System.currentTimeMillis(),
null
)
}
private fun voiceAttachment(): SignalServiceAttachmentPointer {
return SignalServiceAttachmentPointer(
ReleaseChannel.CDN_NUMBER,
Cdn.S3.cdnNumber,
SignalServiceAttachmentRemoteId.from(""),
"audio/aac",
null,
@@ -184,7 +185,8 @@ object TestMessages {
false,
Optional.empty(),
Optional.empty(),
System.currentTimeMillis()
System.currentTimeMillis(),
null
)
}

View File

@@ -3,39 +3,35 @@ package org.signal.benchmark.setup
import android.app.Application
import android.content.SharedPreferences
import android.preference.PreferenceManager
import org.signal.benchmark.DummyAccountManagerFactory
import org.signal.core.util.concurrent.safeBlockingGet
import kotlinx.coroutines.runBlocking
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
import org.thoughtcrime.securesms.crypto.MasterSecretUtil
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.push.AccountManagerFactory
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.registration.RegistrationData
import org.thoughtcrime.securesms.registration.RegistrationRepository
import org.thoughtcrime.securesms.registration.RegistrationUtil
import org.thoughtcrime.securesms.registration.VerifyResponse
import org.thoughtcrime.securesms.registration.data.LocalRegistrationMetadataUtil
import org.thoughtcrime.securesms.registration.data.RegistrationData
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.util.RegistrationUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.internal.ServiceResponse
import org.whispersystems.signalservice.internal.ServiceResponseProcessor
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import java.util.UUID
object TestUsers {
private var generatedOthers: Int = 0
private val TEST_E164 = "+15555550101"
fun setupSelf(): Recipient {
val application: Application = ApplicationDependencies.getApplication()
val application: Application = AppDependencies.application
DeviceTransferBlockingInterceptor.getInstance().blockNetwork()
PreferenceManager.getDefaultSharedPreferences(application).edit().putBoolean("pref_prompted_push_registration", true).commit()
@@ -44,40 +40,35 @@ object TestUsers {
val preferences: SharedPreferences = application.getSharedPreferences(MasterSecretUtil.PREFERENCES_NAME, 0)
preferences.edit().putBoolean("passphrase_initialized", true).commit()
SignalStore.account().generateAciIdentityKeyIfNecessary()
SignalStore.account().generatePniIdentityKeyIfNecessary()
SignalStore.account.generateAciIdentityKeyIfNecessary()
SignalStore.account.generatePniIdentityKeyIfNecessary()
val registrationRepository = RegistrationRepository(application)
val registrationData = RegistrationData(
code = "123123",
e164 = "+15555550101",
password = Util.getSecret(18),
registrationId = registrationRepository.registrationId,
profileKey = registrationRepository.getProfileKey("+15555550101"),
fcmToken = "fcm-token",
pniRegistrationId = registrationRepository.pniRegistrationId,
recoveryPassword = "asdfasdfasdfasdf"
)
runBlocking {
val registrationData = RegistrationData(
code = "123123",
e164 = TEST_E164,
password = Util.getSecret(18),
registrationId = RegistrationRepository.getRegistrationId(),
profileKey = RegistrationRepository.getProfileKey(TEST_E164),
fcmToken = null,
pniRegistrationId = RegistrationRepository.getPniRegistrationId(),
recoveryPassword = "asdfasdfasdfasdf"
)
val remoteResult = RegistrationRepository.AccountRegistrationResult(
uuid = UUID.randomUUID().toString(),
pni = UUID.randomUUID().toString(),
storageCapable = false,
number = TEST_E164,
masterKey = null,
pin = null,
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account.aciIdentityKey, SignalStore.account.pniPreKeys)
)
val localRegistrationData = LocalRegistrationMetadataUtil.createLocalRegistrationMetadata(SignalStore.account.aciIdentityKey, SignalStore.account.pniIdentityKey, registrationData, remoteResult, false)
RegistrationRepository.registerAccountLocally(application, localRegistrationData)
}
val verifyResponse = VerifyResponse(
VerifyAccountResponse(UUID.randomUUID().toString(), UUID.randomUUID().toString(), false),
masterKey = null,
pin = null,
aciPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().aciPreKeys),
pniPreKeyCollection = RegistrationRepository.generateSignedAndLastResortPreKeys(SignalStore.account().aciIdentityKey, SignalStore.account().pniPreKeys)
)
AccountManagerFactory.setInstance(DummyAccountManagerFactory())
val response: ServiceResponse<VerifyResponse> = registrationRepository.registerAccount(
registrationData,
verifyResponse,
false
).safeBlockingGet()
ServiceResponseProcessor.DefaultProcessor(response).resultOrThrow
SignalStore.svr().optOut()
SignalStore.svr.optOut()
RegistrationUtil.maybeMarkRegistrationComplete()
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
@@ -104,7 +95,7 @@ object TestUsers {
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
ApplicationDependencies.getProtocolStore().aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
AppDependencies.protocolStore.aci().saveIdentity(SignalProtocolAddress(aci.toString(), 0), otherIdentity.publicKey)
others += recipientId
}

View File

@@ -12,7 +12,7 @@ import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
@@ -96,6 +96,7 @@ class ConversationElementGenerator {
0,
0,
0,
0,
false,
true,
null,
@@ -123,7 +124,7 @@ class ConversationElementGenerator {
)
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
ApplicationDependencies.getApplication(),
AppDependencies.application,
record,
Recipient.UNKNOWN
)

View File

@@ -67,7 +67,8 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
hasWallpaper = springboardViewModel.hasWallpaper.value,
colorizer = Colorizer(),
startExpirationTimeout = {},
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) }
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) },
displayDialogFragment = {}
)
if (springboardViewModel.hasWallpaper.value) {
@@ -230,6 +231,10 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onChangeProfileNameUpdateContact(recipient: Recipient) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onCallToAction(action: String) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
@@ -279,7 +284,7 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
override fun onEditedIndicatorClicked(conversationMessage: ConversationMessage) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
@@ -299,6 +304,14 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onItemDoubleClick(item: MultiselectPart) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onPaymentTombstoneClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onShowSafetyTips(forGroup: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}

View File

@@ -83,6 +83,15 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
<!-- For services -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<!-- For vestigial telecom integration service -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL" />
<application android:name=".ApplicationContext"
android:icon="@mipmap/ic_launcher"
@@ -115,19 +124,32 @@
<meta-data android:name="firebase_analytics_collection_deactivated" android:value="true" />
<meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
<meta-data android:name="firebase_messaging_auto_init_enabled" android:value="false" />
<meta-data android:name="android.webkit.WebView.MetricsOptOut" android:value="true" />
<activity android:name=".WebRtcCallActivity"
android:theme="@style/TextSecure.DarkTheme.WebRTCCall"
android:excludeFromRecents="true"
android:supportsPictureInPicture="true"
android:windowSoftInputMode="stateAlwaysHidden"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:configChanges="screenSize|smallestScreenSize|screenLayout"
android:taskAffinity=".calling"
android:resizeableActivity="true"
android:launchMode="singleTask"
android:exported="false" />
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
<activity
android:name=".components.webrtc.v2.CallActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:supportsPictureInPicture="true"
android:taskAffinity=".calling"
android:theme="@style/TextSecure.DarkTheme.WebRTCCall"
android:windowSoftInputMode="stateAlwaysHidden" />
<activity android:name=".messagerequests.CalleeMustAcceptMessageRequestActivity"
android:theme="@style/TextSecure.DarkNoActionBar"
android:noHistory="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -610,7 +632,7 @@
<activity android:name=".conversation.v2.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:parentActivityName=".MainActivity"
android:resizeableActivity="true"
android:exported="false">
@@ -661,7 +683,7 @@
<activity android:name=".PassphrasePromptActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightIntroTheme"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
@@ -748,13 +770,6 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".backup.v2.ui.MessageBackupsFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".stories.settings.StorySettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -797,13 +812,6 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".badges.gifts.flow.GiftFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|screenLayout|screenSize"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".wallpaper.ChatWallpaperActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -830,7 +838,14 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".registration.RegistrationNavigationActivity"
<activity android:name=".registration.ui.RegistrationActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".restore.RestoreActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
@@ -933,12 +948,18 @@
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.call" />
</intent-filter>
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="vnd.android.cursor.item/vnd.org.thoughtcrime.securesms.videocall" />
</intent-filter>
</activity>
<activity android:name=".mediasend.AvatarSelectionActivity"
@@ -962,8 +983,8 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".backup.v2.ui.MessageBackupsTestRestoreActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
<activity android:name=".registration.ui.restore.RemoteRestoreActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:exported="false"/>
<activity android:name=".profiles.manage.EditProfileActivity"
@@ -1079,7 +1100,7 @@
android:theme="@style/Theme.Signal.WallpaperCropper"
android:exported="false"/>
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrImageSelectionActivity"
<activity android:name=".components.settings.app.usernamelinks.main.QrImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar"
android:exported="false"/>
@@ -1094,25 +1115,34 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".components.settings.app.subscription.donate.CheckoutFlowActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".backup.v2.ui.subscription.MessageBackupsCheckoutActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<service
android:enabled="true"
android:name=".service.webrtc.WebRtcCallService"
android:foregroundServiceType="camera|microphone"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
android:enabled="true"
android:exported="false"
android:name=".service.KeyCachingService" />
android:name=".service.KeyCachingService"
android:foregroundServiceType="remoteMessaging"/>
<service
android:enabled="true"
android:name=".messages.IncomingMessageObserver$ForegroundService"
android:foregroundServiceType="remoteMessaging"
android:exported="false"/>
<service
@@ -1123,6 +1153,7 @@
<service
android:name=".service.webrtc.AndroidCallConnectionService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
android:foregroundServiceType="phoneCall"
android:exported="true">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
@@ -1162,10 +1193,17 @@
<service
android:name=".service.GenericForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
android:name=".service.AttachmentProgressService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
android:name=".service.BackupProgressService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
@@ -1174,6 +1212,7 @@
<service
android:name=".gcm.FcmFetchForegroundService"
android:foregroundServiceType="remoteMessaging"
android:exported="false"/>
<service android:name=".gcm.FcmReceiveService" android:exported="true">
@@ -1288,6 +1327,12 @@
</intent-filter>
</receiver>
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
@@ -1352,7 +1397,11 @@
</intent-filter>
</receiver>
<service android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService" android:exported="false" />
<service
android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync|microphone" />
<receiver android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallServiceReceiver" android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.DENY"/>

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,38 +0,0 @@
package androidx.documentfile.provider;
import android.content.Context;
import android.net.Uri;
import android.provider.DocumentsContract;
import org.signal.core.util.logging.Log;
/**
* Located in androidx package as {@link TreeDocumentFile} is package protected.
*/
public class DocumentFileHelper {
private static final String TAG = Log.tag(DocumentFileHelper.class);
/**
* System implementation swallows the exception and we are having problems with the rename. This inlines the
* same call and logs the exception. Note this implementation does not update the passed in document file like
* the system implementation. Do not use the provided document file after calling this method.
*
* @return true if rename successful
*/
public static boolean renameTo(Context context, DocumentFile documentFile, String displayName) {
if (documentFile instanceof TreeDocumentFile) {
Log.d(TAG, "Renaming document directly");
try {
final Uri result = DocumentsContract.renameDocument(context.getContentResolver(), documentFile.getUri(), displayName);
return result != null;
} catch (Exception e) {
Log.w(TAG, "Unable to rename document file", e);
return false;
}
} else {
Log.d(TAG, "Letting OS rename document: " + documentFile.getClass().getSimpleName());
return documentFile.renameTo(displayName);
}
}
}

View File

@@ -11,13 +11,8 @@ object AppCapabilities {
fun getCapabilities(storageCapable: Boolean): AccountAttributes.Capabilities {
return AccountAttributes.Capabilities(
storage = storageCapable,
senderKey = true,
announcementGroup = true,
changeNumber = true,
stories = true,
giftBadges = true,
pni = true,
paymentActivation = true
deleteSync = true,
versionedExpirationTimer = true
)
}
}

View File

@@ -5,7 +5,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob;
@@ -33,33 +33,33 @@ public final class AppInitialization {
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
TextSecurePreferences.setPasswordDisabled(context, true);
SignalStore.settings().setPassphraseDisabled(true);
TextSecurePreferences.setReadReceiptsEnabled(context, true);
TextSecurePreferences.setTypingIndicatorsEnabled(context, true);
TextSecurePreferences.setHasSeenWelcomeScreen(context, false);
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
AppDependencies.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()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
public static void onPostBackupRestore(@NonNull Context context) {
Log.i(TAG, "onPostBackupRestore()");
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
AppDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.onPostBackupRestore();
SignalStore.onFirstEverAppLaunch();
SignalStore.onboarding().clearAll();
TextSecurePreferences.onPostBackupRestore(context);
TextSecurePreferences.setPasswordDisabled(context, true);
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
ApplicationDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
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()));
SignalStore.settings().setPassphraseDisabled(true);
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
EmojiSearchIndexDownloadJob.scheduleImmediately();
}
@@ -73,13 +73,13 @@ public final class AppInitialization {
TextSecurePreferences.setJobManagerVersion(context, JobManager.CURRENT_VERSION);
TextSecurePreferences.setLastVersionCode(context, Util.getCanonicalVersionCode());
TextSecurePreferences.setHasSeenStickerIntroTooltip(context, true);
TextSecurePreferences.setPasswordDisabled(context, true);
ApplicationDependencies.getMegaphoneRepository().onFirstEverAppLaunch();
SignalStore.settings().setPassphraseDisabled(true);
AppDependencies.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()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.ZOZO.getPackId(), BlessedPacks.ZOZO.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.BANDIT.getPackId(), BlessedPacks.BANDIT.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forInstall(BlessedPacks.DAY_BY_DAY.getPackId(), BlessedPacks.DAY_BY_DAY.getPackKey(), false));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_HANDS.getPackId(), BlessedPacks.SWOON_HANDS.getPackKey()));
AppDependencies.getJobManager().add(StickerPackDownloadJob.forReference(BlessedPacks.SWOON_FACES.getPackId(), BlessedPacks.SWOON_FACES.getPackKey()));
}
}

View File

@@ -16,13 +16,13 @@
*/
package org.thoughtcrime.securesms;
import android.app.Application;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.multidex.MultiDexApplication;
import com.bumptech.glide.Glide;
import com.google.android.gms.security.ProviderInstaller;
@@ -40,26 +40,29 @@ import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencyProvider;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
import org.thoughtcrime.securesms.gcm.FcmFetchManager;
import org.thoughtcrime.securesms.jobs.AccountConsistencyWorkerJob;
import org.thoughtcrime.securesms.jobs.BuildExpirationConfirmationJob;
import org.thoughtcrime.securesms.jobs.CheckServiceReachabilityJob;
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.ExternalLaunchDonationJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.InAppPaymentAuthCheckJob;
import org.thoughtcrime.securesms.jobs.InAppPaymentKeepAliveJob;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
@@ -69,32 +72,34 @@ import org.thoughtcrime.securesms.jobs.RefreshSvrCredentialsJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.RetrieveRemoteAnnouncementsJob;
import org.thoughtcrime.securesms.jobs.StoryOnboardingDownloadJob;
import org.thoughtcrime.securesms.jobs.SubscriptionKeepAliveJob;
import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver;
import org.thoughtcrime.securesms.messages.GroupSendEndorsementInternalNotifier;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.registration.util.RegistrationUtil;
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
import org.thoughtcrime.securesms.service.AnalyzeDatabaseAlarmListener;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.MessageBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
import org.thoughtcrime.securesms.service.webrtc.ActiveCallManager;
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
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.RemoteConfig;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@@ -124,7 +129,7 @@ import rxdogtag2.RxDogTag;
*
* @author Moxie Marlinspike
*/
public class ApplicationContext extends MultiDexApplication implements AppForegroundObserver.Listener {
public class ApplicationContext extends Application implements AppForegroundObserver.Listener {
private static final String TAG = Log.tag(ApplicationContext.class);
@@ -140,10 +145,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
long startTime = System.currentTimeMillis();
if (FeatureFlags.internalUser()) {
Tracer.getInstance().setMaxBufferSize(35_000);
}
super.onCreate();
AppStartup.getInstance().addBlocking("sqlcipher-init", () -> {
@@ -152,20 +153,21 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
})
.addBlocking("signal-store", () -> SignalStore.init(this))
.addBlocking("logging", () -> {
initializeLogging();
Log.i(TAG, "onCreate()");
})
.addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("anr-detector", this::startAnrDetector)
.addBlocking("security-provider", this::initializeSecurityProvider)
.addBlocking("crash-handling", this::initializeCrashHandling)
.addBlocking("rx-init", this::initializeRx)
.addBlocking("event-bus", () -> EventBus.builder().logNoSubscriberMessages(false).installDefaultEventBus())
.addBlocking("app-dependencies", this::initializeAppDependencies)
.addBlocking("scrubber", () -> Scrubber.setIdentifierHmacKeyProvider(() -> SignalStore.svr().getOrCreateMasterKey().deriveLoggingKey()))
.addBlocking("first-launch", this::initializeFirstEverAppLaunch)
.addBlocking("app-migrations", this::initializeApplicationMigrations)
.addBlocking("lifecycle-observer", () -> ApplicationDependencies.getAppForegroundObserver().addListener(this))
.addBlocking("lifecycle-observer", () -> AppForegroundObserver.addListener(this))
.addBlocking("message-retriever", this::initializeMessageRetrieval)
.addBlocking("dynamic-theme", () -> DynamicTheme.setDefaultDayNightMode(this))
.addBlocking("proxy-init", () -> {
@@ -175,9 +177,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
})
.addBlocking("blob-provider", this::initializeBlobProvider)
.addBlocking("feature-flags", FeatureFlags::init)
.addBlocking("remote-config", RemoteConfig::init)
.addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addBlocking("tracer", this::initializeTracer)
.addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete())
.addNonBlocking(() -> Glide.get(this))
.addNonBlocking(this::cleanAvatarStorage)
@@ -193,30 +196,33 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(this::beginJobLoop)
.addNonBlocking(EmojiSource::refresh)
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(() -> AppDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(AppDependencies::getBillingApi)
.addNonBlocking(this::ensureProfileUploaded)
.addNonBlocking(() -> ApplicationDependencies.getExpireStoriesManager().scheduleIfNecessary())
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
.addNonBlocking(() -> AppDependencies.getExpireStoriesManager().scheduleIfNecessary())
.addPostRender(() -> AppDependencies.getDeletedCallEventManager().scheduleIfNecessary())
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(this::initializeTrimThreadsByDateManager)
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), FeatureFlags.retryRespondMaxAge()))
.addPostRender(() -> SignalDatabase.messageLog().trimOldMessages(System.currentTimeMillis(), RemoteConfig.retryRespondMaxAge()))
.addPostRender(() -> JumboEmoji.updateCurrentVersion(this))
.addPostRender(RetrieveRemoteAnnouncementsJob::enqueue)
.addPostRender(() -> AndroidTelecomUtil.registerPhoneAccount())
.addPostRender(() -> ApplicationDependencies.getJobManager().add(new FontDownloaderJob()))
.addPostRender(() -> AppDependencies.getJobManager().add(new FontDownloaderJob()))
.addPostRender(CheckServiceReachabilityJob::enqueueIfNecessary)
.addPostRender(GroupV2UpdateSelfProfileKeyJob::enqueueForGroupsIfNecessary)
.addPostRender(StoryOnboardingDownloadJob.Companion::enqueueIfNeeded)
.addPostRender(PnpInitializeDevicesJob::enqueueIfNecessary)
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
.addPostRender(() -> AppDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
.addPostRender(() -> AppDependencies.getRecipientCache().warmUp())
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
.addPostRender(GroupRingCleanupJob::enqueue)
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
.addPostRender(() -> ActiveCallManager.clearNotifications(this))
.addPostRender(() -> GroupSendEndorsementInternalNotifier.init())
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -229,20 +235,20 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
long startTime = System.currentTimeMillis();
Log.i(TAG, "App is now visible.");
ApplicationDependencies.getFrameRateTracker().start();
ApplicationDependencies.getMegaphoneRepository().onAppForegrounded();
ApplicationDependencies.getDeadlockDetector().start();
SubscriptionKeepAliveJob.enqueueAndTrackTimeIfNecessary();
ExternalLaunchDonationJob.enqueueIfNecessary();
AppDependencies.getFrameRateTracker().start();
AppDependencies.getMegaphoneRepository().onAppForegrounded();
AppDependencies.getDeadlockDetector().start();
InAppPaymentKeepAliveJob.enqueueAndTrackTimeIfNecessary();
FcmFetchManager.onForeground(this);
startAnrDetector();
SignalExecutors.BOUNDED.execute(() -> {
FeatureFlags.refreshIfNecessary();
InAppPaymentAuthCheckJob.enqueueIfNeeded();
RemoteConfig.refreshIfNecessary();
RetrieveProfileJob.enqueueRoutineFetchIfNecessary();
executePendingContactSync();
KeyCachingService.onAppForegrounded(this);
ApplicationDependencies.getShakeToReport().enable();
AppDependencies.getShakeToReport().enable();
checkBuildExpiration();
MemoryTracker.start();
@@ -251,7 +257,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
long timeDiff = currentTime - lastForegroundTime;
if (timeDiff < 0) {
Log.w(TAG, "Time travel! The system clock has moved backwards. (currentTime: " + currentTime + " ms, lastForegroundTime: " + lastForegroundTime + " ms, diff: " + timeDiff + " ms)");
Log.w(TAG, "Time travel! The system clock has moved backwards. (currentTime: " + currentTime + " ms, lastForegroundTime: " + lastForegroundTime + " ms, diff: " + timeDiff + " ms)", true);
}
SignalStore.misc().setLastForegroundTime(currentTime);
@@ -264,18 +270,18 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
public void onBackground() {
Log.i(TAG, "App is no longer visible.");
KeyCachingService.onAppBackgrounded(this);
ApplicationDependencies.getMessageNotifier().clearVisibleThread();
ApplicationDependencies.getFrameRateTracker().stop();
ApplicationDependencies.getShakeToReport().disable();
ApplicationDependencies.getDeadlockDetector().stop();
AppDependencies.getMessageNotifier().clearVisibleThread();
AppDependencies.getFrameRateTracker().stop();
AppDependencies.getShakeToReport().disable();
AppDependencies.getDeadlockDetector().stop();
MemoryTracker.stop();
AnrDetector.stop();
}
public void checkBuildExpiration() {
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
Log.w(TAG, "Build expired!");
SignalStore.misc().setClientDeprecated(true);
if (Util.getTimeUntilBuildExpiry(SignalStore.misc().getEstimatedServerTime()) <= 0 && !SignalStore.misc().isClientDeprecated()) {
Log.w(TAG, "Build potentially expired! Enqueing job to check.", true);
AppDependencies.getJobManager().add(new BuildExpirationConfirmationJob());
}
}
@@ -284,7 +290,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
* This is so we can capture ANR's that happen on boot before the foreground event.
*/
private void startAnrDetector() {
AnrDetector.start(TimeUnit.SECONDS.toMillis(5), FeatureFlags::internalUser, (dumps) -> {
AnrDetector.start(TimeUnit.SECONDS.toMillis(5), RemoteConfig::internalUser, (dumps) -> {
LogDatabase.getInstance(this).anrs().save(System.currentTimeMillis(), dumps);
return Unit.INSTANCE;
});
@@ -309,9 +315,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
@VisibleForTesting
protected void initializeLogging() {
Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), new PersistentLogger(this));
Log.initialize(RemoteConfig::internalUser, new AndroidLogger(), new PersistentLogger(this));
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
SignalProtocolLoggerProvider.initializeLogging(BuildConfig.LIBSIGNAL_LOG_LEVEL);
SignalExecutors.UNBOUNDED.execute(() -> {
Log.blockUntilAllWritesFinished();
@@ -350,16 +357,20 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
private void initializeApplicationMigrations() {
ApplicationMigrations.onApplicationCreate(this, ApplicationDependencies.getJobManager());
ApplicationMigrations.onApplicationCreate(this, AppDependencies.getJobManager());
}
public void initializeMessageRetrieval() {
ApplicationDependencies.getIncomingMessageObserver();
AppDependencies.getIncomingMessageObserver();
}
@VisibleForTesting
void initializeAppDependencies() {
ApplicationDependencies.init(this, new ApplicationDependencyProvider(this));
if (!AppDependencies.isInitialized()) {
Log.i(TAG, "Initializing AppDependencies.");
AppDependencies.init(this, new ApplicationDependencyProvider(this));
}
AppForegroundObserver.begin();
}
private void initializeFirstEverAppLaunch() {
@@ -371,45 +382,53 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
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) {
} else if (!SignalStore.settings().getPassphraseDisabled() && 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) {
} else if (!SignalStore.settings().getPassphraseDisabled() && VersionTracker.getDaysSinceFirstInstalled(this) < 912) {
Log.i(TAG, "Detected a not-recent install that doesn't have passphrases disabled -- disabling now.");
TextSecurePreferences.setPasswordDisabled(this, true);
SignalStore.settings().setPassphraseDisabled(true);
}
}
private void initializeFcmCheck() {
if (SignalStore.account().isRegistered()) {
long nextSetTime = SignalStore.account().getFcmTokenLastSetTime() + TimeUnit.HOURS.toMillis(6);
long lastSetTime = SignalStore.account().getFcmTokenLastSetTime();
long nextSetTime = lastSetTime + TimeUnit.HOURS.toMillis(6);
long now = System.currentTimeMillis();
if (SignalStore.account().getFcmToken() == null || nextSetTime <= System.currentTimeMillis()) {
ApplicationDependencies.getJobManager().add(new FcmRefreshJob());
if (SignalStore.account().getFcmToken() == null || nextSetTime <= now || lastSetTime > now) {
AppDependencies.getJobManager().add(new FcmRefreshJob());
}
}
}
private void initializeExpiringMessageManager() {
ApplicationDependencies.getExpiringMessageManager().checkSchedule();
AppDependencies.getExpiringMessageManager().checkSchedule();
}
private void initializeRevealableMessageManager() {
ApplicationDependencies.getViewOnceMessageManager().scheduleIfNecessary();
AppDependencies.getViewOnceMessageManager().scheduleIfNecessary();
}
private void initializePendingRetryReceiptManager() {
ApplicationDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
AppDependencies.getPendingRetryReceiptManager().scheduleIfNecessary();
}
private void initializeScheduledMessageManager() {
ApplicationDependencies.getScheduledMessageManager().scheduleIfNecessary();
AppDependencies.getScheduledMessageManager().scheduleIfNecessary();
}
private void initializeTrimThreadsByDateManager() {
KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration();
if (keepMessagesDuration != KeepMessagesDuration.FOREVER) {
ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
AppDependencies.getTrimThreadsByDateManager().scheduleIfNecessary();
}
}
private void initializeTracer() {
if (RemoteConfig.internalUser()) {
Tracer.getInstance().setMaxBufferSize(35_000);
}
}
@@ -417,8 +436,10 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
RotateSignedPreKeyListener.schedule(this);
DirectoryRefreshListener.schedule(this);
LocalBackupListener.schedule(this);
MessageBackupListener.schedule(this);
RotateSenderCertificateListener.schedule(this);
RoutineMessageFetchReceiver.startOrUpdateAlarm(this);
AnalyzeDatabaseAlarmListener.schedule(this);
if (BuildConfig.MANAGES_APP_UPDATES) {
ApkUpdateRefreshListener.schedule(this);
@@ -428,12 +449,9 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
private void initializeRingRtc() {
try {
Map<String, String> fieldTrials = new HashMap<>();
if (FeatureFlags.callingFieldTrialAnyAddressPortsKillSwitch()) {
if (RemoteConfig.callingFieldTrialAnyAddressPortsKillSwitch()) {
fieldTrials.put("RingRTC-AnyAddressPortsKillSwitch", "Enabled");
}
if (!SignalStore.internalValues().callingDisableLBRed()) {
fieldTrials.put("RingRTC-Audio-LBRed-For-Opus", "Enabled,bitrate_pri:22000");
}
CallManager.initialize(this, new RingRtcLogger(), fieldTrials);
} catch (UnsatisfiedLinkError e) {
throw new AssertionError("Unable to load ringrtc library", e);
@@ -442,7 +460,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
@WorkerThread
private void initializeCircumvention() {
if (ApplicationDependencies.getSignalServiceNetworkAccess().isCensored()) {
if (AppDependencies.getSignalServiceNetworkAccess().isCensored()) {
try {
ProviderInstaller.installIfNeeded(ApplicationContext.this);
} catch (Throwable t) {
@@ -452,21 +470,21 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
private void ensureProfileUploaded() {
if (SignalStore.account().isRegistered() && !SignalStore.registrationValues().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
if (SignalStore.account().isRegistered() && !SignalStore.registration().hasUploadedProfile() && !Recipient.self().getProfileName().isEmpty()) {
Log.w(TAG, "User has a profile, but has not uploaded one. Uploading now.");
ApplicationDependencies.getJobManager().add(new ProfileUploadJob());
AppDependencies.getJobManager().add(new ProfileUploadJob());
}
}
private void executePendingContactSync() {
if (TextSecurePreferences.needsFullContactSync(this)) {
ApplicationDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
AppDependencies.getJobManager().add(new MultiDeviceContactUpdateJob(true));
}
}
@VisibleForTesting
protected void beginJobLoop() {
ApplicationDependencies.getJobManager().beginJobLoop();
AppDependencies.getJobManager().beginJobLoop();
}
@WorkerThread

View File

@@ -28,11 +28,11 @@ import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar;
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatarDrawable;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FullscreenHelper;
@@ -91,16 +91,18 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
Recipient.live(recipientId).observe(this, recipient -> {
ContactPhoto contactPhoto = recipient.isSelf() ? new ProfileContactPhoto(recipient)
: recipient.getContactPhoto();
FallbackContactPhoto fallbackPhoto = recipient.isSelf() ? new ResourceContactPhoto(R.drawable.ic_profile_outline_40, R.drawable.ic_profile_outline_20, R.drawable.ic_person_large)
: recipient.getFallbackContactPhoto();
FallbackAvatar fallbackAvatar = recipient.isSelf() ? new FallbackAvatar.Resource.Person(recipient.getAvatarColor())
: recipient.getFallbackAvatar();
Drawable fallbackDrawable = new FallbackAvatarDrawable(context, fallbackAvatar);
Resources resources = this.getResources();
Glide.with(this)
.asBitmap()
.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this))
.error(fallbackPhoto.asCallCard(this))
.fallback(fallbackDrawable)
.error(fallbackDrawable)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.addListener(new RequestListener<Bitmap>() {
@Override

View File

@@ -14,7 +14,7 @@ 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.dependencies.AppDependencies;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.ConfigurationUtil;
import org.thoughtcrime.securesms.util.WindowUtil;
@@ -47,7 +47,7 @@ public abstract class BaseActivity extends AppCompatActivity {
@Override
protected void onStart() {
logEvent("onStart()");
ApplicationDependencies.getShakeToReport().registerActivity(this);
AppDependencies.getShakeToReport().registerActivity(this);
super.onStart();
}

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms;
import android.net.Uri;
import android.view.GestureDetector;
import android.view.View;
import androidx.annotation.NonNull;
@@ -58,6 +59,10 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void setEventListener(@Nullable EventListener listener);
default void setGestureDetector(@Nullable GestureDetector gestureDetector) {
// Intentionally Blank.
}
default void setParentScrolling(boolean isParentScrolling) {
// Intentionally Blank.
}
@@ -107,6 +112,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
void onChangeProfileNameUpdateContact(@NonNull Recipient recipient);
void onCallToAction(@NonNull String action);
void onDonateClicked();
void onBlockJoinRequest(@NonNull Recipient recipient);
@@ -120,11 +126,13 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onEditedIndicatorClicked(@NonNull ConversationMessage conversationMessage);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
void onShowSafetyTips(boolean forGroup);
void onReportSpamLearnMoreClicked();
void onMessageRequestAcceptOptionsClicked();
void onItemDoubleClick(MultiselectPart multiselectPart);
void onPaymentTombstoneClicked();
}
}

View File

@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.util.ServiceUtil
class BiometricDeviceAuthentication(
private val biometricManager: BiometricManager,
private val biometricPrompt: BiometricPrompt,
private val biometricPromptInfo: PromptInfo
private var biometricPromptInfo: PromptInfo
) {
companion object {
const val AUTHENTICATED = 1
@@ -35,6 +35,11 @@ class BiometricDeviceAuthentication(
private val DISALLOWED_BIOMETRIC_VERSIONS = setOf(28, 29)
}
fun canAuthenticate(context: Context): Boolean {
val isKeyGuardSecure = ServiceUtil.getKeyguardManager(context).isKeyguardSecure
return isKeyGuardSecure && biometricManager.canAuthenticate(ALLOWED_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS
}
fun authenticate(context: Context, force: Boolean, showConfirmDeviceCredentialIntent: () -> Unit): Boolean {
val isKeyGuardSecure = ServiceUtil.getKeyguardManager(context).isKeyguardSecure
@@ -65,6 +70,10 @@ class BiometricDeviceAuthentication(
fun cancelAuthentication() {
biometricPrompt.cancelAuthentication()
}
fun updatePromptInfo(promptInfo: PromptInfo) {
biometricPromptInfo = promptInfo
}
}
class BiometricDeviceLockContract : ActivityResultContract<String, Int>() {

View File

@@ -29,14 +29,10 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DisplayMetricsUtil;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
import java.io.IOException;
import java.lang.ref.WeakReference;

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms
import android.content.Context
import android.view.View
import android.widget.TextView
import com.google.android.material.button.MaterialButton
import org.thoughtcrime.securesms.contacts.paged.ContactSearchAdapter
import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration
import org.thoughtcrime.securesms.contacts.paged.ContactSearchData
@@ -24,6 +25,8 @@ class ContactSelectionListAdapter(
init {
registerFactory(NewGroupModel::class.java, LayoutFactory({ NewGroupViewHolder(it, onClickCallbacks::onNewGroupClicked) }, R.layout.contact_selection_new_group_item))
registerFactory(InviteToSignalModel::class.java, LayoutFactory({ InviteToSignalViewHolder(it, onClickCallbacks::onInviteToSignalClicked) }, R.layout.contact_selection_invite_action_item))
registerFactory(FindContactsModel::class.java, LayoutFactory({ FindContactsViewHolder(it, onClickCallbacks::onFindContactsClicked) }, R.layout.contact_selection_find_contacts_item))
registerFactory(FindContactsBannerModel::class.java, LayoutFactory({ FindContactsBannerViewHolder(it, onClickCallbacks::onDismissFindContactsBannerClicked, onClickCallbacks::onFindContactsClicked) }, R.layout.contact_selection_find_contacts_banner_item))
registerFactory(RefreshContactsModel::class.java, LayoutFactory({ RefreshContactsViewHolder(it, onClickCallbacks::onRefreshContactsClicked) }, R.layout.contact_selection_refresh_action_item))
registerFactory(MoreHeaderModel::class.java, LayoutFactory({ MoreHeaderViewHolder(it) }, R.layout.contact_search_section_header))
registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.contact_selection_empty_state))
@@ -46,6 +49,16 @@ class ContactSelectionListAdapter(
override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true
}
class FindContactsModel : MappingModel<FindContactsModel> {
override fun areItemsTheSame(newItem: FindContactsModel): Boolean = true
override fun areContentsTheSame(newItem: FindContactsModel): Boolean = true
}
class FindContactsBannerModel : MappingModel<FindContactsBannerModel> {
override fun areItemsTheSame(newItem: FindContactsBannerModel): Boolean = true
override fun areContentsTheSame(newItem: FindContactsBannerModel): Boolean = true
}
class FindByUsernameModel : MappingModel<FindByUsernameModel> {
override fun areItemsTheSame(newItem: FindByUsernameModel): Boolean = true
override fun areContentsTheSame(newItem: FindByUsernameModel): Boolean = true
@@ -86,6 +99,23 @@ class ContactSelectionListAdapter(
override fun bind(model: RefreshContactsModel) = Unit
}
private class FindContactsViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<FindContactsModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: FindContactsModel) = Unit
}
private class FindContactsBannerViewHolder(itemView: View, onDismissListener: () -> Unit, onClickListener: () -> Unit) : MappingViewHolder<FindContactsBannerModel>(itemView) {
init {
itemView.findViewById<MaterialButton>(R.id.no_thanks_button).setOnClickListener { onDismissListener() }
itemView.findViewById<MaterialButton>(R.id.allow_contacts_button).setOnClickListener { onClickListener() }
}
override fun bind(model: FindContactsBannerModel) = Unit
}
private class MoreHeaderViewHolder(itemView: View) : MappingViewHolder<MoreHeaderModel>(itemView) {
private val headerTextView: TextView = itemView.findViewById(R.id.section_header)
@@ -129,6 +159,8 @@ class ContactSelectionListAdapter(
INVITE_TO_SIGNAL("invite-to-signal"),
MORE_HEADING("more-heading"),
REFRESH_CONTACTS("refresh-contacts"),
FIND_CONTACTS("find-contacts"),
FIND_CONTACTS_BANNER("find-contacts-banner"),
FIND_BY_USERNAME("find-by-username"),
FIND_BY_PHONE_NUMBER("find-by-phone-number");
@@ -152,6 +184,8 @@ class ContactSelectionListAdapter(
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
ArbitraryRow.FIND_CONTACTS -> FindContactsModel()
ArbitraryRow.FIND_CONTACTS_BANNER -> FindContactsBannerModel()
ArbitraryRow.FIND_BY_PHONE_NUMBER -> FindByPhoneNumberModel()
ArbitraryRow.FIND_BY_USERNAME -> FindByUsernameModel()
}
@@ -162,6 +196,8 @@ class ContactSelectionListAdapter(
fun onNewGroupClicked()
fun onInviteToSignalClicked()
fun onRefreshContactsClicked()
fun onFindContactsClicked()
fun onDismissFindContactsBannerClicked()
fun onFindByPhoneNumberClicked()
fun onFindByUsernameClicked()
}

View File

@@ -53,6 +53,7 @@ import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.RxExtensions;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.components.RecyclerViewFastScroller;
import org.thoughtcrime.securesms.contacts.ContactChipViewModel;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
@@ -70,13 +71,13 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchState;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository;
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameAciFetchResult;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -125,10 +126,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
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;
@@ -223,43 +220,25 @@ public final class ContactSelectionListFragment extends LoggingFragment {
public void onStart() {
super.onStart();
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
.ifNecessary()
.onAllGranted(() -> {
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
handleContactPermissionGranted();
} else {
contactSearchMediator.refresh();
}
})
.onAnyDenied(() -> {
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
if (safeArguments().getBoolean(RECENTS, requireActivity().getIntent().getBooleanExtra(RECENTS, false))) {
contactSearchMediator.refresh();
} else {
initializeNoContactsPermission();
}
})
.execute();
if (hasContactsPermissions(requireContext()) && !TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
handleContactPermissionGranted();
} else {
requireActivity().getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
contactSearchMediator.refresh();
}
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.contact_selection_list_fragment, container, false);
emptyText = view.findViewById(android.R.id.empty);
recyclerView = view.findViewById(R.id.recycler_view);
swipeRefresh = view.findViewById(R.id.swipe_refresh);
fastScroller = view.findViewById(R.id.fast_scroller);
showContactsLayout = view.findViewById(R.id.show_contacts_container);
showContactsButton = view.findViewById(R.id.show_contacts_button);
showContactsDescription = view.findViewById(R.id.show_contacts_description);
showContactsProgress = view.findViewById(R.id.progress);
chipRecycler = view.findViewById(R.id.chipRecycler);
constraintLayout = view.findViewById(R.id.container);
headerActionView = view.findViewById(R.id.header_action);
emptyText = view.findViewById(android.R.id.empty);
recyclerView = view.findViewById(R.id.recycler_view);
swipeRefresh = view.findViewById(R.id.swipe_refresh);
fastScroller = view.findViewById(R.id.fast_scroller);
chipRecycler = view.findViewById(R.id.chipRecycler);
constraintLayout = view.findViewById(R.id.container);
headerActionView = view.findViewById(R.id.header_action);
final LinearLayoutManager layoutManager = new LinearLayoutManager(requireContext());
@@ -269,6 +248,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder) {
return true;
}
@Override
public void onAnimationFinished(@NonNull RecyclerView.ViewHolder viewHolder) {
recyclerView.setAlpha(1f);
}
});
contactChipViewModel = new ViewModelProvider(this).get(ContactChipViewModel.class);
@@ -354,7 +338,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
selectionLimit,
new ContactSearchAdapter.DisplayOptions(
isMulti,
ContactSearchAdapter.DisplaySmsTag.DEFAULT,
ContactSearchAdapter.DisplaySecondaryInformation.ALWAYS,
newCallCallback != null,
false
@@ -372,6 +355,19 @@ public final class ContactSelectionListFragment extends LoggingFragment {
fixedContacts,
displayOptions,
new ContactSelectionListAdapter.OnContactSelectionClick() {
@Override
public void onDismissFindContactsBannerClicked() {
SignalStore.uiHints().markDismissedContactsPermissionBanner();
if (onRefreshListener != null) {
onRefreshListener.onRefresh();
}
}
@Override
public void onFindContactsClicked() {
requestContactPermissions();
}
@Override
public void onRefreshContactsClicked() {
if (onRefreshListener != null) {
@@ -498,6 +494,27 @@ public final class ContactSelectionListFragment extends LoggingFragment {
return isMulti;
}
private void requestContactPermissions() {
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
.ifNecessary()
.onAllGranted(() -> {
recyclerView.setAlpha(0.5f);
if (!TextSecurePreferences.hasSuccessfullyRetrievedDirectory(getActivity())) {
handleContactPermissionGranted();
} else {
contactSearchMediator.refresh();
if (onRefreshListener != null) {
swipeRefresh.setRefreshing(true);
onRefreshListener.onRefresh();
}
}
})
.onAnyDenied(() -> contactSearchMediator.refresh())
.withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts), null, R.string.ContactSelectionListFragment_allow_access_contacts, R.string.ContactSelectionListFragment_to_find_people, getParentFragmentManager())
.execute();
}
private void initializeCursor() {
recyclerView.addItemDecoration(new LetterHeaderDecoration(requireContext(), this::hideLetterHeaders));
recyclerView.setAdapter(contactSearchMediator.getAdapter());
@@ -521,28 +538,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
return hasQueryFilter() || shouldDisplayRecents();
}
private void initializeNoContactsPermission() {
swipeRefresh.setVisibility(View.GONE);
showContactsLayout.setVisibility(View.VISIBLE);
showContactsProgress.setVisibility(View.INVISIBLE);
showContactsDescription.setText(R.string.contact_selection_list_fragment__signal_needs_access_to_your_contacts_in_order_to_display_them);
showContactsButton.setVisibility(View.VISIBLE);
showContactsButton.setOnClickListener(v -> {
Permissions.with(this)
.request(Manifest.permission.WRITE_CONTACTS, Manifest.permission.READ_CONTACTS)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.ContactSelectionListFragment_signal_requires_the_contacts_permission_in_order_to_display_your_contacts))
.onSomeGranted(permissions -> {
if (permissions.contains(Manifest.permission.WRITE_CONTACTS)) {
handleContactPermissionGranted();
}
})
.execute();
});
}
public void setQueryFilter(String filter) {
if (Objects.equals(filter, this.cursorFilter)) {
return;
@@ -583,7 +578,6 @@ public final class ContactSelectionListFragment extends LoggingFragment {
}
swipeRefresh.setVisibility(View.VISIBLE);
showContactsLayout.setVisibility(View.GONE);
emptyText.setText(R.string.contact_selection_group_activity__no_contacts);
boolean useFastScroller = count > 20;
@@ -614,12 +608,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
new AsyncTask<Void, Void, Boolean>() {
@Override
protected void onPreExecute() {
swipeRefresh.setVisibility(View.GONE);
showContactsLayout.setVisibility(View.VISIBLE);
showContactsButton.setVisibility(View.INVISIBLE);
showContactsDescription.setText(R.string.ConversationListFragment_loading);
showContactsProgress.setVisibility(View.VISIBLE);
showContactsProgress.spin();
if (onRefreshListener != null) {
setRefreshing(true);
onRefreshListener.onRefresh();
}
}
@Override
@@ -636,14 +628,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
@Override
protected void onPostExecute(Boolean result) {
if (result) {
showContactsLayout.setVisibility(View.GONE);
swipeRefresh.setVisibility(View.VISIBLE);
reset();
} else {
Context context = getContext();
if (context != null) {
Toast.makeText(getContext(), R.string.ContactSelectionListFragment_error_retrieving_contacts_check_your_network_connection, Toast.LENGTH_LONG).show();
initializeNoContactsPermission();
}
}
}
@@ -890,6 +879,13 @@ public final class ContactSelectionListFragment extends LoggingFragment {
return ContactSearchConfiguration.build(builder -> {
builder.setQuery(contactSearchState.getQuery());
if (newConversationCallback != null &&
!hasContactsPermissions(requireContext()) &&
!SignalStore.uiHints().getDismissedContactsPermissionBanner() &&
!hasQuery) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_CONTACTS_BANNER.getCode());
}
if (newConversationCallback != null && !hasQuery) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
}
@@ -946,7 +942,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
builder.username(newRowMode);
}
if ((newCallCallback != null || newConversationCallback != null) && !hasQuery) {
if ((newCallCallback != null || newConversationCallback != null)) {
addMoreSection(builder);
builder.withEmptyState(emptyBuilder -> {
emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
@@ -959,9 +955,17 @@ public final class ContactSelectionListFragment extends LoggingFragment {
});
}
private boolean hasContactsPermissions(@NonNull Context context) {
return Permissions.hasAll(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS);
}
private void addMoreSection(@NonNull ContactSearchConfiguration.Builder builder) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.MORE_HEADING.getCode());
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode());
if (hasContactsPermissions(requireContext())) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.REFRESH_CONTACTS.getCode());
} else if (SignalStore.uiHints().getDismissedContactsPermissionBanner()) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_CONTACTS.getCode());
}
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.INVITE_TO_SIGNAL.getCode());
}
@@ -1006,12 +1010,16 @@ public final class ContactSelectionListFragment extends LoggingFragment {
private class CallButtonClickCallbacks implements ContactSearchAdapter.CallButtonClickCallbacks {
@Override
public void onVideoCallButtonClicked(@NonNull Recipient recipient) {
CommunicationActions.startVideoCall(ContactSelectionListFragment.this, recipient);
CommunicationActions.startVideoCall(ContactSelectionListFragment.this, recipient, () -> {
YouAreAlreadyInACallSnackbar.show(requireView());
});
}
@Override
public void onAudioCallButtonClicked(@NonNull Recipient recipient) {
CommunicationActions.startVoiceCall(ContactSelectionListFragment.this, recipient);
CommunicationActions.startVoiceCall(ContactSelectionListFragment.this, recipient, () -> {
YouAreAlreadyInACallSnackbar.show(requireView());
});
}
}

View File

@@ -28,7 +28,7 @@ import org.signal.libsignal.protocol.ecc.ECPublicKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -131,14 +131,15 @@ public class DeviceActivity extends PassphraseRequiredActivity
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code))
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_scan_qr_code_allow_camera), R.drawable.symbol_camera_24)
.withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getSupportFragmentManager())
.onAllGranted(() -> {
getSupportFragmentManager().beginTransaction()
.replace(R.id.fragment_container, deviceAddFragment)
.addToBackStack(null)
.commitAllowingStateLoss();
})
.onAnyDenied(() -> Toast.makeText(this, R.string.DeviceActivity_unable_to_scan_a_qr_code_without_the_camera_permission, Toast.LENGTH_LONG).show())
.onAnyDenied(() -> Toast.makeText(this, R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show())
.execute();
}
@@ -190,7 +191,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
try {
Context context = DeviceActivity.this;
SignalServiceAccountManager accountManager = ApplicationDependencies.getSignalServiceAccountManager();
SignalServiceAccountManager accountManager = AppDependencies.getSignalServiceAccountManager();
String verificationCode = accountManager.getNewDeviceVerificationCode();
String ephemeralId = uri.getQueryParameter("uuid");
String publicKeyEncoded = uri.getQueryParameter("pub_key");

View File

@@ -25,7 +25,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.devicelist.Device;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -60,7 +60,7 @@ public class DeviceListFragment extends ListFragment
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
this.accountManager = AppDependencies.getSignalServiceAccountManager();
}
@Override

View File

@@ -18,8 +18,11 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.donations.StripeApi;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment;
import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet;
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment;
import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner;
@@ -112,14 +115,24 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
switch (state) {
case NONE:
break;
case PROMPT_BATTERY_SAVER_DIALOG:
case PROMPT_SPECIFIC_BATTERY_SAVER_DIALOG:
DeviceSpecificNotificationBottomSheet.show(getSupportFragmentManager());
break;
case PROMPT_GENERAL_BATTERY_SAVER_DIALOG:
PromptBatterySaverDialogFragment.show(getSupportFragmentManager());
break;
case PROMPT_CONNECTIVITY_WARNING:
ConnectivityWarningBottomSheet.show(getSupportFragmentManager());
break;
case PROMPT_DEBUGLOGS_FOR_NOTIFICATIONS:
DebugLogsPromptDialogFragment.show(this, DebugLogsPromptDialogFragment.Purpose.NOTIFICATIONS);
break;
case PROMPT_DEBUGLOGS_FOR_CRASH:
DebugLogsPromptDialogFragment.show(this, DebugLogsPromptDialogFragment.Purpose.CRASH);
break;
case PROMPT_DEBUGLOGS_FOR_CONNECTIVITY_WARNING:
DebugLogsPromptDialogFragment.show(this, DebugLogsPromptDialogFragment.Purpose.CONNECTIVITY_WARNING);
break;
}
}
@@ -231,7 +244,9 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
private void handleCallLinkInIntent(Intent intent) {
Uri data = intent.getData();
if (data != null) {
CommunicationActions.handlePotentialCallLinkUrl(this, data.toString());
CommunicationActions.handlePotentialCallLinkUrl(this, data.toString(), () -> {
YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content));
});
}
}

View File

@@ -39,6 +39,7 @@ import org.signal.core.util.DimensionUnit;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository;
@@ -136,7 +137,7 @@ public class NewConversationActivity extends ContactSelectionActivity
if (result instanceof RecipientRepository.LookupResult.Success) {
Recipient resolved = Recipient.resolved(((RecipientRepository.LookupResult.Success) result).getRecipientId());
if (resolved.isRegistered() && resolved.hasServiceId()) {
if (resolved.isRegistered() && resolved.getHasServiceId()) {
launch(resolved);
}
} else if (result instanceof RecipientRepository.LookupResult.NotFound || result instanceof RecipientRepository.LookupResult.InvalidEntry) {
@@ -305,7 +306,9 @@ public class NewConversationActivity extends ContactSelectionActivity
R.drawable.ic_phone_right_24,
getString(R.string.NewConversationActivity__audio_call),
R.color.signal_colorOnSurface,
() -> CommunicationActions.startVoiceCall(this, recipient)
() -> CommunicationActions.startVoiceCall(this, recipient, () -> {
YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content));
})
);
} else {
return null;
@@ -321,7 +324,9 @@ public class NewConversationActivity extends ContactSelectionActivity
R.drawable.ic_video_call_24,
getString(R.string.NewConversationActivity__video_call),
R.color.signal_colorOnSurface,
() -> CommunicationActions.startVideoCall(this, recipient)
() -> CommunicationActions.startVideoCall(this, recipient, () -> {
YouAreAlreadyInACallSnackbar.show(findViewById(android.R.id.content));
})
);
}
@@ -334,13 +339,7 @@ public class NewConversationActivity extends ContactSelectionActivity
R.drawable.ic_minus_circle_20, // TODO [alex] -- correct asset
getString(R.string.NewConversationActivity__remove),
R.color.signal_colorOnSurface,
() -> {
if (recipient.isSystemContact()) {
displayIsInSystemContactsDialog(recipient);
} else {
displayRemovalDialog(recipient);
}
}
() -> displayRemovalDialog(recipient)
);
}

View File

@@ -63,6 +63,7 @@ public abstract class PassphraseActivity extends BaseActivity {
if (nextIntent != null) {
try {
startActivity(nextIntent);
overridePendingTransition(R.anim.fade_in, R.anim.fade_out);
} catch (java.lang.SecurityException e) {
Log.w(TAG, "Access permission not passed from PassphraseActivity, retry sharing.");
}

View File

@@ -29,9 +29,9 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
* Activity for changing a user's local encryption passphrase.
@@ -81,7 +81,7 @@ public class PassphraseChangeActivity extends PassphraseActivity {
this.okButton.setOnClickListener(new OkButtonClickListener());
this.cancelButton.setOnClickListener(new CancelButtonClickListener());
if (TextSecurePreferences.isPasswordDisabled(this)) {
if (SignalStore.settings().getPassphraseDisabled()) {
this.originalPassphrase.setVisibility(View.GONE);
} else {
this.originalPassphrase.setVisibility(View.VISIBLE);
@@ -97,7 +97,7 @@ public class PassphraseChangeActivity extends PassphraseActivity {
String passphrase = (newText == null ? "" : newText.toString());
String passphraseRepeat = (repeatText == null ? "" : repeatText.toString());
if (TextSecurePreferences.isPasswordDisabled(this)) {
if (SignalStore.settings().getPassphraseDisabled()) {
original = MasterSecretUtil.UNENCRYPTED_PASSPHRASE;
}
@@ -142,7 +142,7 @@ public class PassphraseChangeActivity extends PassphraseActivity {
protected MasterSecret doInBackground(String... params) {
try {
MasterSecret masterSecret = MasterSecretUtil.changeMasterSecretPassphrase(context, params[0], params[1]);
TextSecurePreferences.setPasswordDisabled(context, false);
SignalStore.settings().setPassphraseDisabled(false);
return masterSecret;

View File

@@ -20,7 +20,6 @@ import android.animation.Animator;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputType;
@@ -34,19 +33,19 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.Animation;
import android.view.animation.BounceInterpolator;
import android.view.animation.TranslateAnimation;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import com.airbnb.lottie.LottieAnimationView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
@@ -55,12 +54,13 @@ import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
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;
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
import kotlin.Unit;
@@ -74,14 +74,15 @@ public class PassphrasePromptActivity extends PassphraseActivity {
private static final String TAG = Log.tag(PassphrasePromptActivity.class);
private static final short AUTHENTICATE_REQUEST_CODE = 1007;
private static final String BUNDLE_ALREADY_SHOWN = "bundle_already_shown";
public static final String FROM_FOREGROUND = "from_foreground";
public static final String FROM_FOREGROUND = "from_foreground";
private DynamicIntroTheme dynamicTheme = new DynamicIntroTheme();
private DynamicLanguage dynamicLanguage = new DynamicLanguage();
private View passphraseAuthContainer;
private ImageView fingerprintPrompt;
private TextView lockScreenButton;
private View passphraseAuthContainer;
private LottieAnimationView unlockView;
private TextView lockScreenButton;
private LearnMoreTextView learnMoreText;
private EditText passphraseText;
private ImageButton showButton;
@@ -129,14 +130,11 @@ public class PassphrasePromptActivity extends PassphraseActivity {
setLockTypeVisibility();
if (TextSecurePreferences.isScreenLockEnabled(this) && !authenticated && !hadFailure) {
if (SignalStore.settings().getScreenLockEnabled() && !authenticated && !hadFailure) {
ThreadUtil.postToMain(resumeScreenLockRunnable);
}
hadFailure = false;
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN);
}
@Override
@@ -169,9 +167,6 @@ public class PassphrasePromptActivity extends PassphraseActivity {
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;
@@ -188,6 +183,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
} else {
Log.w(TAG, "Authentication failed");
hadFailure = true;
showHelpDialog();
}
}
@@ -243,8 +239,9 @@ public class PassphrasePromptActivity extends PassphraseActivity {
visibilityToggle = findViewById(R.id.button_toggle);
passphraseText = findViewById(R.id.passphrase_edit);
passphraseAuthContainer = findViewById(R.id.password_auth_container);
fingerprintPrompt = findViewById(R.id.fingerprint_auth_container);
lockScreenButton = findViewById(R.id.lock_screen_auth_container);
unlockView = findViewById(R.id.unlock_view);
lockScreenButton = findViewById(R.id.lock_screen_button);
learnMoreText = findViewById(R.id.learn_more_text);
biometricManager = BiometricManager.from(this);
biometricPrompt = new BiometricPrompt(this, new BiometricAuthenticationListener());
BiometricPrompt.PromptInfo biometricPromptInfo = new BiometricPrompt.PromptInfo
@@ -268,21 +265,17 @@ public class PassphrasePromptActivity extends PassphraseActivity {
passphraseText.setImeActionLabel(getString(R.string.prompt_passphrase_activity__unlock),
EditorInfo.IME_ACTION_DONE);
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.core_ultramarine), PorterDuff.Mode.SRC_IN);
lockScreenButton.setOnClickListener(v -> resumeScreenLock(true));
}
private void setLockTypeVisibility() {
if (TextSecurePreferences.isScreenLockEnabled(this)) {
if (SignalStore.settings().getScreenLockEnabled()) {
passphraseAuthContainer.setVisibility(View.GONE);
fingerprintPrompt.setVisibility(biometricManager.canAuthenticate(BiometricDeviceAuthentication.BIOMETRIC_AUTHENTICATORS) == BiometricManager.BIOMETRIC_SUCCESS ? View.VISIBLE
: View.GONE);
unlockView.setVisibility(View.VISIBLE);
lockScreenButton.setVisibility(View.VISIBLE);
} else {
passphraseAuthContainer.setVisibility(View.VISIBLE);
fingerprintPrompt.setVisibility(View.GONE);
unlockView.setVisibility(View.GONE);
lockScreenButton.setVisibility(View.GONE);
}
}
@@ -360,12 +353,32 @@ public class PassphrasePromptActivity extends PassphraseActivity {
System.gc();
}
private void showHelpDialog() {
lockScreenButton.setText(R.string.prompt_passphrase_activity__try_again);
learnMoreText.setVisibility(View.VISIBLE);
learnMoreText.setLearnMoreVisible(true);
learnMoreText.setLinkColor(ContextCompat.getColor(PassphrasePromptActivity.this, R.color.signal_colorPrimary));
learnMoreText.setOnClickListener(v ->
new MaterialAlertDialogBuilder(PassphrasePromptActivity.this)
.setTitle(R.string.prompt_passphrase_activity__unlock_signal)
.setMessage(R.string.prompt_passphrase_activity__screen_lock_is_on)
.setCancelable(true)
.setPositiveButton(android.R.string.ok, null)
.setNegativeButton(R.string.prompt_passphrase_activity__contact_support, (d,w) -> sendEmailToSupport())
.show()
);
}
private class BiometricAuthenticationListener extends BiometricPrompt.AuthenticationCallback {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errorString) {
Log.w(TAG, "Authentication error: " + errorCode);
hadFailure = true;
showHelpDialog();
if (errorCode != BiometricPrompt.ERROR_CANCELED && errorCode != BiometricPrompt.ERROR_USER_CANCELED) {
onAuthenticationFailed();
}
@@ -374,41 +387,21 @@ public class PassphrasePromptActivity extends PassphraseActivity {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
Log.i(TAG, "onAuthenticationSucceeded");
fingerprintPrompt.setImageResource(R.drawable.symbol_check_white_48);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() {
lockScreenButton.setOnClickListener(null);
unlockView.addAnimatorListener(new AnimationCompleteListener() {
@Override
public void onAnimationEnd(Animator animation) {
handleAuthenticated();
}
}).start();
});
unlockView.playAnimation();
}
@Override
public void onAuthenticationFailed() {
Log.w(TAG, "onAuthenticationFailed()");
fingerprintPrompt.setImageResource(R.drawable.symbol_x_white_48);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
TranslateAnimation shake = new TranslateAnimation(0, 30, 0, 0);
shake.setDuration(50);
shake.setRepeatCount(7);
shake.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
fingerprintPrompt.setImageResource(R.drawable.ic_fingerprint_white_48dp);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.signal_accent_primary), PorterDuff.Mode.SRC_IN);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
fingerprintPrompt.startAnimation(shake);
showHelpDialog();
}
}
}

View File

@@ -9,6 +9,7 @@ import android.os.Bundle;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import org.greenrobot.eventbus.EventBus;
@@ -17,7 +18,7 @@ import org.signal.core.util.tracing.Tracer;
import org.signal.devicetransfer.TransferStatus;
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberLockActivity;
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
@@ -27,9 +28,12 @@ import org.thoughtcrime.securesms.pin.PinRestoreActivity;
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.registration.ui.RegistrationActivity;
import org.thoughtcrime.securesms.restore.RestoreActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
@@ -51,6 +55,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_TRANSFER_ONGOING = 8;
private static final int STATE_TRANSFER_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private static final int STATE_TRANSFER_OR_RESTORE = 11;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -59,7 +64,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
protected final void onCreate(Bundle savedInstanceState) {
Tracer.getInstance().start(Log.tag(getClass()) + "#onCreate()");
AppStartup.getInstance().onCriticalRenderEventStart();
this.networkAccess = ApplicationDependencies.getSignalServiceNetworkAccess();
this.networkAccess = AppDependencies.getSignalServiceNetworkAccess();
onPreCreate();
final boolean locked = KeyCachingService.isLocked(this);
@@ -88,8 +93,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
@Override
public void onMasterSecretCleared() {
Log.d(TAG, "onMasterSecretCleared()");
if (ApplicationDependencies.getAppForegroundObserver().isForegrounded()) routeApplicationState(true);
else finish();
if (AppForegroundObserver.isForegrounded()) routeApplicationState(true);
else finish();
}
protected <T extends Fragment> T initFragment(@IdRes int target,
@@ -125,8 +130,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private void routeApplicationState(boolean locked) {
Intent intent = getIntentForState(getApplicationState(locked));
final int applicationState = getApplicationState(locked);
Intent intent = getIntentForState(applicationState);
if (intent != null) {
Log.d(TAG, "routeApplicationState(), intent: " + intent.getComponent());
startActivity(intent);
finish();
}
@@ -146,6 +153,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
case STATE_TRANSFER_OR_RESTORE: return getTransferOrRestoreIntent();
default: return null;
}
}
@@ -161,8 +169,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.storageService().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userHasSkippedOrForgottenPin()) {
return STATE_CREATE_SIGNAL_PIN;
} else if (userCanTransferOrRestore()) {
return STATE_TRANSFER_OR_RESTORE;
} else if (userMustSetProfileName()) {
return STATE_CREATE_PROFILE_NAME;
} else if (userMustCreateSignalPin()) {
@@ -178,16 +186,20 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
}
private boolean userCanTransferOrRestore() {
return !SignalStore.registration().isRegistrationComplete() && RemoteConfig.restoreAfterRegistration() && !SignalStore.registration().hasSkippedTransferOrRestore() && !SignalStore.registration().hasCompletedRestore();
}
private boolean userMustCreateSignalPin() {
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().lastPinCreateFailed() && !SignalStore.svr().hasOptedOut();
return !SignalStore.registration().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().lastPinCreateFailed() && !SignalStore.svr().hasOptedOut();
}
private boolean userHasSkippedOrForgottenPin() {
return !SignalStore.registrationValues().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().hasOptedOut() && SignalStore.svr().isPinForgottenOrSkipped();
return !SignalStore.registration().isRegistrationComplete() && !SignalStore.svr().hasPin() && !SignalStore.svr().hasOptedOut() && SignalStore.svr().isPinForgottenOrSkipped();
}
private boolean userMustSetProfileName() {
return !SignalStore.registrationValues().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
return !SignalStore.registration().isRegistrationComplete() && Recipient.self().getProfileName().isEmpty();
}
private Intent getCreatePassphraseIntent() {
@@ -196,7 +208,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private Intent getPromptPassphraseIntent() {
Intent intent = getRoutedIntent(PassphrasePromptActivity.class, getIntent());
intent.putExtra(PassphrasePromptActivity.FROM_FOREGROUND, ApplicationDependencies.getAppForegroundObserver().isForegrounded());
intent.putExtra(PassphrasePromptActivity.FROM_FOREGROUND, AppForegroundObserver.isForegrounded());
return intent;
}
@@ -208,7 +220,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private Intent getPushRegistrationIntent() {
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
return RegistrationActivity.newIntentForNewRegistration(this, getIntent());
}
private Intent getEnterSignalPinIntent() {
@@ -227,6 +239,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return getRoutedIntent(CreateSvrPinActivity.class, intent);
}
private Intent getTransferOrRestoreIntent() {
Intent intent = RestoreActivity.getIntentForTransferOrRestore(this);
return getRoutedIntent(intent, MainActivity.clearTop(this));
}
private Intent getCreateProfileNameIntent() {
Intent intent = CreateProfileActivity.getIntentForUserProfile(this);
return getRoutedIntent(intent, getIntent());
@@ -250,13 +267,13 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private Intent getRoutedIntent(Intent destination, @Nullable Intent nextIntent) {
if (nextIntent != null) destination.putExtra("next_intent", nextIntent);
if (nextIntent != null) destination.putExtra(NEXT_INTENT_EXTRA, nextIntent);
return destination;
}
private Intent getRoutedIntent(Class<?> destination, @Nullable Intent nextIntent) {
final Intent intent = new Intent(this, destination);
if (nextIntent != null) intent.putExtra("next_intent", nextIntent);
if (nextIntent != null) intent.putExtra(NEXT_INTENT_EXTRA, nextIntent);
return intent;
}
@@ -269,15 +286,15 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
this.clearKeyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "onReceive() for clear key event. PasswordDisabled: " + TextSecurePreferences.isPasswordDisabled(context) + ", ScreenLock: " + TextSecurePreferences.isScreenLockEnabled(context));
if (TextSecurePreferences.isScreenLockEnabled(context) || !TextSecurePreferences.isPasswordDisabled(context)) {
Log.i(TAG, "onReceive() for clear key event. PasswordDisabled: " + SignalStore.settings().getPassphraseDisabled() + ", ScreenLock: " + SignalStore.settings().getScreenLockEnabled());
if (SignalStore.settings().getScreenLockEnabled() || !SignalStore.settings().getPassphraseDisabled()) {
onMasterSecretCleared();
}
}
};
IntentFilter filter = new IntentFilter(KeyCachingService.CLEAR_KEY_EVENT);
registerReceiver(clearKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
ContextCompat.registerReceiver(this, clearKeyReceiver, filter, KeyCachingService.KEY_PERMISSION, null, ContextCompat.RECEIVER_NOT_EXPORTED);
}
private void removeClearKeyReceiver(Context context) {

View File

@@ -22,13 +22,16 @@ import android.annotation.SuppressLint;
import android.app.PictureInPictureParams;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.media.AudioManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Pair;
import android.util.Rational;
import android.view.Surface;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
@@ -37,7 +40,10 @@ import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.LiveDataReactiveStreams;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModelProvider;
import androidx.window.java.layout.WindowInfoTrackerCallbackAdapter;
import androidx.window.layout.DisplayFeature;
@@ -56,11 +62,12 @@ import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.thoughtcrime.securesms.components.TooltipPopup;
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.components.webrtc.CallLinkProfileKeySender;
import org.thoughtcrime.securesms.components.webrtc.CallOverflowPopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
import org.thoughtcrime.securesms.components.webrtc.CallReactionScrubber;
import org.thoughtcrime.securesms.components.webrtc.CallStateUpdatePopupWindow;
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
@@ -77,8 +84,12 @@ import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoCont
import org.thoughtcrime.securesms.components.webrtc.controls.ControlsAndInfoViewModel;
import org.thoughtcrime.securesms.components.webrtc.participantslist.CallParticipantsListDialog;
import org.thoughtcrime.securesms.components.webrtc.requests.CallLinkIncomingRequestSheet;
import org.thoughtcrime.securesms.components.webrtc.v2.CallControlsChange;
import org.thoughtcrime.securesms.components.webrtc.v2.CallEvent;
import org.thoughtcrime.securesms.components.webrtc.v2.CallIntent;
import org.thoughtcrime.securesms.components.webrtc.v2.CallPermissionsDialogController;
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.messagerequests.CalleeMustAcceptMessageRequestActivity;
import org.thoughtcrime.securesms.permissions.Permissions;
@@ -91,8 +102,8 @@ import org.thoughtcrime.securesms.service.webrtc.SignalCallManager;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.FullscreenHelper;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.Util;
@@ -105,6 +116,7 @@ import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@@ -122,28 +134,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private static final int STANDARD_DELAY_FINISH = 1000;
private static final int VIBRATE_DURATION = 50;
/**
* ANSWER the call via voice-only.
*/
public static final String ANSWER_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_ACTION";
/**
* ANSWER the call via video.
*/
public static final String ANSWER_VIDEO_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".ANSWER_VIDEO_ACTION";
public static final String DENY_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".DENY_ACTION";
public static final String END_CALL_ACTION = WebRtcCallActivity.class.getCanonicalName() + ".END_CALL_ACTION";
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN";
public static final String EXTRA_STARTED_FROM_CALL_LINK = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
public static final String EXTRA_LAUNCH_IN_PIP = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
private CallStateUpdatePopupWindow callStateUpdatePopupWindow;
private CallOverflowPopupWindow callOverflowPopupWindow;
private WifiToCellularPopupWindow wifiToCellularPopupWindow;
private DeviceOrientationMonitor deviceOrientationMonitor;
private FullscreenHelper fullscreenHelper;
private WebRtcCallView callScreen;
@@ -162,8 +156,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private ControlsAndInfoController controlsAndInfo;
private boolean enterPipOnResume;
private long lastProcessedIntentTimestamp;
private Disposable ephemeralStateDisposable = Disposable.empty();
private WebRtcViewModel previousEvent = null;
private Disposable ephemeralStateDisposable = Disposable.empty();
private CallPermissionsDialogController callPermissionsDialogController = new CallPermissionsDialogController();
@Override
protected void attachBaseContext(@NonNull Context newBase) {
@@ -171,10 +166,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
super.attachBaseContext(newBase);
}
@SuppressLint({ "SourceLockedOrientationActivity", "MissingInflatedId" })
@SuppressLint({ "MissingInflatedId" })
@Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, "onCreate(" + getIntent().getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")");
CallIntent callIntent = getCallIntent();
Log.i(TAG, "onCreate(" + callIntent.isStartedFromFullScreen() + ")");
lifecycleDisposable = new LifecycleDisposable();
lifecycleDisposable.bindTo(this);
@@ -183,11 +179,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
super.onCreate(savedInstanceState);
boolean isLandscapeEnabled = getResources().getConfiguration().smallestScreenWidthDp >= 480;
if (!isLandscapeEnabled) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.webrtc_call_activity);
@@ -196,7 +187,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
initializeResources();
initializeViewModel(isLandscapeEnabled);
initializeViewModel();
initializePictureInPictureParams();
controlsAndInfo = new ControlsAndInfoController(this, callScreen, callOverflowPopupWindow, viewModel, controlsAndInfoViewModel);
@@ -209,18 +200,20 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
lifecycleDisposable.add(controlsAndInfo);
logIntent(getIntent());
logIntent(callIntent);
if (ANSWER_VIDEO_ACTION.equals(getIntent().getAction())) {
if (callIntent.getAction() == CallIntent.Action.ANSWER_VIDEO) {
enableVideoIfAvailable = true;
} else if (ANSWER_ACTION.equals(getIntent().getAction()) || getIntent().getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false)) {
} else if (callIntent.getAction() == CallIntent.Action.ANSWER_AUDIO || callIntent.isStartedFromFullScreen()) {
enableVideoIfAvailable = false;
} else {
enableVideoIfAvailable = getIntent().getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false);
getIntent().removeExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE);
enableVideoIfAvailable = callIntent.shouldEnableVideoIfAvailable();
callIntent.setShouldEnableVideoIfAvailable(false);
}
processIntent(getIntent());
processIntent(callIntent);
registerSystemPipChangeListeners();
windowLayoutInfoConsumer = new WindowLayoutInfoConsumer();
@@ -232,16 +225,38 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
initializePendingParticipantFragmentListener();
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface));
if (!hasCameraPermission() & !hasAudioPermission()) {
askCameraAudioPermissions(() -> {
callScreen.setMicEnabled(viewModel.getMicrophoneEnabled().getValue());
handleSetMuteVideo(false);
});
} else if (!hasAudioPermission()) {
askAudioPermissions(() -> callScreen.setMicEnabled(viewModel.getMicrophoneEnabled().getValue()));
}
}
private void registerSystemPipChangeListeners() {
addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfo -> {
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
CallReactionScrubber.dismissCustomEmojiBottomSheet(getSupportFragmentManager());
});
}
@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
controlsAndInfo.onStateRestored();
}
@Override
protected void onStart() {
super.onStart();
ephemeralStateDisposable = ApplicationDependencies.getSignalCallManager()
.ephemeralStates()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(viewModel::updateFromEphemeralState);
ephemeralStateDisposable = AppDependencies.getSignalCallManager()
.ephemeralStates()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(viewModel::updateFromEphemeralState);
}
@Override
@@ -276,10 +291,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onNewIntent(Intent intent) {
Log.i(TAG, "onNewIntent(" + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false) + ")");
CallIntent callIntent = getCallIntent();
Log.i(TAG, "onNewIntent(" + callIntent.isStartedFromFullScreen() + ")");
super.onNewIntent(intent);
logIntent(intent);
processIntent(intent);
logIntent(callIntent);
processIntent(callIntent);
}
@Override
@@ -287,7 +303,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
Log.i(TAG, "onPause");
super.onPause();
if (!viewModel.isCallStarting()) {
if (!callPermissionsDialogController.isAskingForPermission() && !viewModel.isCallStarting() && !isChangingConfigurations()) {
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
finish();
@@ -307,15 +323,15 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
requestNewSizesThrottle.clear();
}
ApplicationDependencies.getSignalCallManager().setEnableVideo(false);
AppDependencies.getSignalCallManager().setEnableVideo(false);
if (!viewModel.isCallStarting()) {
if (!viewModel.isCallStarting() && !isChangingConfigurations()) {
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null) {
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
AppDependencies.getSignalCallManager().cancelPreJoin();
} else if (state.getCallState().getInOngoingCall() && isInPipMode()) {
ApplicationDependencies.getSignalCallManager().relaunchPipOnForeground();
AppDependencies.getSignalCallManager().relaunchPipOnForeground();
}
}
}
@@ -323,6 +339,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
protected void onDestroy() {
Log.d(TAG, "onDestroy");
super.onDestroy();
windowInfoTrackerCallbackAdapter.removeWindowLayoutInfoListener(windowLayoutInfoConsumer);
EventBus.getDefault().unregister(this);
@@ -336,6 +353,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
protected void onUserLeaveHint() {
super.onUserLeaveHint();
enterPipModeIfPossible();
}
@@ -346,6 +364,10 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
}
private @NonNull CallIntent getCallIntent() {
return new CallIntent(getIntent());
}
private boolean enterPipModeIfPossible() {
if (isSystemPipEnabledAndAvailable()) {
if (viewModel.canEnterPipMode()) {
@@ -356,8 +378,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
return false;
}
CallParticipantsListDialog.dismiss(getSupportFragmentManager());
return true;
}
if (Build.VERSION.SDK_INT >= 31) {
@@ -371,33 +391,27 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
return isSystemPipEnabledAndAvailable() && isInPictureInPictureMode();
}
private void logIntent(@NonNull Intent intent) {
Log.d(TAG, "Intent: Action: " + intent.getAction());
Log.d(TAG, "Intent: EXTRA_STARTED_FROM_FULLSCREEN: " + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false));
Log.d(TAG, "Intent: EXTRA_ENABLE_VIDEO_IF_AVAILABLE: " + intent.getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false));
Log.d(TAG, "Intent: EXTRA_LAUNCH_IN_PIP: " + intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false));
private void logIntent(@NonNull CallIntent intent) {
Log.d(TAG, intent.toString());
}
private void processIntent(@NonNull Intent intent) {
if (ANSWER_ACTION.equals(intent.getAction())) {
handleAnswerWithAudio();
} else if (ANSWER_VIDEO_ACTION.equals(intent.getAction())) {
handleAnswerWithVideo();
} else if (DENY_ACTION.equals(intent.getAction())) {
handleDenyCall();
} else if (END_CALL_ACTION.equals(intent.getAction())) {
handleEndCall();
private void processIntent(@NonNull CallIntent intent) {
switch (intent.getAction()) {
case ANSWER_AUDIO -> handleAnswerWithAudio();
case ANSWER_VIDEO -> handleAnswerWithVideo();
case DENY -> handleDenyCall();
case END_CALL -> handleEndCall();
}
if (System.currentTimeMillis() - lastProcessedIntentTimestamp > TimeUnit.SECONDS.toMillis(1)) {
enterPipOnResume = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false);
enterPipOnResume = intent.shouldLaunchInPip();
}
lastProcessedIntentTimestamp = System.currentTimeMillis();
}
private void initializePendingParticipantFragmentListener() {
if (!FeatureFlags.adHocCalling()) {
if (!RemoteConfig.adHocCalling()) {
return;
}
@@ -422,7 +436,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.WebRtcCallActivity__approve_all, (dialog, which) -> {
for (RecipientId id : recipientIds) {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id);
AppDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(id);
}
})
.show();
@@ -434,7 +448,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.WebRtcCallActivity__deny_all, (dialog, which) -> {
for (RecipientId id : recipientIds) {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id);
AppDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(id);
}
})
.show();
@@ -466,16 +480,34 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
return state.getLocalParticipant().isHandRaised();
});
getLifecycle().addObserver(participantUpdateWindow);
}
private void initializeViewModel(boolean isLandscapeEnabled) {
deviceOrientationMonitor = new DeviceOrientationMonitor(this);
getLifecycle().addObserver(deviceOrientationMonitor);
private @NonNull Orientation resolveOrientationFromContext() {
int displayOrientation = getResources().getConfiguration().orientation;
int displayRotation = ContextCompat.getDisplayOrDefault(this).getRotation();
WebRtcCallViewModel.Factory factory = new WebRtcCallViewModel.Factory(deviceOrientationMonitor);
if (displayOrientation == Configuration.ORIENTATION_PORTRAIT) {
return Orientation.PORTRAIT_BOTTOM_EDGE;
} else if (displayRotation == Surface.ROTATION_270) {
return Orientation.LANDSCAPE_RIGHT_EDGE;
} else {
return Orientation.LANDSCAPE_LEFT_EDGE;
}
}
viewModel = new ViewModelProvider(this, factory).get(WebRtcCallViewModel.class);
viewModel.setIsLandscapeEnabled(isLandscapeEnabled);
private void initializeViewModel() {
final Orientation orientation = resolveOrientationFromContext();
if (orientation == PORTRAIT_BOTTOM_EDGE) {
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface2));
WindowUtil.clearTranslucentNavigationBar(getWindow());
}
LiveData<Pair<Orientation, Boolean>> orientationAndLandscapeEnabled = Transformations.map(new MutableLiveData<>(orientation), o -> Pair.create(o, true));
viewModel = new ViewModelProvider(this).get(WebRtcCallViewModel.class);
viewModel.setIsLandscapeEnabled(true);
viewModel.setIsInPipMode(isInPipMode());
viewModel.getMicrophoneEnabled().observe(this, callScreen::setMicEnabled);
viewModel.getWebRtcControls().observe(this, controls -> {
@@ -486,9 +518,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
lifecycleDisposable.add(viewModel.getInCallstatus().subscribe(this::handleInCallStatus));
boolean isStartedFromCallLink = getIntent().getBooleanExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, false);
boolean isStartedFromCallLink = getCallIntent().isStartedFromCallLink();
LiveDataUtil.combineLatest(LiveDataReactiveStreams.fromPublisher(viewModel.getCallParticipantsState().toFlowable(BackpressureStrategy.LATEST)),
viewModel.getOrientationAndLandscapeEnabled(),
orientationAndLandscapeEnabled,
viewModel.getEphemeralState(),
(s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second, isStartedFromCallLink))
.observe(this, p -> callScreen.updateCallParticipants(p));
@@ -502,13 +534,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null) {
if (state.needsNewRequestSizes()) {
requestNewSizesThrottle.publish(() -> ApplicationDependencies.getSignalCallManager().updateRenderedResolutions());
requestNewSizesThrottle.publish(() -> AppDependencies.getSignalCallManager().updateRenderedResolutions());
}
}
});
viewModel.getOrientationAndLandscapeEnabled().observe(this, pair -> ApplicationDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
viewModel.getControlsRotation().observe(this, callScreen::rotateControls);
orientationAndLandscapeEnabled.observe(this, pair -> AppDependencies.getSignalCallManager().orientationChanged(pair.second, pair.first.getDegrees()));
addOnPictureInPictureModeChangedListener(info -> {
viewModel.setIsInPipMode(info.isInPictureInPictureMode());
@@ -531,8 +562,17 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void initializePictureInPictureParams() {
if (isSystemPipEnabledAndAvailable()) {
final Orientation orientation = resolveOrientationFromContext();
final Rational aspectRatio;
if (orientation == PORTRAIT_BOTTOM_EDGE) {
aspectRatio = new Rational(9, 16);
} else {
aspectRatio = new Rational(16, 9);
}
pipBuilderParams = new PictureInPictureParams.Builder();
pipBuilderParams.setAspectRatio(new Rational(9, 16));
pipBuilderParams.setAspectRatio(aspectRatio);
if (Build.VERSION.SDK_INT >= 31) {
pipBuilderParams.setAutoEnterEnabled(true);
}
@@ -546,18 +586,18 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
}
private void handleViewModelEvent(@NonNull WebRtcCallViewModel.Event event) {
if (event instanceof WebRtcCallViewModel.Event.StartCall) {
startCall(((WebRtcCallViewModel.Event.StartCall) event).isVideoCall());
private void handleViewModelEvent(@NonNull CallEvent event) {
if (event instanceof CallEvent.StartCall) {
startCall(((CallEvent.StartCall) event).isVideoCall());
return;
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
SafetyNumberBottomSheet.forGroupCall(((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords())
} else if (event instanceof CallEvent.ShowGroupCallSafetyNumberChange) {
SafetyNumberBottomSheet.forGroupCall(((CallEvent.ShowGroupCallSafetyNumberChange) event).getIdentityRecords())
.show(getSupportFragmentManager());
return;
} else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) {
} else if (event instanceof CallEvent.SwitchToSpeaker) {
callScreen.switchToSpeakerView();
return;
} else if (event instanceof WebRtcCallViewModel.Event.ShowSwipeToSpeakerHint) {
} else if (event instanceof CallEvent.ShowSwipeToSpeakerHint) {
CallToastPopupWindow.show(callScreen);
return;
}
@@ -566,7 +606,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
return;
}
if (event instanceof WebRtcCallViewModel.Event.ShowVideoTooltip) {
if (event instanceof CallEvent.ShowVideoTooltip) {
if (videoTooltip == null) {
videoTooltip = TooltipPopup.forTarget(callScreen.getVideoTooltipTarget())
.setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
@@ -575,14 +615,14 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
.setOnDismissListener(() -> viewModel.onDismissedVideoTooltip())
.show(TooltipPopup.POSITION_ABOVE);
}
} else if (event instanceof WebRtcCallViewModel.Event.DismissVideoTooltip) {
} else if (event instanceof CallEvent.DismissVideoTooltip) {
if (videoTooltip != null) {
videoTooltip.dismiss();
videoTooltip = null;
}
} else if (event instanceof WebRtcCallViewModel.Event.ShowWifiToCellularPopup) {
} else if (event instanceof CallEvent.ShowWifiToCellularPopup) {
wifiToCellularPopupWindow.show();
} else if (event instanceof WebRtcCallViewModel.Event.ShowSwitchCameraTooltip) {
} else if (event instanceof CallEvent.ShowSwitchCameraTooltip) {
if (switchCameraTooltip == null) {
switchCameraTooltip = TooltipPopup.forTarget(callScreen.getSwitchCameraTooltipTarget())
.setBackgroundTint(ContextCompat.getColor(this, R.color.core_ultramarine))
@@ -591,7 +631,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
.setOnDismissListener(() -> viewModel.onDismissedSwitchCameraTooltip())
.show(TooltipPopup.POSITION_ABOVE);
}
} else if (event instanceof WebRtcCallViewModel.Event.DismissSwitchCameraTooltip) {
} else if (event instanceof CallEvent.DismissSwitchCameraTooltip) {
if (switchCameraTooltip != null) {
switchCameraTooltip.dismiss();
switchCameraTooltip = null;
@@ -633,83 +673,66 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void handleSetAudioHandset() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.EARPIECE));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.EARPIECE));
}
private void handleSetAudioSpeaker() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.SPEAKER_PHONE));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.SPEAKER_PHONE));
}
private void handleSetAudioBluetooth() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.BLUETOOTH));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.BLUETOOTH));
}
private void handleSetAudioWiredHeadset() {
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.WIRED_HEADSET));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(SignalAudioManager.AudioDevice.WIRED_HEADSET));
}
private void handleSetMuteAudio(boolean enabled) {
ApplicationDependencies.getSignalCallManager().setMuteAudio(enabled);
AppDependencies.getSignalCallManager().setMuteAudio(enabled);
}
private void handleSetMuteVideo(boolean muted) {
Recipient recipient = viewModel.getRecipient().get();
if (!recipient.equals(Recipient.UNKNOWN)) {
String recipientDisplayName = recipient.getDisplayName(this);
Permissions.with(this)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName), R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName))
.onAllGranted(() -> ApplicationDependencies.getSignalCallManager().setEnableVideo(!muted))
.execute();
Runnable onGranted = () -> AppDependencies.getSignalCallManager().setEnableVideo(!muted);
askCameraPermissions(onGranted);
}
}
private void handleFlipCamera() {
ApplicationDependencies.getSignalCallManager().flipCamera();
AppDependencies.getSignalCallManager().flipCamera();
}
private void handleAnswerWithAudio() {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone),
R.drawable.ic_mic_solid_24)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
ApplicationDependencies.getSignalCallManager().acceptCall(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
Runnable onGranted = () -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
AppDependencies.getSignalCallManager().acceptCall(false);
};
askAudioPermissions(onGranted);
}
private void handleAnswerWithVideo() {
Permissions.with(this)
.request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone_and_camera), R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted)
.withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls))
.onAllGranted(() -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
ApplicationDependencies.getSignalCallManager().acceptCall(true);
handleSetMuteVideo(false);
})
.onAnyDenied(this::handleDenyCall)
.execute();
Runnable onGranted = () -> {
callScreen.setStatus(getString(R.string.RedPhone_answering));
AppDependencies.getSignalCallManager().acceptCall(true);
handleSetMuteVideo(false);
};
if (!hasCameraPermission() &!hasAudioPermission()) {
askCameraAudioPermissions(onGranted);
} else if (!hasAudioPermission()) {
askAudioPermissions(onGranted);
} else {
askCameraPermissions(onGranted);
}
}
private void handleDenyCall() {
Recipient recipient = viewModel.getRecipient().get();
if (!recipient.equals(Recipient.UNKNOWN)) {
ApplicationDependencies.getSignalCallManager().denyCall();
AppDependencies.getSignalCallManager().denyCall();
callScreen.setRecipient(recipient);
callScreen.setStatus(getString(R.string.RedPhone_ending_call));
@@ -719,7 +742,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void handleEndCall() {
Log.i(TAG, "Hangup pressed, handling termination now...");
ApplicationDependencies.getSignalCallManager().localHangup();
AppDependencies.getSignalCallManager().localHangup();
}
private void handleOutgoingCall(@NonNull WebRtcViewModel event) {
@@ -817,13 +840,13 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private void updateGroupMembersForGroupCall() {
ApplicationDependencies.getSignalCallManager().requestUpdateGroupMembers();
AppDependencies.getSignalCallManager().requestUpdateGroupMembers();
}
public void handleGroupMemberCountChange(int count) {
boolean canRing = count <= FeatureFlags.maxGroupCallRingSize();
boolean canRing = count <= RemoteConfig.maxGroupCallRingSize();
callScreen.enableRingGroup(canRing);
ApplicationDependencies.getSignalCallManager().setRingGroup(canRing);
AppDependencies.getSignalCallManager().setRingGroup(canRing);
}
private void updateSpeakerHint(boolean showSpeakerHint) {
@@ -847,7 +870,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
if (state.getGroupCallState().isConnected()) {
ApplicationDependencies.getSignalCallManager().groupApproveSafetyChange(changedRecipients);
AppDependencies.getSignalCallManager().groupApproveSafetyChange(changedRecipients);
} else {
viewModel.startCall(state.getLocalParticipant().isVideoEnabled());
}
@@ -861,7 +884,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null && state.getGroupCallState().isNotIdle()) {
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
AppDependencies.getSignalCallManager().cancelPreJoin();
finish();
} else {
handleEndCall();
@@ -885,7 +908,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event);
Log.i(TAG, "Got message from service: " + event.describeDifference(previousEvent));
previousEvent = event;
viewModel.setRecipient(event.getRecipient());
callScreen.setRecipient(event.getRecipient());
@@ -980,18 +1004,53 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
callScreen.setRingGroup(event.shouldRingGroup());
if (event.shouldRingGroup() && event.areRemoteDevicesInCall()) {
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
AppDependencies.getSignalCallManager().setRingGroup(false);
}
}
}
private boolean hasCameraPermission() {
return Permissions.hasAll(this, Manifest.permission.CAMERA);
}
private boolean hasAudioPermission() {
return Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO);
}
private void askCameraPermissions(@NonNull Runnable onGranted) {
callPermissionsDialogController.requestCameraPermission(
this,
() -> {
onGranted.run();
findViewById(R.id.missing_permissions_container).setVisibility(View.GONE);
}
);
}
private void askAudioPermissions(@NonNull Runnable onGranted) {
callPermissionsDialogController.requestAudioPermission(
this,
onGranted,
this::handleDenyCall
);
}
public void askCameraAudioPermissions(@NonNull Runnable onGranted) {
callPermissionsDialogController.requestCameraAndAudioPermission(
this,
onGranted,
() -> findViewById(R.id.missing_permissions_container).setVisibility(View.GONE),
this::handleDenyCall
);
}
private void startCall(boolean isVideoCall) {
enableVideoIfAvailable = isVideoCall;
if (isVideoCall) {
ApplicationDependencies.getSignalCallManager().startOutgoingVideoCall(viewModel.getRecipient().get());
AppDependencies.getSignalCallManager().startOutgoingVideoCall(viewModel.getRecipient().get());
} else {
ApplicationDependencies.getSignalCallManager().startOutgoingAudioCall(viewModel.getRecipient().get());
AppDependencies.getSignalCallManager().startOutgoingAudioCall(viewModel.getRecipient().get());
}
MessageSender.onMessageSent();
@@ -1002,7 +1061,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onReactWithAnyEmojiSelected(@NonNull String emoji) {
ApplicationDependencies.getSignalCallManager().react(emoji);
AppDependencies.getSignalCallManager().react(emoji);
callOverflowPopupWindow.dismiss();
}
@@ -1021,11 +1080,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void toggleControls() {
WebRtcControls controlState = viewModel.getWebRtcControls().getValue();
if (controlState != null && !controlState.displayIncomingCallButtons()) {
if (controlState != null && !controlState.displayIncomingCallButtons() && !controlState.displayErrorControls()) {
controlsAndInfo.toggleControls();
}
}
@Override
public void onAudioPermissionsRequested(Runnable onGranted) {
askAudioPermissions(onGranted);
}
@Override
public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) {
maybeDisplaySpeakerphonePopup(audioOutput);
@@ -1051,7 +1115,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onAudioOutputChanged31(@NonNull WebRtcAudioDevice audioOutput) {
maybeDisplaySpeakerphonePopup(audioOutput.getWebRtcAudioOutput());
ApplicationDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId()));
AppDependencies.getSignalCallManager().selectAudioDevice(new SignalAudioManager.ChosenAudioDeviceIdentifier(audioOutput.getDeviceId()));
}
@Override
@@ -1061,9 +1125,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onMicChanged(boolean isMicEnabled) {
callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallStateUpdatePopupWindow.CallStateUpdate.MIC_ON
: CallStateUpdatePopupWindow.CallStateUpdate.MIC_OFF);
handleSetMuteAudio(!isMicEnabled);
Runnable onGranted = () -> {
callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallControlsChange.MIC_ON
: CallControlsChange.MIC_OFF);
handleSetMuteAudio(!isMicEnabled);
};
askAudioPermissions(onGranted);
}
@Override
@@ -1114,12 +1181,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onRingGroupChanged(boolean ringGroup, boolean ringingAllowed) {
if (ringingAllowed) {
ApplicationDependencies.getSignalCallManager().setRingGroup(ringGroup);
callStateUpdatePopupWindow.onCallStateUpdate(ringGroup ? CallStateUpdatePopupWindow.CallStateUpdate.RINGING_ON
: CallStateUpdatePopupWindow.CallStateUpdate.RINGING_OFF);
AppDependencies.getSignalCallManager().setRingGroup(ringGroup);
callStateUpdatePopupWindow.onCallStateUpdate(ringGroup ? CallControlsChange.RINGING_ON
: CallControlsChange.RINGING_OFF);
} else {
ApplicationDependencies.getSignalCallManager().setRingGroup(false);
callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.RINGING_DISABLED);
AppDependencies.getSignalCallManager().setRingGroup(false);
callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.RINGING_DISABLED);
}
}
@@ -1137,9 +1204,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private void maybeDisplaySpeakerphonePopup(WebRtcAudioOutput nextOutput) {
final WebRtcAudioOutput currentOutput = viewModel.getCurrentAudioOutput();
if (currentOutput == WebRtcAudioOutput.SPEAKER && nextOutput != WebRtcAudioOutput.SPEAKER) {
callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.SPEAKER_OFF);
callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.SPEAKER_OFF);
} else if (currentOutput != WebRtcAudioOutput.SPEAKER && nextOutput == WebRtcAudioOutput.SPEAKER) {
callStateUpdatePopupWindow.onCallStateUpdate(CallStateUpdatePopupWindow.CallStateUpdate.SPEAKER_ON);
callStateUpdatePopupWindow.onCallStateUpdate(CallControlsChange.SPEAKER_ON);
}
}
@@ -1147,12 +1214,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onAllowPendingRecipient(@NonNull Recipient pendingRecipient) {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId());
AppDependencies.getSignalCallManager().setCallLinkJoinRequestAccepted(pendingRecipient.getId());
}
@Override
public void onRejectPendingRecipient(@NonNull Recipient pendingRecipient) {
ApplicationDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId());
AppDependencies.getSignalCallManager().setCallLinkJoinRequestRejected(pendingRecipient.getId());
}
@Override
@@ -1173,8 +1240,6 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
Log.d(TAG, "On WindowLayoutInfo accepted: " + windowLayoutInfo.toString());
Optional<DisplayFeature> feature = windowLayoutInfo.getDisplayFeatures().stream().filter(f -> f instanceof FoldingFeature).findFirst();
viewModel.setIsLandscapeEnabled(feature.isPresent());
setRequestedOrientation(feature.isPresent() ? ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED : ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
if (feature.isPresent()) {
FoldingFeature foldingFeature = (FoldingFeature) feature.get();
Rect bounds = foldingFeature.getBounds();
@@ -1198,9 +1263,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Override
public void onHidden() {
fullscreenHelper.hideSystemUI();
if (videoTooltip != null) {
videoTooltip.dismiss();
WebRtcControls controlState = viewModel.getWebRtcControls().getValue();
if (controlState == null || !controlState.displayErrorControls()) {
fullscreenHelper.hideSystemUI();
if (videoTooltip != null) {
videoTooltip.dismiss();
}
}
}
}

View File

@@ -21,6 +21,7 @@ class SignalBackupAgent : BackupAgent() {
)
override fun onBackup(oldState: ParcelFileDescriptor?, data: BackupDataOutput, newState: ParcelFileDescriptor) {
Log.i(TAG, "Performing backup to Android Backup Service.")
val contentsHash = cumulativeHashCode()
if (oldState == null) {
performBackup(data)
@@ -36,9 +37,11 @@ class SignalBackupAgent : BackupAgent() {
}
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(contentsHash) }
Log.i(TAG, "Backup finished.")
}
private fun performBackup(data: BackupDataOutput) {
Log.i(TAG, "Creating new backup data.")
items.forEach {
val backupData = it.getDataForBackup()
data.writeEntityHeader(it.getKey(), backupData.size)
@@ -54,6 +57,7 @@ class SignalBackupAgent : BackupAgent() {
items.find { dataInput.key == it.getKey() }?.restoreData(buffer)
}
DataOutputStream(FileOutputStream(newState.fileDescriptor)).use { it.writeInt(cumulativeHashCode()) }
Log.i(TAG, "Android Backup Service restore complete.")
}
private fun cumulativeHashCode(): Int {
@@ -61,6 +65,6 @@ class SignalBackupAgent : BackupAgent() {
}
companion object {
private const val TAG = "SignalBackupAgent"
private val TAG = Log.tag(SignalBackupAgent::class)
}
}

View File

@@ -17,19 +17,19 @@ object SvrAuthTokens : AndroidBackupItem {
}
override fun getDataForBackup(): ByteArray {
val proto = SvrAuthToken(tokens = SignalStore.svr().authTokenList)
val proto = SvrAuthToken(svr2Tokens = SignalStore.svr.svr2AuthTokens)
return proto.encode()
}
override fun restoreData(data: ByteArray) {
if (SignalStore.svr().authTokenList.isNotEmpty()) {
if (SignalStore.svr.svr2AuthTokens.isNotEmpty()) {
return
}
try {
val proto = SvrAuthToken.ADAPTER.decode(data)
SignalStore.svr().putAuthTokenList(proto.tokens)
SignalStore.svr.putSvr2AuthTokens(proto.svr2Tokens)
} catch (e: IOException) {
Log.w(TAG, "Cannot restore KbsAuthToken from backup service.")
}

View File

@@ -32,7 +32,7 @@ class ApkUpdateDownloadManagerReceiver : BroadcastReceiver() {
}
val downloadId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -2)
if (downloadId != SignalStore.apkUpdate().downloadId) {
if (downloadId != SignalStore.apkUpdate.downloadId) {
Log.w(TAG, "downloadId doesn't match the one we're waiting for! Ignoring.")
return
}

View File

@@ -14,9 +14,10 @@ import org.signal.core.util.PendingIntentFlags
import org.signal.core.util.StreamUtil
import org.signal.core.util.getDownloadManager
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobs.ApkUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.AppForegroundObserver
import org.thoughtcrime.securesms.util.Environment
import org.thoughtcrime.securesms.util.FileUtils
import java.io.FileInputStream
@@ -38,30 +39,30 @@ object ApkUpdateInstaller {
* [userInitiated] = true, and then everything installs.
*/
fun installOrPromptForInstall(context: Context, downloadId: Long, userInitiated: Boolean) {
if (downloadId != SignalStore.apkUpdate().downloadId) {
Log.w(TAG, "DownloadId doesn't match the one we're waiting for (current: $downloadId, expected: ${SignalStore.apkUpdate().downloadId})! We likely have newer data. Ignoring.")
if (downloadId != SignalStore.apkUpdate.downloadId) {
Log.w(TAG, "DownloadId doesn't match the one we're waiting for (current: $downloadId, expected: ${SignalStore.apkUpdate.downloadId})! We likely have newer data. Ignoring.")
ApkUpdateNotifications.dismissInstallPrompt(context)
ApplicationDependencies.getJobManager().add(ApkUpdateJob())
AppDependencies.jobManager.add(ApkUpdateJob())
return
}
val digest = SignalStore.apkUpdate().digest
val digest = SignalStore.apkUpdate.digest
if (digest == null) {
Log.w(TAG, "DownloadId matches, but digest is null! Inconsistent state. Failing and clearing state.")
SignalStore.apkUpdate().clearDownloadAttributes()
SignalStore.apkUpdate.clearDownloadAttributes()
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
return
}
if (!isMatchingDigest(context, downloadId, digest)) {
Log.w(TAG, "DownloadId matches, but digest does not! Bad download or inconsistent state. Failing and clearing state.")
SignalStore.apkUpdate().clearDownloadAttributes()
SignalStore.apkUpdate.clearDownloadAttributes()
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
return
}
if (!userInitiated && !shouldAutoUpdate()) {
Log.w(TAG, "Not user-initiated and not eligible for auto-update. Prompting. (API=${Build.VERSION.SDK_INT}, Foreground=${ApplicationDependencies.getAppForegroundObserver().isForegrounded}, AutoUpdate=${SignalStore.apkUpdate().autoUpdate})")
Log.w(TAG, "Not user-initiated and not eligible for auto-update. Prompting. (API=${Build.VERSION.SDK_INT}, Foreground=${AppForegroundObserver.isForegrounded()}, AutoUpdate=${SignalStore.apkUpdate.autoUpdate})")
ApkUpdateNotifications.showInstallPrompt(context, downloadId)
return
}
@@ -70,11 +71,11 @@ object ApkUpdateInstaller {
installApk(context, downloadId, userInitiated)
} catch (e: IOException) {
Log.w(TAG, "Hit IOException when trying to install APK!", e)
SignalStore.apkUpdate().clearDownloadAttributes()
SignalStore.apkUpdate.clearDownloadAttributes()
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
} catch (e: SecurityException) {
Log.w(TAG, "Hit SecurityException when trying to install APK!", e)
SignalStore.apkUpdate().clearDownloadAttributes()
SignalStore.apkUpdate.clearDownloadAttributes()
ApkUpdateNotifications.showInstallFailed(context, ApkUpdateNotifications.FailureReason.UNKNOWN)
}
}
@@ -145,6 +146,6 @@ object ApkUpdateInstaller {
private fun shouldAutoUpdate(): Boolean {
// TODO Auto-updates temporarily restricted to nightlies. Once we have designs for allowing users to opt-out of auto-updates, we can re-enable this
return Environment.IS_NIGHTLY && Build.VERSION.SDK_INT >= 31 && SignalStore.apkUpdate().autoUpdate && !ApplicationDependencies.getAppForegroundObserver().isForegrounded
return Environment.IS_NIGHTLY && Build.VERSION.SDK_INT >= 31 && SignalStore.apkUpdate.autoUpdate && !AppForegroundObserver.isForegrounded()
}
}

View File

@@ -83,7 +83,7 @@ object ApkUpdateNotifications {
ServiceUtil.getNotificationManager(context).notify(NotificationIds.APK_UPDATE_FAILED_INSTALL, notification)
}
fun showAutoUpdateSuccess(context: Context) {
fun showUpdateSuccess(context: Context, userInitiated: Boolean) {
val pendingIntent = PendingIntent.getActivity(
context,
0,
@@ -93,9 +93,15 @@ object ApkUpdateNotifications {
val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName
val body = if (userInitiated) {
context.getString(R.string.ApkUpdateNotifications_manual_update_success_body, appVersionName)
} else {
context.getString(R.string.ApkUpdateNotifications_auto_update_success_body, appVersionName)
}
val notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().APP_UPDATES)
.setContentTitle(context.getString(R.string.ApkUpdateNotifications_auto_update_success_title))
.setContentText(context.getString(R.string.ApkUpdateNotifications_auto_update_success_body, appVersionName))
.setContentText(body)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
.setContentIntent(pendingIntent)

View File

@@ -36,10 +36,10 @@ class ApkUpdatePackageInstallerReceiver : BroadcastReceiver() {
when (statusCode) {
PackageInstaller.STATUS_SUCCESS -> {
if (SignalStore.apkUpdate().lastApkUploadTime != SignalStore.apkUpdate().pendingApkUploadTime) {
Log.i(TAG, "Update installed successfully! Updating our lastApkUploadTime to ${SignalStore.apkUpdate().pendingApkUploadTime}")
SignalStore.apkUpdate().lastApkUploadTime = SignalStore.apkUpdate().pendingApkUploadTime
ApkUpdateNotifications.showAutoUpdateSuccess(context)
if (SignalStore.apkUpdate.lastApkUploadTime != SignalStore.apkUpdate.pendingApkUploadTime) {
Log.i(TAG, "Update installed successfully! Updating our lastApkUploadTime to ${SignalStore.apkUpdate.pendingApkUploadTime}")
SignalStore.apkUpdate.lastApkUploadTime = SignalStore.apkUpdate.pendingApkUploadTime
ApkUpdateNotifications.showUpdateSuccess(context, userInitiated)
} else {
Log.i(TAG, "Spurious 'success' notification?")
}

View File

@@ -10,7 +10,7 @@ import android.content.Context;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.ApkUpdateJob;
import org.thoughtcrime.securesms.service.PersistentAlarmManagerListener;
import org.thoughtcrime.securesms.util.Environment;
@@ -35,7 +35,7 @@ public class ApkUpdateRefreshListener extends PersistentAlarmManagerListener {
if (scheduledTime != 0 && BuildConfig.MANAGES_APP_UPDATES) {
Log.i(TAG, "Queueing APK update job...");
ApplicationDependencies.getJobManager().add(new ApkUpdateJob());
AppDependencies.getJobManager().add(new ApkUpdateJob());
}
long newTime = System.currentTimeMillis() + INTERVAL;

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.stickers.StickerLocator
import java.util.UUID
class ArchivedAttachment : Attachment {
@JvmField
val archiveCdn: Int
@JvmField
val archiveMediaName: String
@JvmField
val archiveMediaId: String
@JvmField
val archiveThumbnailMediaId: String
constructor(
contentType: String?,
size: Long,
cdn: Int,
key: ByteArray,
iv: ByteArray?,
cdnKey: String?,
archiveCdn: Int?,
archiveMediaName: String,
archiveMediaId: String,
archiveThumbnailMediaId: String,
digest: ByteArray,
incrementalMac: ByteArray?,
incrementalMacChunkSize: Int?,
width: Int?,
height: Int?,
caption: String?,
blurHash: String?,
voiceNote: Boolean,
borderless: Boolean,
stickerLocator: StickerLocator?,
gif: Boolean,
quote: Boolean,
uuid: UUID?,
fileName: String?
) : super(
contentType = contentType ?: "",
quote = quote,
transferState = AttachmentTable.TRANSFER_NEEDS_RESTORE,
size = size,
fileName = fileName,
cdn = Cdn.fromCdnNumber(cdn),
remoteLocation = cdnKey,
remoteKey = Base64.encodeWithoutPadding(key),
remoteIv = iv,
remoteDigest = digest,
incrementalDigest = incrementalMac,
fastPreflightId = null,
voiceNote = voiceNote,
borderless = borderless,
videoGif = gif,
width = width ?: 0,
height = height ?: 0,
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
uploadTimestamp = 0,
caption = caption,
stickerLocator = stickerLocator,
blurHash = BlurHash.parseOrNull(blurHash),
audioHash = null,
transformProperties = null,
uuid = uuid
) {
this.archiveCdn = archiveCdn ?: Cdn.CDN_3.cdnNumber
this.archiveMediaName = archiveMediaName
this.archiveMediaId = archiveMediaId
this.archiveThumbnailMediaId = archiveThumbnailMediaId
}
constructor(parcel: Parcel) : super(parcel) {
archiveCdn = parcel.readInt()
archiveMediaName = parcel.readString()!!
archiveMediaId = parcel.readString()!!
archiveThumbnailMediaId = parcel.readString()!!
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeInt(archiveCdn)
dest.writeString(archiveMediaName)
dest.writeString(archiveMediaId)
dest.writeString(archiveThumbnailMediaId)
}
override val uri: Uri? = null
override val publicUri: Uri? = null
override val thumbnailUri: Uri? = null
}

View File

@@ -14,6 +14,8 @@ import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.ParcelUtil
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.UUID
/**
* Note: We have to use our own Parcelable implementation because we need to do custom stuff to preserve
@@ -21,7 +23,7 @@ import org.thoughtcrime.securesms.util.ParcelUtil
*/
abstract class Attachment(
@JvmField
val contentType: String,
val contentType: String?,
@JvmField
val transferState: Int,
@JvmField
@@ -29,12 +31,14 @@ abstract class Attachment(
@JvmField
val fileName: String?,
@JvmField
val cdnNumber: Int,
val cdn: Cdn,
@JvmField
val remoteLocation: String?,
@JvmField
val remoteKey: String?,
@JvmField
val remoteIv: ByteArray?,
@JvmField
val remoteDigest: ByteArray?,
@JvmField
val incrementalDigest: ByteArray?,
@@ -65,20 +69,26 @@ abstract class Attachment(
@JvmField
val audioHash: AudioHash?,
@JvmField
val transformProperties: TransformProperties?
val transformProperties: TransformProperties?,
@JvmField
val uuid: UUID?
) : Parcelable {
abstract val uri: Uri?
abstract val publicUri: Uri?
abstract val thumbnailUri: Uri?
val displayUri: Uri?
get() = uri ?: thumbnailUri
protected constructor(parcel: Parcel) : this(
contentType = parcel.readString()!!,
transferState = parcel.readInt(),
size = parcel.readLong(),
fileName = parcel.readString(),
cdnNumber = parcel.readInt(),
cdn = Cdn.deserialize(parcel.readInt()),
remoteLocation = parcel.readString(),
remoteKey = parcel.readString(),
remoteIv = ParcelUtil.readByteArray(parcel),
remoteDigest = ParcelUtil.readByteArray(parcel),
incrementalDigest = ParcelUtil.readByteArray(parcel),
fastPreflightId = parcel.readString(),
@@ -94,7 +104,8 @@ abstract class Attachment(
stickerLocator = ParcelCompat.readParcelable(parcel, StickerLocator::class.java.classLoader, StickerLocator::class.java),
blurHash = ParcelCompat.readParcelable(parcel, BlurHash::class.java.classLoader, BlurHash::class.java),
audioHash = ParcelCompat.readParcelable(parcel, AudioHash::class.java.classLoader, AudioHash::class.java),
transformProperties = ParcelCompat.readParcelable(parcel, TransformProperties::class.java.classLoader, TransformProperties::class.java)
transformProperties = ParcelCompat.readParcelable(parcel, TransformProperties::class.java.classLoader, TransformProperties::class.java),
uuid = UuidUtil.parseOrNull(parcel.readString())
)
override fun writeToParcel(dest: Parcel, flags: Int) {
@@ -103,7 +114,7 @@ abstract class Attachment(
dest.writeInt(transferState)
dest.writeLong(size)
dest.writeString(fileName)
dest.writeInt(cdnNumber)
dest.writeInt(cdn.serialize())
dest.writeString(remoteLocation)
dest.writeString(remoteKey)
ParcelUtil.writeByteArray(dest, remoteDigest)
@@ -122,6 +133,7 @@ abstract class Attachment(
dest.writeParcelable(blurHash, 0)
dest.writeParcelable(audioHash, 0)
dest.writeParcelable(transformProperties, 0)
dest.writeString(uuid?.toString())
}
override fun describeContents(): Int {
@@ -129,7 +141,7 @@ abstract class Attachment(
}
val isInProgress: Boolean
get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE && transferState != AttachmentTable.TRANSFER_RESTORE_OFFLOADED
val isPermanentlyFailed: Boolean
get() = transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE

View File

@@ -17,7 +17,8 @@ object AttachmentCreator : Parcelable.Creator<Attachment> {
DATABASE(DatabaseAttachment::class.java, "database"),
POINTER(PointerAttachment::class.java, "pointer"),
TOMBSTONE(TombstoneAttachment::class.java, "tombstone"),
URI(UriAttachment::class.java, "uri")
URI(UriAttachment::class.java, "uri"),
ARCHIVED(ArchivedAttachment::class.java, "archived")
}
@JvmStatic
@@ -34,6 +35,7 @@ object AttachmentCreator : Parcelable.Creator<Attachment> {
Subclass.POINTER -> PointerAttachment(source)
Subclass.TOMBSTONE -> TombstoneAttachment(source)
Subclass.URI -> UriAttachment(source)
Subclass.ARCHIVED -> ArchivedAttachment(source)
}
}

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