Compare commits

..

197 Commits

Author SHA1 Message Date
Michelle Tang
0323cb5d98 Bump version to 7.59.1 2025-10-03 15:25:44 -04:00
Michelle Tang
f4369f90e0 Update translations and other static files. 2025-10-03 15:20:37 -04:00
Cody Henthorne
8b19cbb603 Show correct dialog when validating AEP after registration. 2025-10-03 15:17:10 -04:00
Alex Hart
aa3a797e19 Drop UNIQUE constraint from table column. 2025-10-03 15:41:29 -03:00
Cody Henthorne
827ceafffb Show skip restore warning. 2025-10-03 14:13:15 -04:00
Michelle Tang
cf1afb739f Bump version to 7.59.0 2025-10-02 13:17:59 -04:00
Michelle Tang
b9fe377afd Update translations and other static files. 2025-10-02 13:11:22 -04:00
Michelle Tang
a381697949 Update button color when viewing votes. 2025-10-02 12:56:04 -04:00
Michelle Tang
2d87078495 Show character count when creating a poll. 2025-10-02 12:56:04 -04:00
Alex Hart
1b9695cb98 Reject last-use kyber key sets that we've seen before. 2025-10-02 12:56:04 -04:00
Cody Henthorne
5324290fab Dedupe attachment downloads for matching attachments and fix size calculations. 2025-10-02 12:56:04 -04:00
Michelle Tang
b8e4ffb5ae Release polls behind feature flag. 2025-10-02 12:56:04 -04:00
Cody Henthorne
67a693107e Fix bad data for users that rotated their recovery key. 2025-10-02 12:56:04 -04:00
andrew-signal
e08b86cda6 Bump to libsignal v0.82.1 2025-10-02 12:56:04 -04:00
Michelle Tang
92bab9fb20 Drop duplicate name error when editing profiles. 2025-10-02 12:56:04 -04:00
Michelle Tang
e7502f08ce Clear etag on internal only config refresh. 2025-10-02 12:56:04 -04:00
Alex Hart
3a530022fc Add additional dialog checks. 2025-10-02 12:56:04 -04:00
jeffrey-signal
2c8144b32f Add more compose preview annotations. 2025-10-02 12:56:04 -04:00
Alex Hart
87535a917a Fully check result code when processing purchase results. 2025-10-02 12:56:04 -04:00
Alex Hart
76448f5426 Apply missing callback for entering remote backups settings fragment. 2025-10-02 12:56:04 -04:00
Alex Hart
019df97a22 Add proper gesture when user navigates to or from a conversation. 2025-10-02 12:56:04 -04:00
Cody Henthorne
51897bb74f Fix outgoing disappearing message export oddity check. 2025-10-02 12:56:04 -04:00
jeffrey-signal
5f3b4056e9 Fix incorrect nav bar colors on registration screens for Android 8.1 and newer. 2025-10-02 12:56:04 -04:00
Jeffrey Starke
73a3c21716 Media review - Fix alignment of recipient name and arrow. 2025-10-02 12:56:04 -04:00
jeffrey-signal
a37209d8ba Prevent infinite archive attachment reconciliation attempts after server storage quota disagreement. 2025-10-02 12:56:04 -04:00
andrew-signal
415021eedf Bump to libsignal v0.82.0 2025-10-02 12:56:04 -04:00
Alex Hart
ea6d512cc8 Do not reroute to main activity if launching in a bubble. 2025-10-02 12:56:04 -04:00
Alex Hart
fba6673907 Eliminate dependency material icons core. 2025-10-02 12:56:04 -04:00
jeffrey-signal
faba4682ed Fix indeterminate progress dialog animation. 2025-10-02 12:56:04 -04:00
Jeffrey Starke
71b92f03bc Fix DonationsService ServiceResponse to use exception body instead of message. 2025-10-02 12:56:04 -04:00
Alex Hart
d4a1cb0bfb Upgrade compose to latest stable. 2025-10-02 12:56:04 -04:00
Alex Hart
e16ca2b2d2 Several navhost behavioural updates to ensure the right pane is displayed at the right time. 2025-10-02 12:56:04 -04:00
Michelle Tang
77e678e05c Bump version to 7.58.2 2025-10-02 12:53:42 -04:00
Michelle Tang
efe0e3b816 Update translations and other static files. 2025-10-02 12:49:37 -04:00
andrew-signal
6c497e131a Remove android.libsignalWebSocketEnabled flag and always use LibSignalChatConnection. 2025-09-30 19:22:47 -04:00
Michelle Tang
ccb8c1b1b9 Bump version to 7.58.1 2025-09-29 16:18:06 -04:00
Michelle Tang
4aa965144d Update translations and other static files. 2025-09-29 16:09:49 -04:00
Cody Henthorne
786bcc3da7 Fix header case bugs in status code errors. 2025-09-26 12:54:18 -04:00
Cody Henthorne
4447b29e6c Fix upload to archive running while not on wifi. 2025-09-25 13:27:07 -04:00
Jeffrey Starke
3ebbb94a1a Bump version to 7.58.0 2025-09-24 16:39:40 -04:00
Jeffrey Starke
64a7cdafa8 Update translations and other static files. 2025-09-24 16:37:04 -04:00
Cody Henthorne
c3350c0bb0 Clear credentials in pre-restore state. 2025-09-24 16:29:57 -04:00
Cody Henthorne
e2be1e0c79 Prevent IMO from running before registration. 2025-09-24 16:29:57 -04:00
Alex Hart
228a993237 Ignore PNI messages for everything except server delivery receipts. 2025-09-24 16:29:57 -04:00
Alex Hart
04923487c4 Ignore mismatch state if FREE tier user has GPB sub.
Co-authored-by: jeffrey-signal <jeffrey@signal.org>
2025-09-24 16:29:57 -04:00
Alex Hart
9777aa411c Remove transitions from base NavHost. 2025-09-24 16:29:57 -04:00
Alex Hart
d0c1e93b3c Do not display a price if it's been zeroed. 2025-09-24 16:29:57 -04:00
Alex Hart
9b517a14cb Remove separate controllers and consolidate logic. 2025-09-24 16:29:57 -04:00
Alex Hart
369085e162 Add new log sections to backups. 2025-09-24 16:29:57 -04:00
Alex Hart
93815a0504 Add checks to skip check job if we have a pending or pre-pending transaction. 2025-09-24 16:29:57 -04:00
Alex Hart
b88097a6ae Utilize keepLonger throughout BillingApiImpl. 2025-09-24 16:29:57 -04:00
Alex Hart
120cc9c521 Fix padding on button when we have a current tier. 2025-09-24 16:29:57 -04:00
Cody Henthorne
58304a0fb6 Fix RTL ByteSize rendering. 2025-09-24 16:29:57 -04:00
Cody Henthorne
6e867d678c Fix de related crash and bug. 2025-09-24 16:29:57 -04:00
Cody Henthorne
8b2f58e0e7 Remove hard coded message backups remote config. 2025-09-24 16:29:57 -04:00
Cody Henthorne
6976ac7d44 Move v3 classes to base registration package. 2025-09-24 16:29:57 -04:00
Cody Henthorne
8dc2077ad0 Remove regv2. 2025-09-24 16:29:57 -04:00
jeffrey-signal
52fa86046b Fix backups UI scaling issues. 2025-09-24 16:29:57 -04:00
Alex Hart
3352ebaa06 Move large screen check to wrapper. 2025-09-24 16:29:57 -04:00
Cody Henthorne
cbfdc4b57a Improve free tier UX around media. 2025-09-24 16:29:57 -04:00
Greyson Parrelli
c5753b96ff Update BackupMediaSnapshot to be based on attachments in backup frames. 2025-09-24 16:29:57 -04:00
Alex Hart
f39ad24cc1 Increase desired width increment to aleviate situations where certain text would still flow to multiple lines. 2025-09-24 16:29:57 -04:00
Greyson Parrelli
6b6877bae7 Update username link logo.
Resolves #14258
2025-09-24 16:29:57 -04:00
andrew-signal
930254da7b Bump to libsignal v0.81.1. 2025-09-24 16:29:56 -04:00
Alex Hart
3df2fa53e8 Don't exit multiselect mode when swapping screens. 2025-09-24 16:29:56 -04:00
Alex Hart
c901639ce8 Add lint detection for System.out.println add kotlin.io.println usage. 2025-09-24 16:29:56 -04:00
Alex Hart
9e1cec7a60 Fix anchor offset when search or action mode is entered. 2025-09-24 16:29:56 -04:00
Alex Hart
9269c66d1e Add remote config support for large screen UI. 2025-09-24 16:29:56 -04:00
Alex Hart
fd999be41a Add new navigation and pane support. 2025-09-24 16:29:56 -04:00
Alex Hart
146a5f5701 Remove ParcelableGroupId. 2025-09-23 20:21:30 -04:00
Alex Hart
d49ef1dd7d Convert RecipientId to Kotlin. 2025-09-23 20:21:30 -04:00
Greyson Parrelli
49c5fead39 Catch ZK validation error in profile fetch. 2025-09-23 20:21:30 -04:00
Greyson Parrelli
9c705f3a45 Remove unnecessary SMS entrypoint.
Fixes #14213
2025-09-23 20:21:30 -04:00
Alex Hart
bea204ab82 Convert GroupId to Kotlin. 2025-09-23 20:21:29 -04:00
Jeffrey Starke
9350438866 Bump version to 7.57.2 2025-09-23 20:00:39 -04:00
Jeffrey Starke
4d827adc8b Update translations and other static files. 2025-09-23 19:50:47 -04:00
Cody Henthorne
9f839b75fb Improve restore error messaging and actual available restore method options. 2025-09-23 14:32:11 -04:00
Alex Hart
c0482e8247 Ensure api availability is properly loaded in checkout flow. 2025-09-23 15:12:37 -03:00
Greyson Parrelli
17f27f45fc Bump version to 7.57.1 2025-09-18 10:36:19 -04:00
Greyson Parrelli
2401e33222 Update translations and other static files. 2025-09-18 10:35:53 -04:00
Greyson Parrelli
4345179a1d Fix replying to non-media. 2025-09-18 10:10:56 -04:00
Greyson Parrelli
5aa6fc78ee Bump version to 7.57.0 2025-09-17 14:37:30 -04:00
Greyson Parrelli
e0a86ead58 Update translations and other static files. 2025-09-17 14:36:54 -04:00
Alex Hart
169d0fa964 Convert Media to kotlin. 2025-09-17 14:21:43 -04:00
Greyson Parrelli
c5397bc7d2 Fix potential crash in story send.
Fixes #14331
2025-09-17 14:21:43 -04:00
Greyson Parrelli
43f6e0ad8e Fix restore error string formatting. 2025-09-17 14:21:43 -04:00
Alex Hart
736811393f Upgrade Kotlin, AGP, Gradle versions and bring in kotlinx-serialization for use with navigation-compose. 2025-09-17 14:21:43 -04:00
andrew-signal
957ddc82b5 Switch lookupUsernameHash to use libsignal's typed API wrapper. 2025-09-17 14:21:43 -04:00
andrew-signal
16d6e98355 Pass all android.libsignal.* prefixed remote configs down automatically. 2025-09-17 14:21:43 -04:00
Alex Hart
2a90809ba3 Add Billing API and Google API availability error dialogs. 2025-09-17 14:21:43 -04:00
andrew-signal
0713a88ddb Bump to libsignal v0.81.0 2025-09-17 14:21:43 -04:00
Greyson Parrelli
c78b47fbe3 Make max envelope size remote configurable. 2025-09-17 14:21:43 -04:00
jeffrey-signal
5807cbc9e9 Disable autofill for PIN entry fields. 2025-09-17 14:21:43 -04:00
Cody Henthorne
6d90330e86 Improve restore complete dialog for old device. 2025-09-17 14:21:43 -04:00
Michelle Tang
862bab55af Add more logging around notification profile overrides. 2025-09-17 14:21:43 -04:00
jeffrey-signal
7235a3730c Fix crash when opening the change number registration lock screen. 2025-09-17 14:21:43 -04:00
jeffrey-signal
c24993960d Fix inconsistent default PIN keyboard type. 2025-09-17 14:21:43 -04:00
Greyson Parrelli
7f429dc769 Bring back proper archive delete reconciliation. 2025-09-17 14:21:43 -04:00
Michelle Tang
a575626abb Add logging around overrides in notification profiles. 2025-09-17 14:21:43 -04:00
moiseev-signal
0b71b1837c Upgrade to libsignal 0.80.3 and add a new trust root for sealed sender. 2025-09-17 14:21:43 -04:00
jeffrey-signal
f0df1b99e5 Always include english translations for emoji search.
Updates the `emoji_search` table by including English emoji labels alongside existing localized labels, enabling users to search for emojis in both their preferred language and English.
2025-09-17 14:21:43 -04:00
Alex Hart
23b7ea90a1 Add fixes for primary choice when returning to chats. 2025-09-17 14:21:43 -04:00
Alex Hart
53a6b0c719 Fix navigator to ensure we don't end up with a weird backstack. 2025-09-17 14:21:43 -04:00
Alex Hart
bf3135b2d0 Fix various issues in main activity display. 2025-09-17 14:21:43 -04:00
Alex Hart
897461b594 Expand the detail anchor if we select a conversation while the list is maximized. 2025-09-17 14:21:43 -04:00
Alex Hart
63800306a0 Pre-seed navigation when intent is processed before navigator is set. 2025-09-17 14:21:43 -04:00
Greyson Parrelli
b649b8c943 Hide some error states for unregistered users. 2025-09-17 14:21:42 -04:00
andrew-signal
2c0aa40c61 Disable vector drawable rasterization in donations library. 2025-09-17 14:21:42 -04:00
Alex Hart
2eb4f650d8 Convert NotificationsSettingsFragment to compose. 2025-09-17 14:21:42 -04:00
Ehren Kret
7af811eb3f Accept legacy call links. 2025-09-17 14:21:42 -04:00
Alex Hart
d7f43c436e Mark decision state during instrumentation testing. 2025-09-17 14:21:42 -04:00
Cody Henthorne
2792b9e676 Add prompt to re-enable local backups post restore. 2025-09-17 14:21:42 -04:00
Cody Henthorne
bdf2ef5a05 Allow for multiple captchas to be solved during registration. 2025-09-17 14:21:42 -04:00
Alex Hart
23b5a3dcb0 Start conversion from Fragment Nav Framework to utilizing a centralized AppSettingsRouter. 2025-09-17 14:21:42 -04:00
Alex Hart
909ea6b925 Add MainActivity scaffold anchoring. 2025-09-17 14:21:42 -04:00
Greyson Parrelli
a5922c31b1 Fix debuglog spacing. 2025-09-17 14:21:42 -04:00
Greyson Parrelli
d8758bcc4e Add data seeding playground. 2025-09-17 14:21:42 -04:00
Jim Gustafson
f88181cc82 Update to RingRTC v2.57.1 2025-09-17 14:21:42 -04:00
Michelle Tang
c3f1036686 Always fetch remote configs on app update. 2025-09-17 14:21:42 -04:00
Greyson Parrelli
96292cd4a1 Bump version to 7.56.9 2025-09-17 14:20:05 -04:00
Greyson Parrelli
81f6035027 Update translations and other static files. 2025-09-17 14:03:43 -04:00
Greyson Parrelli
52005cf62c Fix bug when replying with a voice note. 2025-09-17 13:30:02 -04:00
Greyson Parrelli
f5effa5be9 Bump version to 7.56.8 2025-09-16 10:48:51 -04:00
Greyson Parrelli
cae7906f04 Mark some archive logs as keep longer. 2025-09-16 10:48:29 -04:00
Greyson Parrelli
7ea8cc6b0a Fix database migrations post-backup-restore. 2025-09-16 09:44:49 -04:00
Greyson Parrelli
8669a3d6e0 Bump version to 7.56.7 2025-09-15 20:42:16 -04:00
Greyson Parrelli
cb3bc91865 Update translations and other static files. 2025-09-15 20:41:54 -04:00
Cody Henthorne
1a0c4b8135 Fix crash with media restore progress banner. 2025-09-15 20:33:58 -04:00
Cody Henthorne
6a456a288d Fix signal backup daily schedule bug. 2025-09-15 20:33:58 -04:00
Cody Henthorne
901a81fb74 Add edit proxy ability to quick restore flow. 2025-09-15 20:33:58 -04:00
Cody Henthorne
b1b99855b2 Improve understanding of last signal backup time in main backup settings screen. 2025-09-15 12:51:41 -04:00
Alex Hart
c6f0b4cf83 Remove frequency row. 2025-09-15 13:20:29 -03:00
Cody Henthorne
1a5dede780 Bump version to 7.56.6 2025-09-12 15:35:19 -04:00
Cody Henthorne
2c8b1c6acb Update translations and other static files. 2025-09-12 15:29:44 -04:00
Greyson Parrelli
d7da56b82f Temporarily disable reconciliation deletes. 2025-09-12 15:21:27 -04:00
Cody Henthorne
d9cfdd1b32 Update backups all set bottom sheet. 2025-09-12 15:21:27 -04:00
Greyson Parrelli
b3b3a4bebf Put invisible marker for keepLonger logs to improve log viewing. 2025-09-12 15:21:27 -04:00
Greyson Parrelli
9021883baa Improve debug logging for reconciliation. 2025-09-12 15:21:27 -04:00
Cody Henthorne
c19017f037 Update svr as part of remote restore. 2025-09-12 14:54:35 -04:00
Alex Hart
bff40ff60b If subscription will cancel at end of period, then show the cancelled state. 2025-09-12 12:24:57 -03:00
Greyson Parrelli
299445d5f9 Fix query for enqueuing thumbnail jobs. 2025-09-12 10:50:19 -04:00
Alex Hart
b2e3d7ba20 Move pending payment check above subscription checks. 2025-09-12 11:17:47 -03:00
Greyson Parrelli
60df7502ee Fix configuration. 2025-09-11 15:49:19 -04:00
Cody Henthorne
28ea4dbc16 Fix stale backup time on backup setting screen. 2025-09-11 15:32:36 -04:00
Cody Henthorne
c4d9942f0e Fix crash when remote key is missing but not null. 2025-09-11 15:14:36 -04:00
Greyson Parrelli
3e50d2318f Try to fix phone numbers if we hit a dupe recipient during archive export. 2025-09-11 13:55:53 -04:00
Greyson Parrelli
040881e5a6 Fix a chat update validation error. 2025-09-11 11:37:27 -04:00
Cody Henthorne
681234ace3 Make failure to trigger backup id a warning log. 2025-09-11 11:00:52 -04:00
Greyson Parrelli
98e9694b35 Support backup4 and backup5 capabilities for linked devices. 2025-09-11 10:44:38 -04:00
andrew-signal
95c46b9d82 Bump libsignal to v0.80.2 2025-09-11 10:04:26 -04:00
andrew-signal
76dfa5d7fe Bump libsignal to v0.80.1 2025-09-11 10:03:42 -04:00
Cody Henthorne
b39d562d56 Include last proto size in internal settion of backups settings. 2025-09-10 16:28:53 -04:00
Cody Henthorne
148cf63a92 Fix bugs around showing 0 bytes for backup size. 2025-09-10 16:05:12 -04:00
Greyson Parrelli
c155b4e025 Update legacy app icons. 2025-09-10 15:54:54 -04:00
Greyson Parrelli
90ae9e1636 Reduce max envelope size for internal users. 2025-09-10 15:33:10 -04:00
jeffrey-signal
79ee14826d Disable keyboard suggestions when typing PIN.
Converts `PinKeyboardType` to Kotlin and introduces methods to consistently configure PIN entry fields throughout the app, including a fix to disable keyboard suggestions.
2025-09-10 14:20:46 -04:00
Greyson Parrelli
179bb6e1da Add mitigation for mentions with unknown recipients.
Relates to #14310
2025-09-10 14:17:23 -04:00
Greyson Parrelli
c393d65ce6 Do not include scheduled messages in the archive. 2025-09-10 14:16:36 -04:00
Alex Hart
eeb8164c18 Always display paid tier but stick a dialog in front of it for non-GPS devices. 2025-09-10 15:12:35 -03:00
Cody Henthorne
ea772cbf55 Fix reglock setting incorrect during backup restore. 2025-09-10 12:57:25 -04:00
Cody Henthorne
dd67398a70 Fix upload media progress bar not showing after backup create. 2025-09-10 11:49:38 -04:00
Greyson Parrelli
d44bed0379 Fix archive handling of mixed error send statuses, add more tests. 2025-09-10 11:42:43 -04:00
Greyson Parrelli
236c79bfbb Update beta labeling string. 2025-09-10 10:22:54 -04:00
Cody Henthorne
7dfee7e315 Bump version to 7.56.5 2025-09-09 16:50:31 -04:00
Cody Henthorne
6600849cc4 Update translations and other static files. 2025-09-09 16:31:03 -04:00
Cody Henthorne
c4255157ac Fix benchmark test. 2025-09-09 16:23:22 -04:00
Cody Henthorne
3bb6a0a560 Fix daily backups being incorrectly scheduled for multiple days away. 2025-09-09 15:55:11 -04:00
Greyson Parrelli
a05d5ff5e6 Fix some issues with BackupMediaSnapshots. 2025-09-09 15:12:02 -04:00
Alex Hart
da6ad2b629 Add fallback biometric handling for older api versions. 2025-09-09 12:48:49 -03:00
Cody Henthorne
09a05c9f4c Fix dangling progress state prior to progress status determination. 2025-09-09 11:48:15 -04:00
Greyson Parrelli
57319d3189 Perform additional APNG validations.
Special thank you to Stanislav Fort of Aisle Research (stanislav.fort@aisle.com)
for finding this issue, bringing it to our attention, and offering a solution!
2025-09-09 11:45:04 -04:00
Alex Hart
40ba967192 Don't fail backup redemption pipeline after 24hrs. 2025-09-09 11:46:59 -03:00
Greyson Parrelli
3c7534f7fa Add some job logging improvements. 2025-09-09 10:38:02 -04:00
Greyson Parrelli
79ec47f901 Fix archive tests around optimize storage. 2025-09-08 16:21:57 -04:00
Greyson Parrelli
6108b5ab77 Another attempt to fix validation errors around invalid long text attachments. 2025-09-08 16:18:21 -04:00
Michelle Tang
49417bdf9d Update edit text fields for pins. 2025-09-08 14:28:04 -04:00
Alex Hart
d2fcb191b6 Add new text label for when backups are off. 2025-09-08 14:59:59 -03:00
Alex Hart
dca876e40d Don't mark deletion failure if we are in an IDLE state. 2025-09-08 14:50:12 -03:00
Alex Hart
5e35c209c2 Fix backup state observation and expand logging. 2025-09-08 14:29:46 -03:00
Greyson Parrelli
22382bc8a3 Keep some message backup logs for longer. 2025-09-08 13:22:27 -04:00
Greyson Parrelli
bf9e75d983 Ensure that permanent thumbnail failures can be properly marked as such. 2025-09-08 11:05:06 -04:00
Greyson Parrelli
f96e29c9c9 Fix thumbnail query in debug attachment stats. 2025-09-08 11:05:06 -04:00
Greyson Parrelli
0bf2f9aca7 Add extra buttons to the Backup Playground. 2025-09-08 11:05:06 -04:00
Greyson Parrelli
2ba427e3dd Do not attempt archive restore when data hash is null. 2025-09-08 11:05:06 -04:00
Greyson Parrelli
624ae32a0e Bump version to 7.56.4 2025-09-06 00:02:21 -04:00
Greyson Parrelli
1339c44892 Fix potential crash when backing up attachment. 2025-09-05 23:38:57 -04:00
Greyson Parrelli
8499402831 Remove unnecessary BackupSubscriptionJob tests cases. 2025-09-05 19:37:38 -04:00
Greyson Parrelli
4df6b87c13 Fix attachment table archive eligibility tests. 2025-09-05 19:33:50 -04:00
Greyson Parrelli
7d16e857d4 Fix unit tests around quote deduping. 2025-09-05 16:56:52 -04:00
Greyson Parrelli
a6c215801b Delay the first archive reconciliation. 2025-09-05 16:42:07 -04:00
andrew-signal
b3a9b92717 Bump libsignal to v0.80.0 2025-09-05 16:14:56 -04:00
Alex Hart
071496e0c1 Bump version to 7.56.2 2025-09-05 16:23:00 -03:00
Alex Hart
af36b9adbd Update translations and other static files. 2025-09-05 16:22:42 -03:00
Greyson Parrelli
630f998ea4 Add some better debug info around backup state. 2025-09-05 15:00:00 -04:00
Greyson Parrelli
9058f7ed55 Ignore invalid long text attachment pointers. 2025-09-05 14:31:14 -04:00
Alex Hart
0dac6344ab Access configuration via cache layer when evaluating state. 2025-09-05 14:44:49 -03:00
Greyson Parrelli
8adb16912f Update string describing backup frequency. 2025-09-05 11:02:25 -04:00
681 changed files with 29723 additions and 15611 deletions

View File

@@ -12,6 +12,7 @@ plugins {
alias(libs.plugins.jetbrains.kotlin.android)
alias(libs.plugins.ktlint)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.kotlinx.serialization)
id("androidx.navigation.safeargs")
id("kotlin-parcelize")
id("com.squareup.wire")
@@ -21,8 +22,8 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1581
val canonicalVersionName = "7.56.1"
val canonicalVersionCode = 1596
val canonicalVersionName = "7.59.1"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
@@ -217,7 +218,7 @@ android {
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY_LEGACY", "\"9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570\"")
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY", "\"093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"29cd63c87bea751e3bfd0fbd401279192e2e5c99948b4ee9437eafc4968355fb\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
buildConfigField("String[]", "UNIDENTIFIED_SENDER_TRUST_ROOTS", "new String[]{ \"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\", \"BUkY0I+9+oPgDCn4+Ac6Iu813yvqkDr/ga8DzLxFxuk6\"}")
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==\"")
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"")
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AJwNSU55fsFCbgaxGRD11wO1juAs8Yr5GF8FPlGzzvdJJIKH5/4CC7ZJSOe3yL2vturVaRU2Cx0n751Vt8wkj1bozK3CBV1UokxV09GWf+hdVImLGjXGYLLhnI1J2TWEe7iWHyb553EEnRb5oxr9n3lUbNAJuRmFM7hrr0Al0F0wrDD4S8lo2mGaXe0MJCOM166F8oYRQqpFeEHfiLnxA1O8ZLh7vMdv4g9jI5phpRBTsJ5IjiJrWeP0zdIGHEssUeprDZ9OUJ14m0v61eYJMKsf59Bn+mAT2a7YfB+Don9O\"")
@@ -237,8 +238,7 @@ android {
buildConfigField("String", "STRIPE_BASE_URL", "\"https://api.stripe.com/v1\"")
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
buildConfigField("boolean", "TRACING_ENABLED", "false")
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
buildConfigField("boolean", "LINK_DEVICE_UX_ENABLED", "true")
buildConfigField("boolean", "LINK_DEVICE_UX_ENABLED", "false")
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@@ -378,7 +378,6 @@ android {
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "\"${apkUpdateManifestUrl}\"")
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\"")
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
}
create("prod") {
@@ -405,7 +404,7 @@ android {
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY_LEGACY", "\"38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3\"")
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY", "\"2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"a75542d82da9f6914a1e31f8a7407053b99cc99a0e7291d8fbd394253e19b036\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
buildConfigField("String[]", "UNIDENTIFIED_SENDER_TRUST_ROOTS", "new String[]{\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\", \"BYhU6tPjqP46KGZEzRs1OL4U39V5dlPJ/X09ha4rErkm\"}")
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==\"")
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"")
buildConfigField("String", "BACKUP_SERVER_PUBLIC_PARAMS", "\"AHYrGb9IfugAAJiPKp+mdXUx+OL9zBolPYHYQz6GI1gWjpEu5me3zVNSvmYY4zWboZHif+HG1sDHSuvwFd0QszSwuSF4X4kRP3fJREdTZ5MCR0n55zUppTwfHRW2S4sdQ0JGz7YDQIJCufYSKh0pGNEHL6hv79Agrdnr4momr3oXdnkpVBIp3HWAQ6IbXQVSG18X36GaicI1vdT0UFmTwU2KTneluC2eyL9c5ff8PcmiS+YcLzh0OKYQXB5ZfQ06d6DiINvDQLy75zcfUOniLAj0lGJiHxGczin/RXisKSR8\"")
@@ -417,7 +416,6 @@ android {
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
}
create("backup") {
@@ -429,7 +427,6 @@ android {
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Backup\"")
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
}
}
@@ -607,6 +604,7 @@ dependencies {
implementation(libs.rxdogtag)
implementation(libs.androidx.credentials)
implementation(libs.androidx.credentials.compat)
implementation(libs.kotlinx.serialization.json)
implementation(project(":billing"))

View File

@@ -171,6 +171,11 @@ class ArchiveImportExportTests {
runTests { it.startsWith("chat_item_standard_message_standard_attachments_") }
}
// @Test
fun chatItemStandardMessageGroupTextOnly() {
runTests { it.startsWith("chat_item_standard_message_group_text_only_") }
}
// @Test
fun chatItemStandardMessageTextOnly() {
runTests { it.startsWith("chat_item_standard_message_text_only_") }

View File

@@ -21,7 +21,6 @@ import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockkStatic
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -43,7 +42,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.RemoteConfig
import java.math.BigDecimal
import java.util.Currency
@@ -67,9 +65,6 @@ class MessageBackupsCheckoutActivityTest {
every { AppDependencies.billingApi.getBillingPurchaseResults() } returns purchaseResults
coEvery { AppDependencies.billingApi.queryProduct() } returns BillingProduct(price = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")))
coEvery { AppDependencies.billingApi.launchBillingFlow(any()) } returns Unit
mockkStatic(RemoteConfig::class)
every { RemoteConfig.messageBackups } returns true
}
@Test

View File

@@ -36,6 +36,8 @@ import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.polls.PollOption
import org.thoughtcrime.securesms.polls.PollRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
@@ -348,5 +350,11 @@ class V2ConversationItemShapeTest {
override fun onShowUnverifiedProfileSheet(forGroup: Boolean) = Unit
override fun onUpdateSignalClicked() = Unit
override fun onViewResultsClicked(pollId: Long) = Unit
override fun onViewPollClicked(messageId: Long) = Unit
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) = Unit
}
}

View File

@@ -259,6 +259,7 @@ class AttachmentTableTest {
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
SignalDatabase.attachments.finalizeAttachmentAfterUpload(AttachmentId(1L), AttachmentTableTestUtil.createUploadResult(AttachmentId(1L)))
// WHEN
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()
@@ -277,6 +278,7 @@ class AttachmentTableTest {
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
SignalDatabase.attachments.finalizeAttachmentAfterUpload(AttachmentId(1L), AttachmentTableTestUtil.createUploadResult(AttachmentId(1L)))
// WHEN
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()
@@ -296,6 +298,7 @@ class AttachmentTableTest {
SignalDatabase.messages.markExpireStarted(messageId)
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
SignalDatabase.attachments.finalizeAttachmentAfterUpload(AttachmentId(1L), AttachmentTableTestUtil.createUploadResult(AttachmentId(1L)))
// WHEN
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()
@@ -314,6 +317,7 @@ class AttachmentTableTest {
val messageId = SignalDatabase.messages.insertMessageInbox(message).map { it.messageId }.get()
SignalDatabase.attachments.setArchiveTransferState(AttachmentId(1L), AttachmentTable.ArchiveTransferState.NONE)
SignalDatabase.attachments.setTransferState(messageId, AttachmentId(1L), AttachmentTable.TRANSFER_PROGRESS_DONE)
SignalDatabase.attachments.finalizeAttachmentAfterUpload(AttachmentId(1L), AttachmentTableTestUtil.createUploadResult(AttachmentId(1L)))
// WHEN
val attachments = SignalDatabase.attachments.getAttachmentsThatNeedArchiveUpload()

View File

@@ -47,6 +47,8 @@ class AttachmentTableTest_deduping {
val DATA_A_HASH = byteArrayOf(1, 1, 1)
val DATA_B = byteArrayOf(7, 8, 9)
val DATA_C_JPEG = Base64.decode("/9j/4AAQSkZJRgABAQEBLAEsAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoHBwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQMEBAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBT/wgARCAAKAAoDAREAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAb/xAAWAQEBAQAAAAAAAAAAAAAAAAAABgf/2gAMAwEAAhADEAAAAY/ZpAAf/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQABBQIf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAwEBPwEf/8QAFBEBAAAAAAAAAAAAAAAAAAAAIP/aAAgBAgEBPwEf/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQAGPwIf/8QAFBABAAAAAAAAAAAAAAAAAAAAIP/aAAgBAQABPyEf/9oADAMBAAIAAwAAABBtv//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQMBAT8QH//EABQRAQAAAAAAAAAAAAAAAAAAACD/2gAIAQIBAT8QH//EABQQAQAAAAAAAAAAAAAAAAAAACD/2gAIAQEAAT8QH//Z")
}
@Before
@@ -483,38 +485,27 @@ class AttachmentTableTest_deduping {
fun quotes() {
// Basic quote deduping
test {
val id1 = insertWithData(DATA_A)
val id2 = insertQuote(id1)
val targetAttachment1 = insertWithData(DATA_C_JPEG)
val quoteAttachment1 = insertQuote(targetAttachment1)
val quoteAttachment2 = insertQuote(targetAttachment1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataFilesAreTheSame(quoteAttachment1, quoteAttachment2)
assertDataHashStartMatches(quoteAttachment1, quoteAttachment2)
}
// Making sure remote fields carry
test {
val id1 = insertWithData(DATA_A)
val id2 = insertQuote(id1)
upload(id1)
val targetAttachment1 = insertWithData(DATA_C_JPEG)
val quoteAttachment1 = insertQuote(targetAttachment1)
upload(quoteAttachment1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
}
val quoteAttachment2 = insertQuote(targetAttachment1)
// Making sure things work for quotes of videos, which have trickier transform properties
test {
val id1 = insertWithData(DATA_A, transformProperties = TransformProperties.forVideoTrim(1, 2))
compress(id1, DATA_A_COMPRESSED)
upload(id1)
val id2 = insertQuote(id1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertArchiveFieldsMatch(id1, id2)
assertDataFilesAreTheSame(quoteAttachment1, quoteAttachment2)
assertDataHashStartMatches(quoteAttachment1, quoteAttachment2)
assertDataHashEndMatches(quoteAttachment1, quoteAttachment2)
assertRemoteFieldsMatch(quoteAttachment1, quoteAttachment2)
assertArchiveFieldsMatch(quoteAttachment1, quoteAttachment2)
}
}

View File

@@ -10,7 +10,7 @@ import org.junit.runner.RunWith
import org.signal.core.util.count
import org.signal.core.util.readToSingleInt
import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable.ArchiveMediaItem
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable.MediaEntry
import org.thoughtcrime.securesms.testing.SignalActivityRule
@RunWith(AndroidJUnit4::class)
@@ -21,7 +21,7 @@ class BackupMediaSnapshotTableTest {
@Test
fun givenAnEmptyTable_whenIWriteToTable_thenIExpectEmptyTable() {
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = 100))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = 100))
val count = getCountForLatestSnapshot(includeThumbnails = true)
@@ -30,7 +30,7 @@ class BackupMediaSnapshotTableTest {
@Test
fun givenAnEmptyTable_whenIWriteToTableAndCommit_thenIExpectFilledTable() {
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = 100))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = 100))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val count = getCountForLatestSnapshot(includeThumbnails = false)
@@ -43,7 +43,8 @@ class BackupMediaSnapshotTableTest {
val inputCount = 100
val countWithThumbnails = inputCount * 2
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = inputCount))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = inputCount, thumbnail = true))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val count = getCountForLatestSnapshot(includeThumbnails = true)
@@ -51,40 +52,16 @@ class BackupMediaSnapshotTableTest {
assertThat(count).isEqualTo(countWithThumbnails)
}
@Test
fun givenAnEmptyTable_whenIWriteToTableAndCommitQuotes_thenIExpectFilledTableWithNoThumbnails() {
val inputCount = 100
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount, quote = true))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val count = getCountForLatestSnapshot(includeThumbnails = true)
assertThat(count).isEqualTo(inputCount)
}
@Test
fun givenAnEmptyTable_whenIWriteToTableAndCommitNonMedia_thenIExpectFilledTableWithNoThumbnails() {
val inputCount = 100
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = inputCount, contentType = "text/plain"))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val count = getCountForLatestSnapshot(includeThumbnails = true)
assertThat(count).isEqualTo(inputCount)
}
@Test
fun givenAFilledTable_whenIReinsertObjects_thenIExpectUncommittedOverrides() {
val initialCount = 100
val additionalCount = 25
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = initialCount))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = initialCount))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
// This relies on how the sequence of mediaIds is generated in tests -- the ones we generate here will have the mediaIds as the ones we generated above
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = additionalCount))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = additionalCount))
val pendingCount = getCountForPending(includeThumbnails = false)
val latestVersionCount = getCountForLatestSnapshot(includeThumbnails = false)
@@ -98,11 +75,11 @@ class BackupMediaSnapshotTableTest {
val initialCount = 100
val additionalCount = 25
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = initialCount))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = initialCount))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
// This relies on how the sequence of mediaIds is generated in tests -- the ones we generate here will have the mediaIds as the ones we generated above
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = additionalCount))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = additionalCount))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val pendingCount = getCountForPending(includeThumbnails = false)
@@ -119,10 +96,10 @@ class BackupMediaSnapshotTableTest {
val initialCount = 100
val additionalCount = 25
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = initialCount))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = initialCount))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = additionalCount))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = additionalCount))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val page = SignalDatabase.backupMediaSnapshots.getPageOfOldMediaObjects(pageSize = 1_000)
@@ -145,7 +122,7 @@ class BackupMediaSnapshotTableTest {
createArchiveMediaObject(seed = 2, cdn = 2)
)
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(localData.asSequence())
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(localData)
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val mismatches = SignalDatabase.backupMediaSnapshots.getMediaObjectsWithNonMatchingCdn(remoteData)
@@ -164,7 +141,7 @@ class BackupMediaSnapshotTableTest {
createArchiveMediaObject(seed = 2, cdn = 99)
)
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(localData.asSequence())
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(localData)
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val mismatches = SignalDatabase.backupMediaSnapshots.getMediaObjectsWithNonMatchingCdn(remoteData)
@@ -186,7 +163,7 @@ class BackupMediaSnapshotTableTest {
createArchiveMediaObject(seed = 2, cdn = 2)
)
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(localData.asSequence())
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(localData)
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val notFound = SignalDatabase.backupMediaSnapshots.getMediaObjectsThatCantBeFound(remoteData)
@@ -205,7 +182,7 @@ class BackupMediaSnapshotTableTest {
createArchiveMediaObject(seed = 3, cdn = 2)
)
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(localData.asSequence())
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(localData)
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val notFound = SignalDatabase.backupMediaSnapshots.getMediaObjectsThatCantBeFound(remoteData)
@@ -222,7 +199,7 @@ class BackupMediaSnapshotTableTest {
@Test
fun getCurrentSnapshotVersion_singleCommit() {
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = 100))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = 100))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val version = SignalDatabase.backupMediaSnapshots.getCurrentSnapshotVersion()
@@ -234,14 +211,12 @@ class BackupMediaSnapshotTableTest {
fun getMediaObjectsLastSeenOnCdnBeforeSnapshotVersion_noneMarkedSeen() {
val initialCount = 100
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(generateArchiveMediaItemSequence(count = initialCount))
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(generateArchiveMediaItemSequence(count = initialCount))
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val notSeenCount = SignalDatabase.backupMediaSnapshots.getMediaObjectsLastSeenOnCdnBeforeSnapshotVersion(1).count
val expectedOldCountIncludingThumbnails = initialCount * 2
assertThat(notSeenCount).isEqualTo(expectedOldCountIncludingThumbnails)
assertThat(notSeenCount).isEqualTo(initialCount)
}
@Test
@@ -249,22 +224,25 @@ class BackupMediaSnapshotTableTest {
val initialCount = 100
val markSeenCount = 25
val itemsToCommit = generateArchiveMediaItemSequence(count = initialCount)
SignalDatabase.backupMediaSnapshots.writePendingMediaObjects(itemsToCommit)
val fullSizeItems = generateArchiveMediaItemSequence(count = initialCount, thumbnail = false)
val thumbnailItems = generateArchiveMediaItemSequence(count = initialCount, thumbnail = true)
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(fullSizeItems)
SignalDatabase.backupMediaSnapshots.writePendingMediaEntries(thumbnailItems)
SignalDatabase.backupMediaSnapshots.commitPendingRows()
val normalIdsToMarkSeen = itemsToCommit.take(markSeenCount).map { it.mediaId }.toList()
val thumbnailIdsToMarkSeen = itemsToCommit.take(markSeenCount).map { it.thumbnailMediaId }.toList()
val allItemsToMarkSeen = normalIdsToMarkSeen + thumbnailIdsToMarkSeen
val fullSizeIdsToMarkSeen = fullSizeItems.take(markSeenCount).map { it.mediaId }.toList()
val thumbnailIdsToMarkSeen = thumbnailItems.take(markSeenCount).map { it.mediaId }.toList()
SignalDatabase.backupMediaSnapshots.markSeenOnRemote(allItemsToMarkSeen, 1)
SignalDatabase.backupMediaSnapshots.markSeenOnRemote(fullSizeIdsToMarkSeen, 1)
SignalDatabase.backupMediaSnapshots.markSeenOnRemote(thumbnailIdsToMarkSeen, 1)
val notSeenCount = SignalDatabase.backupMediaSnapshots.getMediaObjectsLastSeenOnCdnBeforeSnapshotVersion(1).count
val expectedOldCount = initialCount - markSeenCount
val expectedOldCountIncludingThumbnails = expectedOldCount * 2
val expectedOldCount = (initialCount * 2) - (markSeenCount * 2)
assertThat(notSeenCount).isEqualTo(expectedOldCountIncludingThumbnails)
assertThat(notSeenCount).isEqualTo(expectedOldCount)
}
private fun getTotalItemCount(includeThumbnails: Boolean): Int {
@@ -314,28 +292,30 @@ class BackupMediaSnapshotTableTest {
.readToSingleInt(0)
}
private fun generateArchiveMediaItemSequence(count: Int, quote: Boolean = false, contentType: String = "image/jpeg"): Sequence<ArchiveMediaItem> {
private fun generateArchiveMediaItemSequence(count: Int, thumbnail: Boolean = false): Collection<MediaEntry> {
return (1..count)
.asSequence()
.map { createArchiveMediaItem(it, quote = quote, contentType = contentType) }
.map { createArchiveMediaItem(it, thumbnail = thumbnail) }
.toList()
}
private fun createArchiveMediaItem(seed: Int, cdn: Int = 0, quote: Boolean = false, contentType: String = "image/jpeg"): ArchiveMediaItem {
return ArchiveMediaItem(
mediaId = "media_id_$seed",
thumbnailMediaId = "thumbnail_media_id_$seed",
private fun createArchiveMediaItem(seed: Int, thumbnail: Boolean = false, cdn: Int = 0): MediaEntry {
return MediaEntry(
mediaId = mediaId(seed, thumbnail),
cdn = cdn,
plaintextHash = Util.toByteArray(seed),
remoteKey = Util.toByteArray(seed),
quote = quote,
contentType = contentType
isThumbnail = thumbnail
)
}
private fun createArchiveMediaObject(seed: Int, cdn: Int = 0): ArchivedMediaObject {
private fun createArchiveMediaObject(seed: Int, thumbnail: Boolean = false, cdn: Int = 0): ArchivedMediaObject {
return ArchivedMediaObject(
mediaId = "media_id_$seed",
mediaId = mediaId(seed, thumbnail),
cdn = cdn
)
}
fun mediaId(seed: Int, thumbnail: Boolean): String {
return "media_id_${seed}_$thumbnail"
}
}

View File

@@ -9,15 +9,10 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireLongOrNull
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.protocol.kem.KEMKeyPair
import org.signal.libsignal.protocol.kem.KEMKeyType
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
import org.whispersystems.signalservice.api.push.ServiceId
import org.signal.libsignal.protocol.ReusedBaseKeyException
import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil.generateECPublicKey
import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil.getStaleTime
import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil.insertTestRecord
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.UUID
@@ -142,42 +137,43 @@ class KyberPreKeyTableTest {
assertNotNull(getStaleTime(aci, 3))
}
private fun insertTestRecord(account: ServiceId, id: Int, staleTime: Long = 0, lastResort: Boolean = false) {
val kemKeyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024)
SignalDatabase.kyberPreKeys.insert(
serviceId = account,
keyId = id,
record = KyberPreKeyRecord(
id,
System.currentTimeMillis(),
kemKeyPair,
ECKeyPair.generate().privateKey.calculateSignature(kemKeyPair.publicKey.serialize())
),
lastResort = lastResort
@Test(expected = ReusedBaseKeyException::class)
fun handleMarkKyberPreKeyUsed_doesNotAllowDuplicateLastResortKeyEntries() {
insertTestRecord(aci, id = 1, staleTime = 10, lastResort = true)
val publicKey = generateECPublicKey()
SignalDatabase.kyberPreKeys.handleMarkKyberPreKeyUsed(
serviceId = aci,
kyberPreKeyId = 1,
signedPreKeyId = 1,
baseKey = publicKey
)
val count = SignalDatabase.rawDatabase
.update(KyberPreKeyTable.TABLE_NAME)
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
assertEquals(1, count)
SignalDatabase.kyberPreKeys.handleMarkKyberPreKeyUsed(
serviceId = aci,
kyberPreKeyId = 1,
signedPreKeyId = 1,
baseKey = publicKey
)
}
private fun getStaleTime(account: ServiceId, id: Int): Long? {
return SignalDatabase.rawDatabase
.select(KyberPreKeyTable.STALE_TIMESTAMP)
.from(KyberPreKeyTable.TABLE_NAME)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
}
@Test
fun handleMarkKyberPreKeyUsed_allowDuplicateNonLastResortKeyEntries() {
insertTestRecord(aci, id = 1, staleTime = 10, lastResort = false)
val publicKey = generateECPublicKey()
private fun ServiceId.toAccountId(): String {
return when (this) {
is ACI -> this.toString()
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
}
SignalDatabase.kyberPreKeys.handleMarkKyberPreKeyUsed(
serviceId = aci,
kyberPreKeyId = 1,
signedPreKeyId = 1,
baseKey = publicKey
)
SignalDatabase.kyberPreKeys.handleMarkKyberPreKeyUsed(
serviceId = aci,
kyberPreKeyId = 1,
signedPreKeyId = 1,
baseKey = publicKey
)
}
}

View File

@@ -0,0 +1,131 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.polls.PollOption
import org.thoughtcrime.securesms.polls.PollRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.testing.SignalActivityRule
@RunWith(AndroidJUnit4::class)
class PollTablesTest {
@get:Rule
val harness = SignalActivityRule()
private lateinit var poll1: PollRecord
@Before
fun setUp() {
poll1 = PollRecord(
id = 1,
question = "how do you feel about unit testing?",
pollOptions = listOf(
PollOption(1, "yay", listOf(1)),
PollOption(2, "ok", emptyList()),
PollOption(3, "nay", emptyList())
),
allowMultipleVotes = false,
hasEnded = false,
authorId = 1,
messageId = 1
)
SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollTable.TABLE_NAME)
SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollOptionTable.TABLE_NAME)
SignalDatabase.polls.writableDatabase.deleteAll(PollTables.PollVoteTable.TABLE_NAME)
val message = IncomingMessage(type = MessageType.NORMAL, from = harness.others[0], sentTimeMillis = 100, serverTimeMillis = 100, receivedTimeMillis = 100)
SignalDatabase.messages.insertMessageInbox(message, SignalDatabase.threads.getOrCreateThreadIdFor(harness.others[0], isGroup = false))
}
@Test
fun givenAPollWithVoting_whenIGetPoll_thenIExpectThatPoll() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 1, messageId = MessageId(1))
assertEquals(poll1, SignalDatabase.polls.getPoll(1))
}
@Test
fun givenAPoll_whenIGetItsOptionIds_thenIExpectAllOptionsIds() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
assertEquals(poll1.pollOptions.map { it.id }, SignalDatabase.polls.getPollOptionIds(1))
}
@Test
fun givenAPollAndVoter_whenIGetItsVoteCount_thenIExpectTheCorrectVoterCount() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 1, messageId = MessageId(1))
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(2), voterId = 2, voteCount = 2, messageId = MessageId(1))
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(3), voterId = 3, voteCount = 3, messageId = MessageId(1))
assertEquals(1, SignalDatabase.polls.getCurrentPollVoteCount(1, 1))
assertEquals(2, SignalDatabase.polls.getCurrentPollVoteCount(1, 2))
assertEquals(3, SignalDatabase.polls.getCurrentPollVoteCount(1, 3))
}
@Test
fun givenMultipleRoundsOfVoting_whenIGetItsCount_thenIExpectTheMostRecentResults() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(2), voterId = 1, voteCount = 1, messageId = MessageId(1))
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(3), voterId = 1, voteCount = 2, messageId = MessageId(1))
SignalDatabase.polls.insertVotes(pollId = 1, pollOptionIds = listOf(1), voterId = 1, voteCount = 3, messageId = MessageId(1))
assertEquals(poll1, SignalDatabase.polls.getPoll(1))
}
@Test
fun givenAPoll_whenITerminateIt_thenIExpectItToEnd() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
SignalDatabase.polls.endPoll(1, System.currentTimeMillis())
assertEquals(true, SignalDatabase.polls.getPoll(1)!!.hasEnded)
}
@Test
fun givenAPoll_whenIIVote_thenIExpectThatVote() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
val poll = SignalDatabase.polls.getPoll(1)!!
val pollOption = poll.pollOptions.first()
val voteCount = SignalDatabase.polls.insertVote(poll, pollOption)
assertEquals(1, voteCount)
assertEquals(listOf(0), SignalDatabase.polls.getVotes(poll.id, false))
}
@Test
fun givenAPoll_whenIRemoveVote_thenVoteIsCleared() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
val poll = SignalDatabase.polls.getPoll(1)!!
val pollOption = poll.pollOptions.first()
val voteCount = SignalDatabase.polls.removeVote(poll, pollOption)
SignalDatabase.polls.markPendingAsRemoved(poll.id, Recipient.self().id.toLong(), voteCount, 1)
assertEquals(1, voteCount)
val status = SignalDatabase.polls.getPollVoteStateForGivenVote(poll.id, voteCount)
assertEquals(PollTables.VoteState.REMOVED, status)
}
@Test
fun givenAVote_whenISetPollOptionId_thenOptionIdIsUpdated() {
SignalDatabase.polls.insertPoll("how do you feel about unit testing?", false, listOf("yay", "ok", "nay"), 1, 1)
val poll = SignalDatabase.polls.getPoll(1)!!
val option = poll.pollOptions.first()
SignalDatabase.polls.insertVotes(poll.id, listOf(option.id), Recipient.self().id.toLong(), 5, MessageId(1))
SignalDatabase.polls.setPollVoteStateForGivenVote(poll.id, Recipient.self().id.toLong(), 5, 1, true)
val status = SignalDatabase.polls.getPollVoteStateForGivenVote(poll.id, 5)
assertEquals(PollTables.VoteState.ADDED, status)
}
}

View File

@@ -38,7 +38,6 @@ class BackupDeleteJobTest {
@Before
fun setUp() {
mockkObject(RemoteConfig)
every { RemoteConfig.messageBackups } returns true
every { RemoteConfig.internalUser } returns true
every { RemoteConfig.defaultMaxBackoff } returns 1000L
@@ -53,17 +52,6 @@ class BackupDeleteJobTest {
unmockkAll()
}
@Test
fun givenBackupsNotEnabled_whenIRun_thenIExpectFailure() {
every { RemoteConfig.messageBackups } returns false
val job = BackupDeleteJob()
val result = job.run()
assertThat(result.isFailure).isTrue()
}
@Test
fun givenUserNotRegistered_whenIRun_thenIExpectFailure() {
mockkObject(SignalStore) {

View File

@@ -24,6 +24,7 @@ import org.junit.runner.RunWith
import org.signal.core.util.billing.BillingProduct
import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.billing.BillingPurchaseState
import org.signal.core.util.billing.BillingResponseCode
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.backup.DeletionState
@@ -64,10 +65,9 @@ class BackupSubscriptionCheckJobTest {
@Before
fun setUp() {
mockkObject(RemoteConfig)
every { RemoteConfig.messageBackups } returns true
every { RemoteConfig.internalUser } returns true
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
coEvery { AppDependencies.billingApi.queryPurchases() } returns mockk()
coEvery { AppDependencies.billingApi.queryProduct() } returns null
@@ -141,19 +141,9 @@ class BackupSubscriptionCheckJobTest {
}
}
@Test
fun givenRemoteBackupsNotAvailable_whenIRun_thenIExpectSuccessAndEarlyExit() {
every { RemoteConfig.messageBackups } returns false
val job = BackupSubscriptionCheckJob.create()
val result = job.run()
assertEarlyExit(result)
}
@Test
fun givenBillingApiNotAvailable_whenIRun_thenIExpectSuccessAndEarlyExit() {
coEvery { AppDependencies.billingApi.isApiAvailable() } returns false
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.BILLING_UNAVAILABLE
val job = BackupSubscriptionCheckJob.create()
val result = job.run()
@@ -183,16 +173,6 @@ class BackupSubscriptionCheckJobTest {
assertEarlyExit(result)
}
@Test
fun givenInternalOverrideIsSet_whenIRun_thenIExpectSuccessAndEarlyExit() {
SignalStore.backup.backupTierInternalOverride = MessageBackupTier.PAID
val job = BackupSubscriptionCheckJob.create()
val result = job.run()
assertEarlyExit(result)
}
@Test
fun givenAPendingPayment_whenIRun_thenIExpectSuccessAndEarlyExit() {
mockProduct()

View File

@@ -0,0 +1,322 @@
package org.thoughtcrime.securesms.messages
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageType
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.GroupTestingUtils.asMember
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
import org.whispersystems.signalservice.internal.push.DataMessage
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class DataMessageProcessorTest_polls {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var alice: Recipient
private lateinit var bob: Recipient
private lateinit var charlie: Recipient
private lateinit var groupId: GroupId.V2
private lateinit var groupRecipientId: RecipientId
@Before
fun setUp() {
alice = Recipient.resolved(harness.others[0])
bob = Recipient.resolved(harness.others[1])
charlie = Recipient.resolved(harness.others[2])
val groupInfo = GroupTestingUtils.insertGroup(revision = 0, harness.self.asMember(), alice.asMember(), bob.asMember())
groupId = groupInfo.groupId
groupRecipientId = groupInfo.recipientId
}
@Test
fun handlePollCreate_whenIHaveAValidPollProto_createPoll() {
val insertResult = handlePollCreate(
pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false),
senderRecipient = alice,
threadRecipient = Recipient.resolved(groupRecipientId),
groupId = groupId
)
assert(insertResult != null)
val poll = SignalDatabase.polls.getPoll(insertResult!!.messageId)
assert(poll != null)
assertThat(poll!!.question).isEqualTo("question?")
assertThat(poll.pollOptions.size).isEqualTo(3)
assertThat(poll.allowMultipleVotes).isEqualTo(false)
assertThat(poll.hasEnded).isEqualTo(false)
}
@Test
fun handlePollCreate_whenSenderIsNotInGroup_dropMessage() {
val insertResult = handlePollCreate(
pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false),
senderRecipient = charlie,
threadRecipient = Recipient.resolved(groupRecipientId),
groupId = groupId
)
assert(insertResult == null)
}
@Test
fun handlePollCreate_whenTargetRecipientIsNotAGroup_dropMessage() {
val insertResult = handlePollCreate(
pollCreate = DataMessage.PollCreate(question = "question?", options = listOf("a", "b", "c"), allowMultiple = false),
senderRecipient = alice,
threadRecipient = bob,
groupId = null
)
assert(insertResult == null)
}
@Test
fun handlePollTerminate_whenIHaveValidProto_endPoll() {
val pollMessageId = insertPoll()
val insertResult = DataMessageProcessor.handlePollTerminate(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(200),
message = DataMessage(pollTerminate = DataMessage.PollTerminate(targetSentTimestamp = 100)),
senderRecipient = alice,
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
threadRecipient = bob,
groupId = groupId,
receivedTime = 200
)
assert(insertResult?.messageId != null)
val poll = SignalDatabase.polls.getPoll(pollMessageId)
assert(poll != null)
assert(poll!!.hasEnded)
}
@Test
fun handlePollTerminate_whenIHaveDifferentTimestamp_dropMessage() {
insertPoll()
val insertResult = DataMessageProcessor.handlePollTerminate(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(200),
message = DataMessage(pollTerminate = DataMessage.PollTerminate(200)),
senderRecipient = alice,
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
threadRecipient = bob,
groupId = groupId,
receivedTime = 200
)
assert(insertResult == null)
}
@Test
fun handlePollTerminate_whenMessageIsNotFromCreatorOfPoll_dropMessage() {
insertPoll()
val insertResult = DataMessageProcessor.handlePollTerminate(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(200),
message = DataMessage(pollTerminate = DataMessage.PollTerminate(100)),
senderRecipient = bob,
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
threadRecipient = bob,
groupId = groupId,
receivedTime = 200
)
assert(insertResult == null)
}
@Test
fun handlePollTerminate_whenPollDoesNotExist_dropMessage() {
val insertResult = DataMessageProcessor.handlePollTerminate(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(200),
message = DataMessage(pollTerminate = DataMessage.PollTerminate(100)),
senderRecipient = alice,
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId()),
threadRecipient = bob,
groupId = groupId,
receivedTime = 200
)
assert(insertResult == null)
}
@Test
fun handlePollVote_whenValidPollVote_processVote() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0),
voteCount = 1
),
bob
)
assert(messageId != null)
assertThat(messageId!!.id).isEqualTo(1)
val poll = SignalDatabase.polls.getPoll(messageId.id)
assert(poll != null)
assertThat(poll!!.pollOptions[0].voterIds).isEqualTo(listOf(bob.id.toLong()))
}
@Test
fun handlePollVote_whenMultipleVoteAllowed_processAllVote() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = 1
),
bob
)
assert(messageId != null)
val poll = SignalDatabase.polls.getPoll(messageId!!.id)
assert(poll != null)
assertThat(poll!!.pollOptions[0].voterIds).isEqualTo(listOf(bob.id.toLong()))
assertThat(poll.pollOptions[1].voterIds).isEqualTo(listOf(bob.id.toLong()))
assertThat(poll.pollOptions[2].voterIds).isEqualTo(listOf(bob.id.toLong()))
}
@Test
fun handlePollVote_whenMultipleVoteSentToSingleVotePolls_dropMessage() {
insertPoll(false)
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = 1
),
bob
)
assert(messageId == null)
}
@Test
fun handlePollVote_whenVoteCountIsNotHigher_dropMessage() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = -1
),
bob
)
assert(messageId == null)
}
@Test
fun handlePollVote_whenVoteOptionDoesNotExist_dropMessage() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(5),
voteCount = 1
),
bob
)
assert(messageId == null)
}
@Test
fun handlePollVote_whenVoterNotInGroup_dropMessage() {
insertPoll()
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = 1
),
charlie
)
assert(messageId == null)
}
@Test
fun handlePollVote_whenPollDoesNotExist_dropMessage() {
val messageId = handlePollVote(
DataMessage.PollVote(
targetAuthorAciBinary = alice.asMember().aciBytes,
targetSentTimestamp = 100,
optionIndexes = listOf(0, 1, 2),
voteCount = 1
),
bob
)
assert(messageId == null)
}
private fun handlePollCreate(pollCreate: DataMessage.PollCreate, senderRecipient: Recipient, threadRecipient: Recipient, groupId: GroupId.V2?): MessageTable.InsertResult? {
return DataMessageProcessor.handlePollCreate(
envelope = MessageContentFuzzer.envelope(100),
message = DataMessage(pollCreate = pollCreate),
senderRecipient = senderRecipient,
threadRecipient = threadRecipient,
groupId = groupId,
receivedTime = 0,
context = ApplicationProvider.getApplicationContext(),
metadata = EnvelopeMetadata(alice.requireServiceId(), null, 1, false, null, harness.self.requireServiceId())
)
}
private fun handlePollVote(pollVote: DataMessage.PollVote, senderRecipient: Recipient): MessageId? {
return DataMessageProcessor.handlePollVote(
context = ApplicationProvider.getApplicationContext(),
envelope = MessageContentFuzzer.envelope(100),
message = DataMessage(pollVote = pollVote),
senderRecipient = senderRecipient,
earlyMessageCacheEntry = null
)
}
private fun insertPoll(allowMultiple: Boolean = true): Long {
val envelope = MessageContentFuzzer.envelope(100)
val pollMessage = IncomingMessage(type = MessageType.NORMAL, from = alice.id, sentTimeMillis = envelope.timestamp!!, serverTimeMillis = envelope.serverTimestamp!!, receivedTimeMillis = 0, groupId = groupId)
val messageId = SignalDatabase.messages.insertMessageInbox(pollMessage).get()
SignalDatabase.polls.insertPoll("question?", allowMultiple, listOf("a", "b", "c"), alice.id.toLong(), messageId.messageId)
return messageId.messageId
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages.protocol
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.libsignal.protocol.ReusedBaseKeyException
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.KyberPreKeysTestUtil
import org.whispersystems.signalservice.api.push.ServiceId
class BufferedKyberPreKeyStoreTest {
@get:Rule
val harness = SignalDatabaseRule()
private lateinit var aci: ServiceId
private lateinit var testSubject: BufferedKyberPreKeyStore
private lateinit var dataStore: BufferedSignalServiceAccountDataStore
@Before
fun setUp() {
SignalStore.account.generateAciIdentityKeyIfNecessary()
aci = harness.localAci
testSubject = BufferedKyberPreKeyStore(aci)
dataStore = BufferedSignalServiceAccountDataStore(aci)
}
@Test
fun givenALastResortKey_whenIMarkKyberPreKeyUsed_thenIExpectNoIssues() {
KyberPreKeysTestUtil.insertTestRecord(aci, 1, lastResort = true)
val publicKey = KyberPreKeysTestUtil.generateECPublicKey()
testSubject.markKyberPreKeyUsed(
kyberPreKeyId = 1,
signedPreKeyId = 2,
publicKey = publicKey
)
}
@Test(expected = ReusedBaseKeyException::class)
fun givenALastResortKey_whenIMarkKyberPreKeyUsedTwice_thenIExpectException() {
KyberPreKeysTestUtil.insertTestRecord(aci, 1, lastResort = true)
val publicKey = KyberPreKeysTestUtil.generateECPublicKey()
testSubject.markKyberPreKeyUsed(
kyberPreKeyId = 1,
signedPreKeyId = 2,
publicKey = publicKey
)
testSubject.markKyberPreKeyUsed(
kyberPreKeyId = 1,
signedPreKeyId = 2,
publicKey = publicKey
)
}
@Test
fun givenAMarkedLastResortKey_whenIFlushTwice_thenIExpectNoIssues() {
KyberPreKeysTestUtil.insertTestRecord(aci, 1, lastResort = true)
val publicKey = KyberPreKeysTestUtil.generateECPublicKey()
testSubject.markKyberPreKeyUsed(
kyberPreKeyId = 1,
signedPreKeyId = 2,
publicKey = publicKey
)
testSubject.flushToDisk(dataStore)
testSubject.flushToDisk(dataStore)
}
}

View File

@@ -12,6 +12,7 @@ import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.billing.BillingPurchaseState
import org.signal.core.util.billing.BillingResponseCode
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.database.InAppPaymentSubscriberTable
import org.thoughtcrime.securesms.database.SignalDatabase
@@ -94,7 +95,7 @@ class GooglePlayBillingPurchaseTokenMigrationJobTest {
)
)
coEvery { AppDependencies.billingApi.isApiAvailable() } returns false
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.BILLING_UNAVAILABLE
val job = GooglePlayBillingPurchaseTokenMigrationJob()
@@ -118,7 +119,7 @@ class GooglePlayBillingPurchaseTokenMigrationJobTest {
)
)
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.None
val job = GooglePlayBillingPurchaseTokenMigrationJob()
@@ -143,7 +144,7 @@ class GooglePlayBillingPurchaseTokenMigrationJobTest {
)
)
coEvery { AppDependencies.billingApi.isApiAvailable() } returns true
coEvery { AppDependencies.billingApi.getApiAvailability() } returns BillingResponseCode.OK
coEvery { AppDependencies.billingApi.queryPurchases() } returns BillingPurchaseResult.Success(
purchaseState = BillingPurchaseState.PURCHASED,
purchaseToken = "purchaseToken",

View File

@@ -20,8 +20,10 @@ 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.database.model.databaseprotos.RestoreDecisionState
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.keyvalue.NewAccount
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
@@ -125,6 +127,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
SignalDatabase.recipients.setProfileName(Recipient.self().id, ProfileName.fromParts("Tester", "McTesterson"))
SignalStore.settings.isMessageNotificationsEnabled = false
SignalStore.registration.restoreDecisionState = RestoreDecisionState.NewAccount
return Recipient.self()
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.util
import org.junit.Assert.assertEquals
import org.signal.core.util.readToSingleObject
import org.signal.core.util.requireLongOrNull
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.protocol.ecc.ECPublicKey
import org.signal.libsignal.protocol.kem.KEMKeyPair
import org.signal.libsignal.protocol.kem.KEMKeyType
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
import org.thoughtcrime.securesms.database.KyberPreKeyTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.security.SecureRandom
object KyberPreKeysTestUtil {
fun insertTestRecord(account: ServiceId, id: Int, staleTime: Long = 0, lastResort: Boolean = false) {
val kemKeyPair = KEMKeyPair.generate(KEMKeyType.KYBER_1024)
SignalDatabase.kyberPreKeys.insert(
serviceId = account,
keyId = id,
record = KyberPreKeyRecord(
id,
System.currentTimeMillis(),
kemKeyPair,
ECKeyPair.generate().privateKey.calculateSignature(kemKeyPair.publicKey.serialize())
),
lastResort = lastResort
)
val count = SignalDatabase.rawDatabase
.update(KyberPreKeyTable.TABLE_NAME)
.values(KyberPreKeyTable.STALE_TIMESTAMP to staleTime)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
assertEquals(1, count)
}
fun getStaleTime(account: ServiceId, id: Int): Long? {
return SignalDatabase.rawDatabase
.select(KyberPreKeyTable.STALE_TIMESTAMP)
.from(KyberPreKeyTable.TABLE_NAME)
.where("${KyberPreKeyTable.ACCOUNT_ID} = ? AND ${KyberPreKeyTable.KEY_ID} = $id", account.toAccountId())
.run()
.readToSingleObject { it.requireLongOrNull(KyberPreKeyTable.STALE_TIMESTAMP) }
}
fun generateECPublicKey(): ECPublicKey {
val byteArray = ByteArray(ECPublicKey.KEY_SIZE - 1)
SecureRandom().nextBytes(byteArray)
return ECPublicKey.fromPublicKeyBytes(byteArray)
}
private fun ServiceId.toAccountId(): String {
return when (this) {
is ACI -> this.toString()
is PNI -> KyberPreKeyTable.PNI_ACCOUNT_ID
}
}
}

View File

@@ -59,11 +59,11 @@ object TestMessages {
null
)
if (timestamp != null) {
TestDbUtils.setMessageReceived(insert, timestamp)
TestDbUtils.setMessageReceived(insert.messageId, timestamp)
}
SignalDatabase.messages.markAsSent(insert, true)
SignalDatabase.messages.markAsSent(insert.messageId, true)
return insert
return insert.messageId
}
fun insertIncomingTextMessage(other: Recipient, body: String, timestamp: Long? = null) {
val message = IncomingMessage(

View File

@@ -115,6 +115,7 @@ class ConversationElementGenerator {
null,
null,
null,
null,
-1,
null,
null,

View File

@@ -44,6 +44,8 @@ import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.polls.PollOption
import org.thoughtcrime.securesms.polls.PollRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
@@ -336,5 +338,17 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
override fun onUpdateSignalClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onViewResultsClicked(pollId: Long) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onViewPollClicked(messageId: Long) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -843,19 +843,12 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".registrationv3.olddevice.TransferAccountActivity"
<activity android:name=".registration.olddevice.TransferAccountActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="false"/>
<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=".registrationv3.ui.RegistrationActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden|adjustResize"
@@ -930,17 +923,7 @@
android:theme="@style/TextSecure.DialogActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
<activity android:name=".SmsSendtoActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SENDTO" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
<activity android:name=".SystemContactsEntrypointActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
@@ -991,7 +974,7 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".registrationv3.ui.restore.RemoteRestoreActivity"
<activity android:name=".registration.ui.restore.RemoteRestoreActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:exported="false"/>

View File

@@ -20,6 +20,7 @@ import org.signal.glide.common.executor.FrameDecoderExecutor;
import org.signal.glide.common.io.Reader;
import org.signal.glide.common.io.Writer;
import org.signal.glide.common.loader.Loader;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -238,9 +239,10 @@ public abstract class FrameSeqDecoder<R extends Reader, W extends Writer> {
return fullRect;
}
private void initCanvasBounds(Rect rect) {
private void initCanvasBounds(Rect rect) throws IOException {
fullRect = rect;
frameBuffer = ByteBuffer.allocate((rect.width() * rect.height() / (sampleSize * sampleSize) + 1) * 4);
int capacity = APNGDecoder.getSafeAllocationSize(fullRect.width(), fullRect.height(), sampleSize);
frameBuffer = ByteBuffer.allocate(capacity);
if (mWriter == null) {
mWriter = getWriter();
}

View File

@@ -38,6 +38,8 @@ public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
private int mLoopCount;
private final Paint paint = new Paint();
public static final int MAX_DIMENSION = 4096;
public static final long MAX_TOTAL_PIXELS = 64_000_000L;
private class SnapShot {
byte dispose_op;
@@ -126,8 +128,12 @@ public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
otherChunks.add(chunk);
}
}
frameBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
snapShot.byteBuffer = ByteBuffer.allocate((canvasWidth * canvasHeight / (sampleSize * sampleSize) + 1) * 4);
int capacity = getSafeAllocationSize(canvasWidth, canvasHeight, sampleSize);
frameBuffer = ByteBuffer.allocate(capacity);
snapShot.byteBuffer = ByteBuffer.allocate(capacity);
return new Rect(0, 0, canvasWidth, canvasHeight);
}
@@ -208,4 +214,27 @@ public class APNGDecoder extends FrameSeqDecoder<APNGReader, APNGWriter> {
Log.e(TAG, "Failed to render!", t);
}
}
public static int getSafeAllocationSize(int width, int height, int sampleSize) throws IOException {
if (width <= 0 || height <= 0 || width > MAX_DIMENSION || height > MAX_DIMENSION) {
throw new IOException("APNG dimensions exceed safe limits: " + width + "x" + height);
}
int capacity;
try {
int ss = Math.multiplyExact(sampleSize, sampleSize);
int canvasSize = Math.multiplyExact(width, height);
int pixelCount = canvasSize / ss + 1;
if (pixelCount <= 0 || pixelCount > MAX_TOTAL_PIXELS) {
throw new IOException("APNG pixel count exceeds safe limits: " + pixelCount);
}
capacity = Math.multiplyExact(pixelCount, 4);
} catch (ArithmeticException e) {
throw new IOException("Failed to multiply dimensions and sample size: " + width + "x" + height + " @ sample size " + sampleSize + " (overflow?)", e);
}
return capacity;
}
}

View File

@@ -299,10 +299,8 @@ public class ApplicationContext extends Application implements AppForegroundObse
}
public void checkFreeDiskSpace() {
if (RemoteConfig.messageBackups()) {
long availableBytes = BackupRepository.INSTANCE.getFreeStorageSpace().getBytes();
SignalStore.backup().setSpaceAvailableOnDiskBytes(availableBytes);
}
long availableBytes = BackupRepository.INSTANCE.getFreeStorageSpace().getBytes();
SignalStore.backup().setSpaceAvailableOnDiskBytes(availableBytes);
}
/**

View File

@@ -30,6 +30,8 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.polls.PollRecord;
import org.thoughtcrime.securesms.polls.PollOption;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@@ -143,5 +145,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onDisplayMediaNoLongerAvailableSheet();
void onShowUnverifiedProfileSheet(boolean forGroup);
void onUpdateSignalClicked();
void onViewResultsClicked(long pollId);
void onViewPollClicked(long messageId);
void onToggleVote(@NonNull PollRecord poll, @NonNull PollOption pollOption, Boolean isChecked);
}
}

View File

@@ -21,8 +21,8 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.setFragmentResult
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.BottomSheetUtil
@@ -106,7 +106,7 @@ fun DevicePinAuthEducationSheet(
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun DevicePinAuthEducationSheetPreview() {
Previews.BottomSheetPreview {

View File

@@ -30,9 +30,9 @@ import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.compose.ComposeFragment
/**
@@ -113,7 +113,7 @@ fun InviteScreen(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun InviteScreenPreview() {
Previews.Preview {

View File

@@ -18,12 +18,12 @@ import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
@@ -37,6 +37,8 @@ import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.layout.PaneExpansionAnchor
import androidx.compose.material3.adaptive.layout.ThreePaneScaffoldRole
import androidx.compose.material3.adaptive.layout.rememberPaneExpansionState
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.runtime.Composable
@@ -48,8 +50,8 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.fragment.app.DialogFragment
import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.rememberFragmentState
@@ -74,6 +76,7 @@ import org.signal.donations.StripeApi
import org.thoughtcrime.securesms.backup.v2.ArchiveRestoreProgress
import org.thoughtcrime.securesms.backup.v2.ui.verify.VerifyBackupKeyActivity
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
import org.thoughtcrime.securesms.calls.log.CallLogFilter
import org.thoughtcrime.securesms.calls.log.CallLogFragment
import org.thoughtcrime.securesms.calls.new.NewCallActivity
@@ -87,7 +90,6 @@ import org.thoughtcrime.securesms.components.settings.app.notifications.manual.N
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay
import org.thoughtcrime.securesms.conversation.v2.ShareDataTimestampViewModel
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment
@@ -100,6 +102,8 @@ import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.main.DetailsScreenNavHost
import org.thoughtcrime.securesms.main.InsetsViewModelUpdater
import org.thoughtcrime.securesms.main.MainBottomChrome
import org.thoughtcrime.securesms.main.MainBottomChromeCallback
import org.thoughtcrime.securesms.main.MainBottomChromeState
@@ -118,6 +122,13 @@ import org.thoughtcrime.securesms.main.MainToolbarViewModel
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
import org.thoughtcrime.securesms.main.NavigationBarSpacerCompat
import org.thoughtcrime.securesms.main.SnackbarState
import org.thoughtcrime.securesms.main.callNavGraphBuilder
import org.thoughtcrime.securesms.main.chatNavGraphBuilder
import org.thoughtcrime.securesms.main.navigateToDetailLocation
import org.thoughtcrime.securesms.main.rememberDetailNavHostController
import org.thoughtcrime.securesms.main.rememberFocusRequester
import org.thoughtcrime.securesms.main.rememberMainNavigationDetailLocation
import org.thoughtcrime.securesms.main.storiesNavGraphBuilder
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
import org.thoughtcrime.securesms.megaphone.Megaphone
@@ -297,7 +308,6 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
val mainNavigationState by mainNavigationViewModel.mainNavigationState.collectAsStateWithLifecycle()
val mainNavigationDetailLocation by mainNavigationViewModel.detailLocation.collectAsStateWithLifecycle()
LaunchedEffect(mainNavigationState.currentListLocation) {
when (mainNavigationState.currentListLocation) {
@@ -308,8 +318,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
}
val isNavigationVisible = remember(mainToolbarState.mode) {
mainToolbarState.mode == MainToolbarMode.FULL
val isNavigationVisible = mainToolbarState.mode == MainToolbarMode.FULL
val isBackHandlerEnabled = mainToolbarState.destination != MainNavigationListLocation.CHATS
BackHandler(enabled = isBackHandlerEnabled) {
mainNavigationViewModel.setFocusedPane(ThreePaneScaffoldRole.Secondary)
mainNavigationViewModel.goTo(MainNavigationListLocation.CHATS)
}
val mainBottomChromeState = remember(mainToolbarState.destination, snackbar, mainToolbarState.mode, megaphone) {
@@ -329,8 +343,79 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
MainContainer {
val wrappedNavigator = rememberNavigator(windowSizeClass, contentLayoutData, maxWidth)
val paneExpansionState = rememberPaneExpansionState()
val listPaneWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
val halfPartitionWidth = contentLayoutData.partitionWidth / 2
val detailOffset = if (mainToolbarState.mode == MainToolbarMode.SEARCH || mainToolbarState.mode == MainToolbarMode.ACTION_MODE) 0.dp else 72.dp
val detailOnlyAnchor = PaneExpansionAnchor.Offset.fromStart(detailOffset + contentLayoutData.listPaddingStart + halfPartitionWidth)
val detailAndListAnchor = PaneExpansionAnchor.Offset.fromStart(listPaneWidth + halfPartitionWidth)
val listOnlyAnchor = PaneExpansionAnchor.Offset.fromEnd(contentLayoutData.detailPaddingEnd - halfPartitionWidth)
val paneExpansionState = rememberPaneExpansionState(
anchors = listOf(detailOnlyAnchor, detailAndListAnchor, listOnlyAnchor)
)
val mutableInteractionSource = remember { MutableInteractionSource() }
val mainNavigationDetailLocation by rememberMainNavigationDetailLocation(mainNavigationViewModel)
val chatsNavHostController = rememberDetailNavHostController(
onRequestFocus = rememberFocusRequester(
mainNavigationViewModel = mainNavigationViewModel,
currentListLocation = mainNavigationState.currentListLocation,
isTargetListLocation = { it in listOf(MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE) }
)
) {
chatNavGraphBuilder()
}
val callsNavHostController = rememberDetailNavHostController(
onRequestFocus = rememberFocusRequester(
mainNavigationViewModel = mainNavigationViewModel,
currentListLocation = mainNavigationState.currentListLocation
) { it == MainNavigationListLocation.CALLS }
) {
callNavGraphBuilder(it)
}
val storiesNavHostController = rememberDetailNavHostController(
onRequestFocus = rememberFocusRequester(
mainNavigationViewModel = mainNavigationViewModel,
currentListLocation = mainNavigationState.currentListLocation
) { it == MainNavigationListLocation.STORIES }
) {
storiesNavGraphBuilder()
}
LaunchedEffect(mainNavigationDetailLocation) {
mainNavigationViewModel.clearEarlyDetailLocation()
when (mainNavigationDetailLocation) {
is MainNavigationDetailLocation.Empty -> {
when (mainNavigationState.currentListLocation) {
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> chatsNavHostController
MainNavigationListLocation.CALLS -> callsNavHostController
MainNavigationListLocation.STORIES -> storiesNavHostController
}.navigateToDetailLocation(mainNavigationDetailLocation)
}
is MainNavigationDetailLocation.Chats -> chatsNavHostController.navigateToDetailLocation(mainNavigationDetailLocation)
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(mainNavigationDetailLocation)
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(mainNavigationDetailLocation)
}
}
LaunchedEffect(mainNavigationDetailLocation) {
if (paneExpansionState.currentAnchor == listOnlyAnchor && wrappedNavigator.currentDestination?.pane == ThreePaneScaffoldRole.Primary) {
paneExpansionState.animateTo(detailOnlyAnchor)
}
}
LaunchedEffect(mainNavigationState.currentListLocation) {
if (paneExpansionState.currentAnchor == detailOnlyAnchor && wrappedNavigator.currentDestination?.pane == ThreePaneScaffoldRole.Secondary) {
paneExpansionState.animateTo(listOnlyAnchor)
}
}
InsetsViewModelUpdater()
AppScaffold(
navigator = wrappedNavigator,
@@ -432,35 +517,26 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
},
detailContent = {
when (val destination = mainNavigationDetailLocation) {
is MainNavigationDetailLocation.Conversation -> {
val fragmentState = key(destination) { rememberFragmentState() }
AndroidFragment(
clazz = ConversationFragment::class.java,
fragmentState = fragmentState,
arguments = requireNotNull(destination.intent.extras) { "Handed null Conversation intent arguments." },
modifier = Modifier
.padding(end = contentLayoutData.detailPaddingEnd)
.clip(contentLayoutData.shape)
.background(color = MaterialTheme.colorScheme.surface)
.fillMaxSize()
when (mainNavigationState.currentListLocation) {
MainNavigationListLocation.CHATS, MainNavigationListLocation.ARCHIVE -> {
DetailsScreenNavHost(
navHostController = chatsNavHostController,
contentLayoutData = contentLayoutData
)
}
MainNavigationDetailLocation.Empty -> {
Box(
modifier = Modifier
.padding(end = contentLayoutData.detailPaddingEnd)
.clip(contentLayoutData.shape)
.background(color = MaterialTheme.colorScheme.surface)
.fillMaxSize()
) {
Image(
painter = painterResource(R.drawable.ic_signal_logo_large),
contentDescription = null,
modifier = Modifier.align(Alignment.Center)
)
}
MainNavigationListLocation.CALLS -> {
DetailsScreenNavHost(
navHostController = callsNavHostController,
contentLayoutData = contentLayoutData
)
}
MainNavigationListLocation.STORIES -> {
DetailsScreenNavHost(
navHostController = storiesNavHostController,
contentLayoutData = contentLayoutData
)
}
}
},
@@ -521,8 +597,20 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
return remember(scaffoldNavigator, coroutine) {
mainNavigationViewModel.wrapNavigator(coroutine, scaffoldNavigator) { detailLocation ->
when (detailLocation) {
is MainNavigationDetailLocation.Conversation -> {
startActivity(detailLocation.intent)
is MainNavigationDetailLocation.Chats.Conversation -> {
startActivity(
ConversationIntents.createBuilderSync(this, detailLocation.conversationArgs.recipientId, detailLocation.conversationArgs.threadId)
.withArgs(detailLocation.conversationArgs)
.build()
)
}
is MainNavigationDetailLocation.Calls.CallLinks.CallLinkDetails -> {
startActivity(CallLinkDetailsActivity.createIntent(this, detailLocation.callLinkRoomId))
}
is MainNavigationDetailLocation.Calls.CallLinks.EditCallLinkName -> {
error("Unexpected subroute EditCallLinkName.")
}
MainNavigationDetailLocation.Empty -> Unit
@@ -744,7 +832,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
private fun handleConversationIntent(intent: Intent) {
if (ConversationIntents.isConversationIntent(intent)) {
mainNavigationViewModel.goTo(MainNavigationListLocation.CHATS)
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Conversation(intent))
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Chats.Conversation(ConversationIntents.readArgsFromBundle(intent.extras!!)))
}
}

View File

@@ -45,8 +45,8 @@ public class MainNavigator {
Disposable disposable = ConversationIntents.createBuilder(activity, recipientId, threadId)
.map(builder -> builder.withDistributionType(distributionType)
.withStartingPosition(startingPosition)
.build())
.subscribe(intent -> viewModel.goTo(new MainNavigationDetailLocation.Conversation(intent)));
.toConversationArgs())
.subscribe(args -> viewModel.goTo(new MainNavigationDetailLocation.Chats.Conversation(args)));
lifecycleDisposable.add(disposable);
}

View File

@@ -34,7 +34,6 @@ 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;
@@ -189,7 +188,6 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private boolean userCanTransferOrRestore() {
return !SignalStore.registration().isRegistrationComplete() &&
RemoteConfig.restoreAfterRegistration() &&
RestoreDecisionStateUtil.isDecisionPending(SignalStore.registration().getRestoreDecisionState());
}

View File

@@ -18,9 +18,9 @@ import org.thoughtcrime.securesms.util.Rfc5724Uri;
import java.net.URISyntaxException;
public class SmsSendtoActivity extends Activity {
public class SystemContactsEntrypointActivity extends Activity {
private static final String TAG = Log.tag(SmsSendtoActivity.class);
private static final String TAG = Log.tag(SystemContactsEntrypointActivity.class);
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -32,9 +32,7 @@ public class SmsSendtoActivity extends Activity {
private Intent getNextIntent(Intent original) {
DestinationAndBody destination;
if (original.getAction().equals(Intent.ACTION_SENDTO)) {
destination = getDestinationForSendTo(original);
} else if (original.getData() != null && "content".equals(original.getData().getScheme())) {
if (original.getData() != null && "content".equals(original.getData().getScheme())) {
destination = getDestinationForSyncAdapter(original);
} else {
destination = getDestinationForView(original);
@@ -64,11 +62,6 @@ public class SmsSendtoActivity extends Activity {
return nextIntent;
}
private @NonNull DestinationAndBody getDestinationForSendTo(Intent intent) {
return new DestinationAndBody(intent.getData().getSchemeSpecificPart(),
intent.getStringExtra("sms_body"));
}
private @NonNull DestinationAndBody getDestinationForView(Intent intent) {
try {
Rfc5724Uri smsUri = new Rfc5724Uri(intent.getData().toString());

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.audio;
import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaCodec;
@@ -191,6 +192,7 @@ public class AudioCodec implements Recorder {
return adtsHeader;
}
@SuppressLint("MissingPermission")
private AudioRecord createAudioRecord(int bufferSize) {
return new AudioRecord(MediaRecorder.AudioSource.MIC, SAMPLE_RATE,
AudioFormat.CHANNEL_IN_MONO,

View File

@@ -18,7 +18,6 @@ import org.thoughtcrime.securesms.util.MediaUtil
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.util.Optional
/**
* Renders Avatar objects into Media objects. This can involve creating a Bitmap, depending on the
@@ -132,6 +131,20 @@ object AvatarRenderer {
}
private fun createMedia(uri: Uri, size: Long): Media {
return Media(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), DIMENSIONS, DIMENSIONS, size, 0, false, false, Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty())
return Media(
uri = uri,
contentType = MediaUtil.IMAGE_JPEG,
date = System.currentTimeMillis(),
width = DIMENSIONS,
height = DIMENSIONS,
size = size,
duration = 0,
isBorderless = false,
isVideoGif = false,
bucketId = null,
caption = null,
transformProperties = null,
fileName = null
)
}
}

View File

@@ -27,8 +27,8 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars
@@ -94,7 +94,7 @@ fun FallbackAvatarImage(
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun FallbackAvatarImagePreview() {
Previews.Preview {

View File

@@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.avatar.text.TextAvatarCreationFragment
import org.thoughtcrime.securesms.avatar.vector.VectorAvatarCreationFragment
import org.thoughtcrime.securesms.components.ButtonStripItemView
import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration
import org.thoughtcrime.securesms.groups.ParcelableGroupId
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity
import org.thoughtcrime.securesms.mediasend.Media
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
@@ -57,9 +56,8 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) {
private fun createFactory(): AvatarPickerViewModel.Factory {
val args = AvatarPickerFragmentArgs.fromBundle(requireArguments())
val groupId = ParcelableGroupId.get(args.groupId)
return AvatarPickerViewModel.Factory(AvatarPickerRepository(requireContext()), groupId, args.isNewGroup, args.groupAvatarMedia)
return AvatarPickerViewModel.Factory(AvatarPickerRepository(requireContext()), args.groupId, args.isNewGroup, args.groupAvatarMedia)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

View File

@@ -168,8 +168,9 @@ object ArchiveUploadProgress {
fun onAttachmentSectionStarted(totalAttachmentBytes: Long) {
debugAttachmentStartTime = System.currentTimeMillis()
attachmentProgress.clear()
updateState {
it.copy(
ArchiveUploadProgressState(
state = ArchiveUploadProgressState.State.UploadMedia,
mediaUploadedBytes = 0,
mediaTotalBytes = totalAttachmentBytes
@@ -203,15 +204,24 @@ object ArchiveUploadProgress {
}
fun onMessageBackupFinishedEarly() {
updateState { PROGRESS_NONE }
resetState()
}
fun onValidationFailure() {
updateState { PROGRESS_NONE }
resetState()
}
fun onMainBackupFileUploadFailure() {
updateState { PROGRESS_NONE }
resetState()
}
private fun resetState() {
val shouldRevertToUploadMedia = SignalStore.backup.backsUpMedia && !AppDependencies.jobManager.areQueuesEmpty(UploadAttachmentToArchiveJob.QUEUES)
if (shouldRevertToUploadMedia) {
onAttachmentSectionStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes())
} else {
updateState { PROGRESS_NONE }
}
}
private fun updateState(

View File

@@ -1,16 +1,20 @@
package org.thoughtcrime.securesms.backup;
import android.Manifest;
import android.app.Notification;
import android.app.PendingIntent;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;
import org.signal.core.util.PendingIntentFlags;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.notifications.NotificationCancellationHelper;
@@ -26,6 +30,7 @@ public enum BackupFileIOError {
ATTACHMENT_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_contains_a_very_large_file),
UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups);
private static final String TAG = Log.tag(BackupFileIOError.class);
private static final short BACKUP_FAILED_ID = 31321;
private final @StringRes int titleId;
@@ -41,6 +46,11 @@ public enum BackupFileIOError {
}
public void postNotification(@NonNull Context context) {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
Log.w(TAG, "postNotification: Notification permission is not granted.");
return;
}
PendingIntent pendingIntent = PendingIntent.getActivity(context, -1, AppSettingsActivity.backups(context), PendingIntentFlags.mutable());
Notification backupFailedNotification = new NotificationCompat.Builder(context, NotificationChannels.getInstance().FAILURES)
.setSmallIcon(R.drawable.ic_signal_backup)

View File

@@ -119,6 +119,10 @@ object ExportSkips {
return log(sentTimestamp, "Failed to parse thread merge event.")
}
fun individualChatUpdateInWrongTypeOfChat(sentTimestamp: Long): String {
return log(sentTimestamp, "A chat update that only makes sense for individual chats was found in a different kind of chat.")
}
private fun log(sentTimestamp: Long, message: String): String {
return "[SKIP][$sentTimestamp] $message"
}
@@ -175,7 +179,7 @@ object ExportOddities {
}
fun unreadableLongTextAttachment(sentTimestamp: Long): String {
return log(sentTimestamp, "Long text attachment was unreadable. Falling back to the known body with an attachment pointer.")
return log(sentTimestamp, "Long text attachment was unreadable. Dropping the pointer.")
}
fun unopenableLongTextAttachment(sentTimestamp: Long): String {

View File

@@ -169,6 +169,18 @@ object ArchiveRestoreProgress {
val remainingRestoreSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize().bytes
var restoreState = SignalStore.backup.restoreState
if (restoreState.isMediaRestoreOperation) {
if (remainingRestoreSize == 0.bytes && SignalStore.backup.totalRestorableAttachmentSize == 0L) {
restoreState = RestoreState.NONE
SignalStore.backup.restoreState = restoreState
unregisterUpdateListeners()
} else {
registerUpdateListeners()
}
} else {
unregisterUpdateListeners()
}
val status = when {
!WifiConstraint.isMet(AppDependencies.application) && !SignalStore.backup.restoreWithCellular -> ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_WIFI
!NetworkConstraint.isMet(AppDependencies.application) -> ArchiveRestoreProgressState.RestoreStatus.WAITING_FOR_INTERNET
@@ -185,17 +197,6 @@ object ArchiveRestoreProgress {
}
}
if (restoreState.isMediaRestoreOperation) {
if (remainingRestoreSize == 0.bytes && SignalStore.backup.totalRestorableAttachmentSize == 0L) {
restoreState = RestoreState.NONE
SignalStore.backup.restoreState = restoreState
} else {
registerUpdateListeners()
}
} else {
unregisterUpdateListeners()
}
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize.bytes
state.copy(
@@ -203,7 +204,7 @@ object ArchiveRestoreProgress {
remainingRestoreSize = remainingRestoreSize,
restoreStatus = status,
totalRestoreSize = totalRestoreSize,
hasActivelyRestoredThisRun = state.hasActivelyRestoredThisRun || SignalStore.backup.totalRestorableAttachmentSize > 0,
hasActivelyRestoredThisRun = state.hasActivelyRestoredThisRun || totalRestoreSize > 0.bytes,
totalToRestoreThisRun = if (totalRestoreSize > 0.bytes) totalRestoreSize else state.totalToRestoreThisRun
)
}

View File

@@ -8,6 +8,8 @@ package org.thoughtcrime.securesms.backup.v2
import org.signal.core.util.ByteSize
import org.signal.core.util.bytes
import org.thoughtcrime.securesms.backup.RestoreState
import kotlin.math.max
import kotlin.math.min
/**
* In-memory view of the current state of an attachment restore process.
@@ -24,13 +26,15 @@ data class ArchiveRestoreProgressState(
val progress: Float? = when (this.restoreState) {
RestoreState.CALCULATING_MEDIA,
RestoreState.CANCELING_MEDIA -> this.completedRestoredSize.percentageOf(this.totalRestoreSize)
RestoreState.CANCELING_MEDIA -> {
max(0f, min(1f, this.completedRestoredSize.percentageOf(this.totalRestoreSize)))
}
RestoreState.RESTORING_MEDIA -> {
when (this.restoreStatus) {
RestoreStatus.NONE -> null
RestoreStatus.FINISHED -> 1f
else -> this.completedRestoredSize.percentageOf(this.totalRestoreSize)
else -> max(0f, min(1f, this.completedRestoredSize.percentageOf(this.totalRestoreSize)))
}
}

View File

@@ -72,6 +72,7 @@ import org.thoughtcrime.securesms.backup.v2.processor.RecipientArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.StickerArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.proto.BackupDebugInfo
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
import org.thoughtcrime.securesms.backup.v2.stream.BackupImportReader
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
@@ -80,6 +81,7 @@ import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.util.ArchiveAttachmentInfo
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
@@ -107,10 +109,14 @@ import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.jobs.CancelRestoreMediaJob
import org.thoughtcrime.securesms.jobs.CreateReleaseChannelJob
import org.thoughtcrime.securesms.jobs.LocalBackupJob
import org.thoughtcrime.securesms.jobs.MultiDeviceKeysUpdateJob
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.jobs.ResetSvrGuessCountJob
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob
import org.thoughtcrime.securesms.jobs.StorageForcePushJob
import org.thoughtcrime.securesms.jobs.Svr2MirrorJob
import org.thoughtcrime.securesms.jobs.UploadAttachmentToArchiveJob
import org.thoughtcrime.securesms.keyvalue.BackupValues.ArchiveServiceCredentials
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
@@ -129,6 +135,7 @@ import org.thoughtcrime.securesms.service.BackupProgressService
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.ServiceUtil
import org.thoughtcrime.securesms.util.TextSecurePreferences
import org.thoughtcrime.securesms.util.toMillis
import org.whispersystems.signalservice.api.AccountEntropyPool
import org.whispersystems.signalservice.api.ApplicationErrorAction
@@ -162,6 +169,7 @@ import java.io.File
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.math.BigDecimal
import java.time.ZonedDateTime
import java.util.Currency
import java.util.Locale
@@ -236,9 +244,22 @@ object BackupRepository {
resetInitializedStateAndAuthCredentials()
SignalStore.account.rotateAccountEntropyPool(stagedKeyRotations.aep)
SignalStore.backup.mediaRootBackupKey = stagedKeyRotations.mediaRootBackupKey
refreshMasterKeyDependents()
BackupMessagesJob.enqueue()
}
private fun refreshMasterKeyDependents() {
val jobs = buildList {
add(Svr2MirrorJob())
if (SignalStore.account.isMultiDevice) {
add(MultiDeviceKeysUpdateJob())
}
add(StorageForcePushJob())
}
AppDependencies.jobManager.addAll(jobs)
}
fun resetInitializedStateAndAuthCredentials() {
SignalStore.backup.backupsInitialized = false
SignalStore.backup.messageCredentials.clearAll()
@@ -541,7 +562,39 @@ object BackupRepository {
return false
}
return SignalStore.backup.hasBackupBeenUploaded && System.currentTimeMillis().milliseconds > SignalStore.backup.nextBackupFailureSheetSnoozeTime
val isRegistered = SignalStore.account.isRegistered && !TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application)
if (!isRegistered) {
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet for unregistered user.")
return false
}
if (SignalStore.backup.lastBackupTime <= 0) {
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet as the last backup time is unset.")
return false
}
if (!SignalStore.backup.hasBackupBeenUploaded) {
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet as a backup has never been uploaded.")
return false
}
val now = System.currentTimeMillis().milliseconds
val lastBackupTime = SignalStore.backup.lastBackupTime.milliseconds
val nextSnoozeTime = SignalStore.backup.nextBackupFailureSnoozeTime
val isLastBackupTimeAtLeastAWeekAgo = now - 7.days > lastBackupTime
if (!isLastBackupTimeAtLeastAWeekAgo) {
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet as the last backup time is less than a week ago.")
return false
}
val isNextSnoozeTimeBeforeNow = nextSnoozeTime < now
if (!isNextSnoozeTimeBeforeNow) {
Log.d(TAG, "[shouldDisplayCouldNotCompleteBackupSheet] Not displaying sheet as the next snooze time is in the future.")
return false
}
return true
}
fun snoozeDownloadYourBackupData() {
@@ -556,7 +609,14 @@ object BackupRepository {
return
}
if (!SignalStore.backup.backsUpMedia || !AppDependencies.jobManager.areQueuesEmpty(UploadAttachmentToArchiveJob.QUEUES)) {
if (!SignalStore.backup.backsUpMedia) {
return
}
if (!AppDependencies.jobManager.areQueuesEmpty(UploadAttachmentToArchiveJob.QUEUES)) {
if (SignalStore.backup.archiveUploadState?.state == ArchiveUploadProgressState.State.None) {
ArchiveUploadProgress.onAttachmentSectionStarted(SignalDatabase.attachments.getPendingArchiveUploadBytes())
}
return
}
@@ -607,7 +667,7 @@ object BackupRepository {
}
private fun shouldNotDisplayBackupFailedMessaging(): Boolean {
return !SignalStore.account.isRegistered || !RemoteConfig.messageBackups || !SignalStore.backup.areBackupsEnabled
return !SignalStore.account.isRegistered || !SignalStore.backup.areBackupsEnabled
}
/**
@@ -710,13 +770,18 @@ object BackupRepository {
append = { main.write(it) }
)
val maxBufferSize = 10_000
var totalAttachmentCount = 0
val attachmentInfos: MutableSet<ArchiveAttachmentInfo> = mutableSetOf()
export(
currentTime = System.currentTimeMillis(),
isLocal = true,
writer = writer,
progressEmitter = localBackupProgressEmitter,
cancellationSignal = cancellationSignal,
forTransfer = false
forTransfer = false,
extraFrameOperation = null
) { dbSnapshot ->
val localArchivableAttachments = dbSnapshot
.attachmentTable
@@ -752,7 +817,7 @@ object BackupRepository {
currentTime: Long,
progressEmitter: ExportProgressListener? = null,
cancellationSignal: () -> Boolean = { false },
extraExportOperations: ((SignalDatabase) -> Unit)?
extraFrameOperation: ((Frame) -> Unit)?
) {
val writer = EncryptedBackupWriter.createForSignalBackup(
key = messageBackupKey,
@@ -770,7 +835,8 @@ object BackupRepository {
forTransfer = false,
progressEmitter = progressEmitter,
cancellationSignal = cancellationSignal,
extraExportOperations = extraExportOperations
extraFrameOperation = extraFrameOperation,
endingExportOperation = null
)
}
@@ -799,7 +865,8 @@ object BackupRepository {
forTransfer = true,
progressEmitter = progressEmitter,
cancellationSignal = cancellationSignal,
extraExportOperations = null
extraFrameOperation = null,
endingExportOperation = null
)
}
@@ -813,8 +880,7 @@ object BackupRepository {
currentTime: Long = System.currentTimeMillis(),
forTransfer: Boolean = false,
progressEmitter: ExportProgressListener? = null,
cancellationSignal: () -> Boolean = { false },
extraExportOperations: ((SignalDatabase) -> Unit)? = null
cancellationSignal: () -> Boolean = { false }
) {
val writer: BackupExportWriter = if (plaintext) {
PlainTextBackupWriter(outputStream)
@@ -834,7 +900,8 @@ object BackupRepository {
forTransfer = forTransfer,
progressEmitter = progressEmitter,
cancellationSignal = cancellationSignal,
extraExportOperations = extraExportOperations
extraFrameOperation = null,
endingExportOperation = null
)
}
@@ -856,7 +923,8 @@ object BackupRepository {
forTransfer: Boolean,
progressEmitter: ExportProgressListener?,
cancellationSignal: () -> Boolean,
extraExportOperations: ((SignalDatabase) -> Unit)?
extraFrameOperation: ((Frame) -> Unit)?,
endingExportOperation: ((SignalDatabase) -> Unit)?
) {
val eventTimer = EventTimer()
val mainDbName = if (isLocal) LOCAL_MAIN_DB_SNAPSHOT_NAME else REMOTE_MAIN_DB_SNAPSHOT_NAME
@@ -894,8 +962,9 @@ object BackupRepository {
// We're using a snapshot, so the transaction is more for perf than correctness
dbSnapshot.rawWritableDatabase.withinTransaction {
progressEmitter?.onAccount()
AccountDataArchiveProcessor.export(dbSnapshot, signalStoreSnapshot) {
writer.write(it)
AccountDataArchiveProcessor.export(dbSnapshot, signalStoreSnapshot) { frame ->
writer.write(frame)
extraFrameOperation?.invoke(frame)
eventTimer.emit("account")
frameCount++
}
@@ -907,6 +976,7 @@ object BackupRepository {
progressEmitter?.onRecipient()
RecipientArchiveProcessor.export(dbSnapshot, signalStoreSnapshot, exportState, selfRecipientId, selfAci) {
writer.write(it)
extraFrameOperation?.invoke(it)
eventTimer.emit("recipient")
frameCount++
}
@@ -918,6 +988,7 @@ object BackupRepository {
progressEmitter?.onThread()
ChatArchiveProcessor.export(dbSnapshot, exportState) { frame ->
writer.write(frame)
extraFrameOperation?.invoke(frame)
eventTimer.emit("thread")
frameCount++
}
@@ -928,6 +999,7 @@ object BackupRepository {
progressEmitter?.onCall()
AdHocCallArchiveProcessor.export(dbSnapshot, exportState) { frame ->
writer.write(frame)
extraFrameOperation?.invoke(frame)
eventTimer.emit("call")
frameCount++
}
@@ -939,6 +1011,7 @@ object BackupRepository {
progressEmitter?.onSticker()
StickerArchiveProcessor.export(dbSnapshot) { frame ->
writer.write(frame)
extraFrameOperation?.invoke(frame)
eventTimer.emit("sticker-pack")
frameCount++
}
@@ -950,6 +1023,7 @@ object BackupRepository {
progressEmitter?.onNotificationProfile()
NotificationProfileProcessor.export(dbSnapshot, exportState) { frame ->
writer.write(frame)
extraFrameOperation?.invoke(frame)
eventTimer.emit("notification-profile")
frameCount++
}
@@ -961,6 +1035,7 @@ object BackupRepository {
progressEmitter?.onChatFolder()
ChatFolderProcessor.export(dbSnapshot, exportState) { frame ->
writer.write(frame)
extraFrameOperation?.invoke(frame)
eventTimer.emit("chat-folder")
frameCount++
}
@@ -974,6 +1049,7 @@ object BackupRepository {
progressEmitter?.onMessage(0, approximateMessageCount)
ChatItemArchiveProcessor.export(dbSnapshot, exportState, selfRecipientId, cancellationSignal) { frame ->
writer.write(frame)
extraFrameOperation?.invoke(frame)
eventTimer.emit("message")
frameCount++
@@ -989,7 +1065,7 @@ object BackupRepository {
}
}
extraExportOperations?.invoke(dbSnapshot)
endingExportOperation?.invoke(dbSnapshot)
Log.d(TAG, "[export] totalFrames: $frameCount | ${eventTimer.stop().summary}")
} finally {
@@ -1334,6 +1410,10 @@ object BackupRepository {
AppDependencies.recipientCache.warmUp()
SignalDatabase.threads.clearCache()
if (SignalStore.svr.pin?.isNotBlank() == true) {
AppDependencies.jobManager.add(ResetSvrGuessCountJob())
}
val stickerJobs = SignalDatabase.stickers.getAllStickerPacks().use { cursor ->
val reader = StickerTable.StickerPackRecordReader(cursor)
reader
@@ -1738,7 +1818,7 @@ object BackupRepository {
return RestoreTimestampResult.Success(SignalStore.backup.lastBackupTime)
}
timestampResult is NetworkResult.StatusCodeError && timestampResult.code == 404 -> {
timestampResult is NetworkResult.StatusCodeError && (timestampResult.code == 401 || timestampResult.code == 404) -> {
Log.i(TAG, "No backup file exists")
SignalStore.backup.lastBackupTime = 0L
SignalStore.backup.isBackupTimestampRestored = true
@@ -1813,7 +1893,7 @@ object BackupRepository {
}
}
suspend fun getAvailableBackupsTypes(availableBackupTiers: List<MessageBackupTier>): List<MessageBackupsType> {
suspend fun getBackupTypes(availableBackupTiers: List<MessageBackupTier>): List<MessageBackupsType> {
return availableBackupTiers.mapNotNull {
val type = getBackupsType(it)
@@ -1830,8 +1910,9 @@ object BackupRepository {
@WorkerThread
fun getBackupLevelConfiguration(): NetworkResult<SubscriptionsConfiguration.BackupLevelConfiguration> {
return AppDependencies.donationsApi
return AppDependencies.donationsService
.getDonationsConfiguration(Locale.getDefault())
.toNetworkResult()
.then {
val config = it.backupConfiguration.backupLevelConfigurationMap[SubscriptionsConfiguration.BACKUPS_LEVEL]
if (config != null) {
@@ -1844,8 +1925,9 @@ object BackupRepository {
@WorkerThread
fun getFreeType(): NetworkResult<MessageBackupsType.Free> {
return AppDependencies.donationsApi
return AppDependencies.donationsService
.getDonationsConfiguration(Locale.getDefault())
.toNetworkResult()
.map {
MessageBackupsType.Free(
mediaRetentionDays = it.backupConfiguration.freeTierMediaDays
@@ -1859,9 +1941,11 @@ object BackupRepository {
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrNull()?.activeSubscription?.let {
FiatMoney.fromSignalNetworkAmount(it.amount, Currency.getInstance(it.currency))
}
} else {
} else if (AppDependencies.billingApi.getApiAvailability().isSuccess) {
Log.d(TAG, "Accessing price via billing api.")
AppDependencies.billingApi.queryProduct()?.price
} else {
FiatMoney(BigDecimal.ZERO, SignalStore.inAppPayments.getRecurringDonationCurrency())
}
if (productPrice == null) {
@@ -1895,15 +1979,14 @@ object BackupRepository {
* prevents early initialization with incorrect keys before we have restored them.
*/
private fun initBackupAndFetchAuth(): NetworkResult<ArchiveServiceAccessPair> {
return if (!RemoteConfig.messageBackups) {
NetworkResult.StatusCodeError(555, null, null, emptyMap(), NonSuccessfulResponseCodeException(555, "Backups disabled!"))
} else if (SignalStore.backup.backupsInitialized || SignalStore.account.isLinkedDevice) {
return if (SignalStore.backup.backupsInitialized || SignalStore.account.isLinkedDevice) {
getArchiveServiceAccessPair()
.runOnStatusCodeError(resetInitializedStateErrorAction)
.runOnApplicationError(clearAuthCredentials)
} else if (isPreRestoreDuringRegistration()) {
Log.w(TAG, "Requesting/using auth credentials in pre-restore state", Throwable())
getArchiveServiceAccessPair()
.runOnApplicationError(clearAuthCredentials)
} else {
val messageBackupKey = SignalStore.backup.messageBackupKey
val mediaRootBackupKey = SignalStore.backup.mediaRootBackupKey
@@ -1955,8 +2038,7 @@ object BackupRepository {
private fun isPreRestoreDuringRegistration(): Boolean {
return !SignalStore.registration.isRegistrationComplete &&
SignalStore.registration.restoreDecisionState.isDecisionPending &&
RemoteConfig.restoreAfterRegistration
SignalStore.registration.restoreDecisionState.isDecisionPending
}
private fun scheduleSyncForAccountChange() {
@@ -2051,7 +2133,7 @@ object BackupRepository {
val messageBackupKey = SignalStore.backup.messageBackupKey
Log.i(TAG, "[remoteRestore] Fetching SVRB data")
val svrBAuth = when (val result = BackupRepository.getSvrBAuth()) {
val svrBAuth = when (val result = getSvrBAuth()) {
is NetworkResult.Success -> result.result
is NetworkResult.NetworkError -> return RemoteRestoreResult.NetworkError.logW(TAG, "[remoteRestore] Network error when getting SVRB auth.", result.getCause())
is NetworkResult.StatusCodeError -> return RemoteRestoreResult.NetworkError.logW(TAG, "[remoteRestore] Status code error when getting SVRB auth.", result.getCause())

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTable.Companion.DATE_RECEIVED
import org.thoughtcrime.securesms.database.MessageTable.Companion.EXPIRES_IN
import org.thoughtcrime.securesms.database.MessageTable.Companion.PARENT_STORY_ID
import org.thoughtcrime.securesms.database.MessageTable.Companion.SCHEDULED_DATE
import org.thoughtcrime.securesms.database.MessageTable.Companion.STORY_TYPE
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -66,7 +67,7 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
${MessageTable.MESSAGE_EXTRAS},
${MessageTable.VIEW_ONCE}
)
WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0
WHERE $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1
""".trimMargin()
)
Log.d(TAG, "Creating index took ${System.currentTimeMillis() - startTime} ms")
@@ -133,7 +134,7 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
PARENT_STORY_ID
)
.from("${MessageTable.TABLE_NAME} INDEXED BY $dateReceivedIndex")
.where("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND ($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds}) AND $DATE_RECEIVED >= $lastSeenReceivedTime")
.where("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND ($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds}) AND $DATE_RECEIVED >= $lastSeenReceivedTime")
.limit(count)
.orderBy("$DATE_RECEIVED ASC")
.run()

View File

@@ -398,7 +398,7 @@ class ChatItemArchiveExporter(
if (record.latestRevisionId == null) {
builder.revisions = revisionMap.remove(record.id)?.repairRevisions(builder) ?: emptyList()
val chatItem = builder.build().validateChatItem() ?: continue
val chatItem = builder.build().validateChatItem(exportState) ?: continue
buffer += chatItem
} else {
var previousEdits = revisionMap[record.latestRevisionId]
@@ -549,7 +549,7 @@ private fun BackupMessageRecord.toBasicChatItemBuilder(selfRecipientId: Recipien
dateReceived = dateReceived
)
if (expiresInMs != null && outgoing?.sendStatus?.all { it.pending == null && it.failed == null } == true) {
if (expiresInMs != null && outgoing?.sendStatus?.all { it.pending == null && it.failed == null } == true && expireStartDate == null) {
Log.w(TAG, ExportOddities.outgoingMessageWasSentButTimerNotStarted(record.dateSent))
expireStartDate = record.dateReceived
}
@@ -1041,19 +1041,22 @@ private fun BackupMessageRecord.getBodyText(attachments: List<DatabaseAttachment
}
if (longTextAttachment.uri == null || longTextAttachment.transferState != AttachmentTable.TRANSFER_PROGRESS_DONE) {
return StringUtil.trimToFit(this.body.emptyIfNull(), MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER) to longTextAttachment
Log.w(TAG, ExportOddities.undownloadedLongTextAttachment(this.dateSent))
val body = StringUtil.trimToFit(this.body.emptyIfNull(), MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER)
return body to longTextAttachment.takeUnless { body.isBlank() }
}
val longText = try {
PartAuthority.getAttachmentStream(AppDependencies.application, longTextAttachment.uri!!)?.readFully()?.toString(Charsets.UTF_8)
} catch (e: IOException) {
Log.w(TAG, ExportOddities.unreadableLongTextAttachment(this.dateSent))
return this.body.emptyIfNull() to longTextAttachment
return this.body.emptyIfNull() to null
}
if (longText == null) {
Log.w(TAG, ExportOddities.unopenableLongTextAttachment(this.dateSent))
return StringUtil.trimToFit(this.body.emptyIfNull(), MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER) to longTextAttachment
val body = StringUtil.trimToFit(this.body.emptyIfNull(), MAX_INLINED_BODY_SIZE_WITH_LONG_ATTACHMENT_POINTER)
return body to longTextAttachment.takeUnless { body.isBlank() }
}
val trimmed = StringUtil.trimToFit(longText, MAX_INLINED_BODY_SIZE)
@@ -1376,7 +1379,7 @@ private fun List<GroupReceiptTable.GroupReceiptInfo>?.toRemoteSendStatus(message
reason = SendStatus.Failed.FailureReason.NETWORK
)
}
MessageTypes.isFailedMessageType(messageRecord.type) -> {
it.status == GroupReceiptTable.STATUS_FAILED -> {
statusBuilder.failed = SendStatus.Failed(
reason = SendStatus.Failed.FailureReason.UNKNOWN
)
@@ -1510,7 +1513,7 @@ private fun <T> ExecutorService.submitTyped(callable: Callable<T>): Future<T> {
return this.submit(callable)
}
fun ChatItem.validateChatItem(): ChatItem? {
private fun ChatItem.validateChatItem(exportState: ExportState): ChatItem? {
if (this.standardMessage == null &&
this.contactMessage == null &&
this.stickerMessage == null &&
@@ -1524,10 +1527,24 @@ fun ChatItem.validateChatItem(): ChatItem? {
Log.w(TAG, ExportSkips.emptyChatItem(this.dateSent))
return null
}
if (this.updateMessage != null && this.updateMessage.isOnlyForIndividualChats() && exportState.threadIdToRecipientId[this.chatId] !in exportState.contactRecipientIds) {
Log.w(TAG, ExportSkips.individualChatUpdateInWrongTypeOfChat(this.dateSent))
return null
}
return this
}
fun List<ChatItem>.repairRevisions(current: ChatItem.Builder): List<ChatItem> {
private fun ChatUpdateMessage.isOnlyForIndividualChats(): Boolean {
return this.simpleUpdate?.type == SimpleChatUpdate.Type.JOINED_SIGNAL ||
this.simpleUpdate?.type == SimpleChatUpdate.Type.END_SESSION ||
this.simpleUpdate?.type == SimpleChatUpdate.Type.CHAT_SESSION_REFRESH ||
this.simpleUpdate?.type == SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST ||
this.simpleUpdate?.type == SimpleChatUpdate.Type.PAYMENTS_ACTIVATED
}
private fun List<ChatItem>.repairRevisions(current: ChatItem.Builder): List<ChatItem> {
return if (current.standardMessage != null) {
val filtered = this
.filter { it.standardMessage != null }

View File

@@ -1073,7 +1073,7 @@ class ChatItemArchiveImporter(
this.read != null -> GroupReceiptTable.STATUS_READ
this.viewed != null -> GroupReceiptTable.STATUS_VIEWED
this.skipped != null -> GroupReceiptTable.STATUS_SKIPPED
this.failed != null -> GroupReceiptTable.STATUS_UNKNOWN
this.failed != null -> GroupReceiptTable.STATUS_FAILED
else -> GroupReceiptTable.STATUS_UNKNOWN
}
}

View File

@@ -37,8 +37,8 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.parcelize.Parcelize
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupRepository
@@ -434,7 +434,7 @@ private fun rememberSecondaryAction(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BackupAlertSheetContentPreviewGeneric() {
Previews.BottomSheetPreview {
@@ -446,7 +446,7 @@ private fun BackupAlertSheetContentPreviewGeneric() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BackupAlertSheetContentPreviewPayment() {
Previews.BottomSheetPreview {
@@ -458,7 +458,7 @@ private fun BackupAlertSheetContentPreviewPayment() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BackupAlertSheetContentPreviewDelete() {
Previews.BottomSheetPreview {
@@ -473,7 +473,7 @@ private fun BackupAlertSheetContentPreviewDelete() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BackupAlertSheetContentPreviewDiskFull() {
Previews.BottomSheetPreview {
@@ -485,7 +485,7 @@ private fun BackupAlertSheetContentPreviewDiskFull() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BackupAlertSheetContentPreviewBackupFailed() {
Previews.BottomSheetPreview {
@@ -497,7 +497,7 @@ private fun BackupAlertSheetContentPreviewBackupFailed() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
Previews.BottomSheetPreview {
@@ -509,7 +509,7 @@ private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BackupAlertSheetContentPreviewSubscriptionExpired() {
Previews.BottomSheetPreview {

View File

@@ -31,8 +31,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.signal.core.ui.R as CoreUiR
@@ -201,7 +201,7 @@ private fun BackupAlertSecondaryActionButton(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BackupAlertBottomSheetContainerPreview() {
Previews.BottomSheetPreview {

View File

@@ -14,8 +14,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
@@ -27,9 +27,10 @@ import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.backups.BackupStateObserver
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.signal.core.ui.R as CoreUiR
@@ -52,15 +53,15 @@ class CreateBackupBottomSheet : ComposeBottomSheetDialogFragment() {
@Composable
override fun SheetContent() {
val isPaidTier: Boolean = remember { BackupStateObserver.getNonIOBackupState().isLikelyPaidTier() }
CreateBackupBottomSheetContent(
isPaidTier = isPaidTier,
onBackupNowClick = {
BackupMessagesJob.enqueue()
setFragmentResult(REQUEST_KEY, bundleOf(REQUEST_KEY to Result.BACKUP_STARTED))
isResultSet = true
dismissAllowingStateLoss()
},
onBackupLaterClick = {
dismissAllowingStateLoss()
}
)
}
@@ -81,8 +82,8 @@ class CreateBackupBottomSheet : ComposeBottomSheetDialogFragment() {
@Composable
private fun CreateBackupBottomSheetContent(
onBackupNowClick: () -> Unit,
onBackupLaterClick: () -> Unit
isPaidTier: Boolean,
onBackupNowClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
@@ -107,8 +108,14 @@ private fun CreateBackupBottomSheetContent(
textAlign = TextAlign.Center
)
val body = if (isPaidTier) {
stringResource(id = R.string.CreateBackupBottomSheet__depending_on_the_size)
} else {
stringResource(id = R.string.CreateBackupBottomSheet__free_tier)
}
Text(
text = stringResource(id = R.string.CreateBackupBottomSheet__depending_on_the_size),
text = body,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
@@ -121,28 +128,30 @@ private fun CreateBackupBottomSheetContent(
modifier = Modifier.widthIn(min = 220.dp)
) {
Text(
text = stringResource(id = R.string.CreateBackupBottomSheet__back_up_now)
)
}
TextButton(
onClick = onBackupLaterClick,
modifier = Modifier.widthIn(min = 220.dp).padding(top = 16.dp)
) {
Text(
text = stringResource(id = R.string.CreateBackupBottomSheet__back_up_later)
text = stringResource(id = android.R.string.ok)
)
}
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun CreateBackupBottomSheetContentPreview() {
private fun CreateBackupBottomSheetContentPaidPreview() {
Previews.BottomSheetPreview {
CreateBackupBottomSheetContent(
onBackupNowClick = {},
onBackupLaterClick = {}
isPaidTier = true,
onBackupNowClick = {}
)
}
}
@DayNightPreviews
@Composable
private fun CreateBackupBottomSheetContentFreePreview() {
Previews.BottomSheetPreview {
CreateBackupBottomSheetContent(
isPaidTier = false,
onBackupNowClick = {}
)
}
}

View File

@@ -11,9 +11,9 @@ import androidx.compose.ui.res.stringResource
import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.compose.ComposeDialogFragment
@@ -70,7 +70,7 @@ private fun DownloadYourBackupTodayDialogContent(
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun DownloadYourBackupTodayDialogContentPreview() {
Previews.Preview {

View File

@@ -18,9 +18,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@@ -35,8 +33,8 @@ import androidx.core.os.BundleCompat
import org.signal.core.ui.R
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.util.gibiBytes
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
@@ -158,7 +156,7 @@ private fun SheetContent(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BackupAlertSheetContentPreviewMedia() {
Previews.BottomSheetPreview {

View File

@@ -12,8 +12,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
@@ -71,7 +71,7 @@ private fun NoManualBackupSheetContent(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun NoManualBackupSheetContentPreview() {
Previews.BottomSheetPreview {

View File

@@ -19,8 +19,8 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.components.contactsupport.ContactSupportDialogFragment
@@ -108,7 +108,7 @@ private fun NoRemoteStorageSpaceAvailableBottomSheetContent(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun NoRemoteStorageSpaceAvailableBottomSheetContentPreview() {
Previews.BottomSheetPreview {

View File

@@ -29,8 +29,8 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withLink
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.signal.core.ui.R as CoreUiR
@@ -112,7 +112,7 @@ private fun BackupAlertText(text: AnnotatedString, inlineContent: Map<String, In
)
}
@SignalPreview
@DayNightPreviews
@Composable
fun BackupStatusRowCouldNotCompleteBackupPreview() {
Previews.Preview {
@@ -120,7 +120,7 @@ fun BackupStatusRowCouldNotCompleteBackupPreview() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun BackupStatusRowBackupFailedPreview() {
Previews.Preview {

View File

@@ -35,8 +35,8 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.util.mebiBytes
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.RestoreState
@@ -309,7 +309,7 @@ private fun ArchiveRestoreProgressState.actionResource(): Int {
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun BackupStatusBannerPreview() {
Previews.Preview {

View File

@@ -21,9 +21,9 @@ import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Rows
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.util.mebiBytes
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.RestoreState
@@ -98,7 +98,7 @@ fun BackupStatusRow(
if (backupStatusData.restoreStatus == RestoreStatus.NOT_ENOUGH_DISK_SPACE) {
BackupAlertText(
text = stringResource(R.string.BackupStatusRow__not_enough_space, backupStatusData.remainingRestoreSize)
text = stringResource(R.string.BackupStatusRow__not_enough_space, backupStatusData.remainingRestoreSize.toUnitString())
)
Rows.TextRow(
@@ -221,7 +221,7 @@ private fun progressColor(backupStatusData: ArchiveRestoreProgressState): Color
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun BackupStatusRowNormalPreview() {
Previews.Preview {
@@ -232,7 +232,7 @@ fun BackupStatusRowNormalPreview() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun BackupStatusRowWaitingForWifiPreview() {
Previews.Preview {
@@ -242,7 +242,7 @@ fun BackupStatusRowWaitingForWifiPreview() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun BackupStatusRowWaitingForInternetPreview() {
Previews.Preview {
@@ -252,7 +252,7 @@ fun BackupStatusRowWaitingForInternetPreview() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun BackupStatusRowLowBatteryPreview() {
Previews.Preview {
@@ -262,7 +262,7 @@ fun BackupStatusRowLowBatteryPreview() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun BackupStatusRowFinishedPreview() {
Previews.Preview {
@@ -273,7 +273,7 @@ fun BackupStatusRowFinishedPreview() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
fun BackupStatusRowNotEnoughFreeSpacePreview() {
Previews.Preview {

View File

@@ -43,9 +43,9 @@ import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.horizontalGutters
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.fonts.MonoTypeface
import org.thoughtcrime.securesms.registrationv3.ui.restore.BackupKeyVisualTransformation
import org.thoughtcrime.securesms.registrationv3.ui.restore.attachBackupKeyAutoFillHelper
import org.thoughtcrime.securesms.registrationv3.ui.restore.backupKeyAutoFillHelper
import org.thoughtcrime.securesms.registration.ui.restore.BackupKeyVisualTransformation
import org.thoughtcrime.securesms.registration.ui.restore.attachBackupKeyAutoFillHelper
import org.thoughtcrime.securesms.registration.ui.restore.backupKeyAutoFillHelper
import org.whispersystems.signalservice.api.AccountEntropyPool
/**

View File

@@ -0,0 +1,231 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.res.stringResource
import com.google.android.gms.common.ConnectionResult
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
/**
* Represents the availability status of Google Play Services on the device.
*
* Maps Google Play Services ConnectionResult codes to enum values for easier handling
* in the application. Each enum value corresponds to a specific state that determines
* what dialog or action should be presented to the user.
*
* @param code The corresponding ConnectionResult code from Google Play Services
*/
enum class GooglePlayServicesAvailability(val code: Int) {
/** An unknown code. Possibly due to an update on Google's end */
UNKNOWN(code = Int.MIN_VALUE),
/** Google Play Services is available and ready to use */
SUCCESS(code = ConnectionResult.SUCCESS),
/** Google Play Services is not installed on the device */
SERVICE_MISSING(code = ConnectionResult.SERVICE_MISSING),
/** Google Play Services is currently being updated */
SERVICE_UPDATING(code = ConnectionResult.SERVICE_UPDATING),
/** Google Play Services requires an update to a newer version */
SERVICE_VERSION_UPDATE_REQUIRED(code = ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED),
/** Google Play Services is installed but disabled by the user */
SERVICE_DISABLED(code = ConnectionResult.SERVICE_DISABLED),
/** Google Play Services installation is invalid or corrupted */
SERVICE_INVALID(code = ConnectionResult.SERVICE_INVALID);
companion object {
private val TAG = Log.tag(GooglePlayServicesAvailability::class)
/**
* Converts a Google Play Services ConnectionResult code to the corresponding enum value.
*
* @param code The ConnectionResult code from Google Play Services
* @return The matching GooglePlayServicesAvailability enum value
*/
fun fromCode(code: Int): GooglePlayServicesAvailability {
val availability = entries.firstOrNull { it.code == code } ?: UNKNOWN
if (availability == UNKNOWN) {
Log.w(TAG, "Unknown availability code: $code")
}
return availability
}
}
}
/**
* Displays a dialog based on the Google Play Services availability status.
*
* Shows different dialogs with appropriate messages and actions depending on whether
* Google Play Services is missing, updating, requires an update, is disabled, or invalid.
* When availability is SUCCESS, automatically calls onDismissRequest to dismiss any dialog.
*
* @param onDismissRequest Callback invoked when the dialog is dismissed or when SUCCESS status is received
* @param onLearnMoreClick Callback invoked when the "Learn More" action is selected
* @param onMakeServicesAvailableClick Callback invoked when an action to make services
* available is selected (e.g., install or update)
* @param googlePlayServicesAvailability The current availability status of Google Play Services
*/
@Composable
fun GooglePlayServicesAvailabilityDialog(
onDismissRequest: () -> Unit,
onLearnMoreClick: () -> Unit,
onMakeServicesAvailableClick: () -> Unit,
googlePlayServicesAvailability: GooglePlayServicesAvailability
) {
when (googlePlayServicesAvailability) {
GooglePlayServicesAvailability.SUCCESS -> {
LaunchedEffect(Unit) {
onDismissRequest()
}
}
GooglePlayServicesAvailability.SERVICE_MISSING, GooglePlayServicesAvailability.UNKNOWN -> {
ServiceMissingDialog(
onDismissRequest = onDismissRequest,
onInstallPlayServicesClick = onMakeServicesAvailableClick
)
}
GooglePlayServicesAvailability.SERVICE_UPDATING -> {
ServiceUpdatingDialog(onDismissRequest = onDismissRequest)
}
GooglePlayServicesAvailability.SERVICE_VERSION_UPDATE_REQUIRED -> {
ServiceVersionUpdateRequiredDialog(
onDismissRequest = onDismissRequest,
onUpdateClick = onMakeServicesAvailableClick
)
}
GooglePlayServicesAvailability.SERVICE_DISABLED -> {
ServiceDisabledDialog(
onDismissRequest = onDismissRequest,
onLearnMoreClick = onLearnMoreClick
)
}
GooglePlayServicesAvailability.SERVICE_INVALID -> {
ServiceInvalidDialog(
onDismissRequest = onDismissRequest,
onLearnMoreClick = onLearnMoreClick
)
}
}
}
@Composable
private fun ServiceMissingDialog(onDismissRequest: () -> Unit, onInstallPlayServicesClick: () -> Unit) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.GooglePlayServicesAvailability__service_missing_title),
body = stringResource(R.string.GooglePlayServicesAvailability__service_missing_message),
confirm = stringResource(R.string.GooglePlayServicesAvailability__install_play_services),
dismiss = stringResource(android.R.string.cancel),
onConfirm = {},
onDeny = onInstallPlayServicesClick,
onDismiss = onDismissRequest,
onDismissRequest = onDismissRequest
)
}
@Composable
private fun ServiceUpdatingDialog(onDismissRequest: () -> Unit) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.GooglePlayServicesAvailability__service_updating_title),
body = stringResource(R.string.GooglePlayServicesAvailability__service_updating_message),
confirm = stringResource(android.R.string.ok),
onConfirm = {},
onDismiss = onDismissRequest,
onDismissRequest = onDismissRequest
)
}
@Composable
private fun ServiceVersionUpdateRequiredDialog(onDismissRequest: () -> Unit, onUpdateClick: () -> Unit) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.GooglePlayServicesAvailability__service_update_required_title),
body = stringResource(R.string.GooglePlayServicesAvailability__service_update_required_message),
confirm = stringResource(R.string.GooglePlayServicesAvailability__update),
dismiss = stringResource(android.R.string.cancel),
onConfirm = onUpdateClick,
onDismiss = onDismissRequest,
onDismissRequest = onDismissRequest
)
}
@Composable
private fun ServiceDisabledDialog(onDismissRequest: () -> Unit, onLearnMoreClick: () -> Unit) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
body = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_message),
confirm = stringResource(android.R.string.ok),
dismiss = stringResource(R.string.GooglePlayServicesAvailability__learn_more),
onConfirm = onDismissRequest,
onDeny = onLearnMoreClick,
onDismiss = onDismissRequest,
onDismissRequest = onDismissRequest
)
}
@Composable
private fun ServiceInvalidDialog(onDismissRequest: () -> Unit, onLearnMoreClick: () -> Unit) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
body = stringResource(R.string.GooglePlayServicesAvailability__service_invalid_message),
confirm = stringResource(android.R.string.ok),
dismiss = stringResource(R.string.GooglePlayServicesAvailability__learn_more),
onConfirm = {},
onDeny = onLearnMoreClick,
onDismiss = onDismissRequest,
onDismissRequest = onDismissRequest
)
}
@DayNightPreviews
@Composable
private fun ServiceMissingDialogPreview() {
Previews.Preview {
ServiceMissingDialog({}, {})
}
}
@DayNightPreviews
@Composable
private fun ServiceUpdatingDialogPreview() {
Previews.Preview {
ServiceUpdatingDialog({})
}
}
@DayNightPreviews
@Composable
private fun ServiceVersionUpdateRequiredDialogPreview() {
Previews.Preview {
ServiceVersionUpdateRequiredDialog({}, {})
}
}
@DayNightPreviews
@Composable
private fun ServiceDisabledDialogPreview() {
Previews.Preview {
ServiceDisabledDialog({}, {})
}
}
@DayNightPreviews
@Composable
private fun ServiceInvalidDialogPreview() {
Previews.Preview {
ServiceInvalidDialog({}, {})
}
}

View File

@@ -28,9 +28,9 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.thoughtcrime.securesms.R
@@ -144,7 +144,7 @@ fun MessageBackupsEducationScreen(
}
}
@Preview
@DayNightPreviews
@Composable
private fun MessageBackupsEducationSheetPreview() {
Previews.Preview {
@@ -156,7 +156,7 @@ private fun MessageBackupsEducationSheetPreview() {
}
}
@Preview
@DayNightPreviews
@Composable
private fun NotableFeatureRowPreview() {
Previews.Preview {

View File

@@ -25,6 +25,7 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.google.android.gms.common.GoogleApiAvailability
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.asFlowable
@@ -40,6 +41,7 @@ import org.thoughtcrime.securesms.compose.Nav
import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.PlayStoreUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.storage.AndroidCredentialRepository
import org.thoughtcrime.securesms.util.viewModel
@@ -63,7 +65,10 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
}
private val viewModel: MessageBackupsFlowViewModel by viewModel {
MessageBackupsFlowViewModel(requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java))
MessageBackupsFlowViewModel(
initialTierSelection = requireArguments().getSerializableCompat(TIER, MessageBackupTier::class.java),
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext())
)
}
private val errorHandler = InAppPaymentCheckoutDelegate.ErrorHandler()
@@ -97,6 +102,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
override fun onResume() {
super.onResume()
viewModel.refreshCurrentTier()
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
}
@Composable
@@ -170,7 +176,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
stage = state.stage,
currentBackupTier = state.currentMessageBackupTier,
selectedBackupTier = state.selectedMessageBackupTier,
availableBackupTypes = state.availableBackupTypes,
allBackupTypes = state.allBackupTypes,
isNextEnabled = state.isCheckoutButtonEnabled(),
onMessageBackupsTierSelected = viewModel::onMessageBackupTierUpdated,
onNavigationClick = viewModel::goToPreviousStage,
@@ -180,7 +186,23 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
getString(R.string.backup_support_url)
)
},
onNextClicked = viewModel::goToNextStage
onNextClicked = viewModel::goToNextStage,
googlePlayServicesAvailability = state.googlePlayApiAvailability,
googlePlayBillingAvailability = state.googlePlayBillingAvailability,
onLearnMoreAboutWhyUserCanNotUpgrade = {
CommunicationActions.openBrowserLink(
requireContext(),
getString(R.string.backup_support_url)
)
},
onMakeGooglePlayServicesAvailable = {
GoogleApiAvailability.getInstance().makeGooglePlayServicesAvailable(requireActivity()).addOnSuccessListener {
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
}
},
onOpenPlayStore = {
PlayStoreUtil.openPlayStoreHome(requireContext())
}
)
}
}

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.runtime.Immutable
import org.signal.core.util.billing.BillingResponseCode
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.backups.remote.BackupKeySaveState
import org.thoughtcrime.securesms.database.InAppPaymentTable
@@ -16,7 +17,9 @@ import org.whispersystems.signalservice.api.AccountEntropyPool
data class MessageBackupsFlowState(
val selectedMessageBackupTier: MessageBackupTier? = SignalStore.backup.backupTier,
val currentMessageBackupTier: MessageBackupTier? = null,
val availableBackupTypes: List<MessageBackupsType> = emptyList(),
val allBackupTypes: List<MessageBackupsType> = emptyList(),
val googlePlayApiAvailability: GooglePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
val googlePlayBillingAvailability: BillingResponseCode = BillingResponseCode.FEATURE_NOT_SUPPORTED,
val inAppPayment: InAppPaymentTable.InAppPayment? = null,
val startScreen: MessageBackupsStage,
val stage: MessageBackupsStage = startScreen,
@@ -35,7 +38,7 @@ data class MessageBackupsFlowState(
* Whether or not the 'next' button on the type selection screen is enabled.
*/
fun isCheckoutButtonEnabled(): Boolean {
return selectedMessageBackupTier in availableBackupTypes.map { it.tier } &&
return selectedMessageBackupTier in allBackupTypes.map { it.tier } &&
selectedMessageBackupTier != currentMessageBackupTier &&
paymentReadyState == PaymentReadyState.READY
}

View File

@@ -45,7 +45,6 @@ import org.thoughtcrime.securesms.jobs.InAppPaymentPurchaseTokenJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.next
import org.whispersystems.signalservice.api.storage.IAPSubscriptionId
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
@@ -53,6 +52,7 @@ import kotlin.time.Duration.Companion.seconds
class MessageBackupsFlowViewModel(
private val initialTierSelection: MessageBackupTier?,
googlePlayApiAvailability: Int,
startScreen: MessageBackupsStage = if (SignalStore.backup.backupTier == null) MessageBackupsStage.EDUCATION else MessageBackupsStage.TYPE_SELECTION
) : ViewModel(), BackupKeyCredentialManagerHandler {
@@ -63,7 +63,8 @@ class MessageBackupsFlowViewModel(
private val internalStateFlow = MutableStateFlow(
MessageBackupsFlowState(
availableBackupTypes = emptyList(),
allBackupTypes = emptyList(),
googlePlayApiAvailability = GooglePlayServicesAvailability.fromCode(googlePlayApiAvailability),
currentMessageBackupTier = SignalStore.backup.backupTier,
selectedMessageBackupTier = resolveSelectedTier(initialTierSelection, SignalStore.backup.backupTier),
startScreen = startScreen
@@ -74,6 +75,14 @@ class MessageBackupsFlowViewModel(
val deletionState: Flow<DeletionState> = SignalStore.backup.deletionStateFlow
init {
viewModelScope.launch(SignalDispatchers.IO) {
internalStateFlow.update {
it.copy(
googlePlayBillingAvailability = AppDependencies.billingApi.getApiAvailability()
)
}
}
viewModelScope.launch {
val result = withContext(SignalDispatchers.IO) {
BackupRepository.triggerBackupIdReservation()
@@ -85,16 +94,16 @@ class MessageBackupsFlowViewModel(
}
result.runOnStatusCodeError { code ->
Log.d(TAG, "Failed to trigger backup id reservation. ($code)")
Log.w(TAG, "Failed to trigger backup id reservation. ($code)")
internalStateFlow.update { it.copy(paymentReadyState = MessageBackupsFlowState.PaymentReadyState.FAILED) }
}
}
viewModelScope.launch {
val availableBackupTypes: List<MessageBackupsType> = try {
val allBackupTypes: List<MessageBackupsType> = try {
withContext(SignalDispatchers.IO) {
BackupRepository.getAvailableBackupsTypes(
if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
BackupRepository.getBackupTypes(
listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
)
}
} catch (e: Exception) {
@@ -104,8 +113,8 @@ class MessageBackupsFlowViewModel(
internalStateFlow.update { state ->
state.copy(
availableBackupTypes = availableBackupTypes,
selectedMessageBackupTier = if (state.selectedMessageBackupTier in availableBackupTypes.map { it.tier }) state.selectedMessageBackupTier else availableBackupTypes.firstOrNull()?.tier
allBackupTypes = allBackupTypes,
selectedMessageBackupTier = if (state.selectedMessageBackupTier in allBackupTypes.map { it.tier }) state.selectedMessageBackupTier else allBackupTypes.firstOrNull()?.tier
)
}
}
@@ -157,6 +166,12 @@ class MessageBackupsFlowViewModel(
}
}
fun setGooglePlayApiAvailability(googlePlayApiAvailability: Int) {
internalStateFlow.update {
it.copy(googlePlayApiAvailability = GooglePlayServicesAvailability.fromCode(googlePlayApiAvailability))
}
}
fun refreshCurrentTier() {
val tier = SignalStore.backup.backupTier
if (tier == MessageBackupTier.PAID) {
@@ -285,7 +300,7 @@ class MessageBackupsFlowViewModel(
MessageBackupTier.PAID -> {
check(state.selectedMessageBackupTier == MessageBackupTier.PAID)
check(state.availableBackupTypes.any { it.tier == state.selectedMessageBackupTier })
check(state.allBackupTypes.any { it.tier == state.selectedMessageBackupTier })
viewModelScope.launch(SignalDispatchers.IO) {
internalStateFlow.update { it.copy(inAppPayment = null) }

View File

@@ -8,10 +8,13 @@ package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -25,9 +28,9 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.signal.core.ui.R as CoreUiR
@@ -39,6 +42,8 @@ fun MessageBackupsKeyEducationScreen(
onNavigationClick: () -> Unit = {},
onNextClick: () -> Unit = {}
) {
val scrollState = rememberScrollState()
Scaffolds.Settings(
title = "",
navigationIcon = ImageVector.vectorResource(R.drawable.symbol_arrow_start_24),
@@ -48,7 +53,8 @@ fun MessageBackupsKeyEducationScreen(
modifier = Modifier
.padding(it)
.padding(horizontal = dimensionResource(CoreUiR.dimen.gutter))
.fillMaxSize(),
.fillMaxSize()
.verticalScroll(scrollState),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
@@ -61,6 +67,7 @@ fun MessageBackupsKeyEducationScreen(
Text(
text = stringResource(R.string.MessageBackupsKeyEducationScreen__your_backup_key),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 16.dp)
)
@@ -81,11 +88,16 @@ fun MessageBackupsKeyEducationScreen(
modifier = Modifier.padding(top = 16.dp)
)
Box(
Spacer(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(bottom = 24.dp)
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 24.dp)
) {
Buttons.LargeTonal(
onClick = onNextClick,
@@ -100,7 +112,7 @@ fun MessageBackupsKeyEducationScreen(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun MessageBackupsKeyEducationScreenPreview() {
Previews.Preview {

View File

@@ -47,10 +47,10 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.Snackbars
import org.signal.core.ui.compose.horizontalGutters
import org.signal.core.ui.compose.theme.SignalTheme
@@ -421,7 +421,7 @@ private suspend fun saveKeyToCredentialManager(
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun MessageBackupsKeyRecordScreenPreview() {
Previews.Preview {
@@ -438,7 +438,7 @@ private fun MessageBackupsKeyRecordScreenPreview() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun SaveKeyConfirmationDialogPreview() {
Previews.Preview {
@@ -452,7 +452,7 @@ private fun SaveKeyConfirmationDialogPreview() {
}
@OptIn(ExperimentalMaterial3Api::class)
@SignalPreview
@DayNightPreviews
@Composable
private fun CreateNewBackupKeySheetContentPreview() {
Previews.BottomSheetPreview {
@@ -462,7 +462,7 @@ private fun CreateNewBackupKeySheetContentPreview() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun DownloadMediaDialogPreview() {
Previews.Preview {

View File

@@ -34,9 +34,9 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -192,7 +192,7 @@ private fun BottomSheetContent(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun MessageBackupsKeyRecordScreenPreview() {
Previews.Preview {
@@ -202,7 +202,7 @@ private fun MessageBackupsKeyRecordScreenPreview() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BottomSheetContentPreview() {
Previews.BottomSheetPreview {

View File

@@ -19,8 +19,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
/**
@@ -58,7 +58,7 @@ fun MessageBackupsTypeFeatureRow(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun MessageBackupsTypeFeatureRowPreview() {
Previews.Preview {

View File

@@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -47,11 +48,12 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.billing.BillingResponseCode
import org.signal.core.util.bytes
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
@@ -74,12 +76,17 @@ fun MessageBackupsTypeSelectionScreen(
stage: MessageBackupsStage,
currentBackupTier: MessageBackupTier?,
selectedBackupTier: MessageBackupTier?,
availableBackupTypes: List<MessageBackupsType>,
allBackupTypes: List<MessageBackupsType>,
googlePlayServicesAvailability: GooglePlayServicesAvailability,
googlePlayBillingAvailability: BillingResponseCode,
isNextEnabled: Boolean,
onMessageBackupsTierSelected: (MessageBackupTier) -> Unit,
onNavigationClick: () -> Unit,
onReadMoreClicked: () -> Unit,
onNextClicked: () -> Unit
onNextClicked: () -> Unit,
onLearnMoreAboutWhyUserCanNotUpgrade: () -> Unit,
onMakeGooglePlayServicesAvailable: () -> Unit,
onOpenPlayStore: () -> Unit
) {
Scaffolds.Settings(
title = "",
@@ -144,7 +151,7 @@ fun MessageBackupsTypeSelectionScreen(
}
itemsIndexed(
availableBackupTypes,
allBackupTypes,
{ _, item -> item.tier }
) { index, item ->
MessageBackupsTypeBlock(
@@ -158,19 +165,40 @@ fun MessageBackupsTypeSelectionScreen(
}
}
val hasCurrentBackupTier = currentBackupTier != null
val paidTierNotAvailableDialogState = remember { PaidTierNotAvailableDialogState() }
val onSubscribeButtonClick = remember(googlePlayServicesAvailability, googlePlayBillingAvailability, selectedBackupTier) {
{
if (selectedBackupTier == MessageBackupTier.PAID && googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS) {
paidTierNotAvailableDialogState.displayGooglePlayApiErrorDialog = true
} else if (selectedBackupTier == MessageBackupTier.PAID && !googlePlayBillingAvailability.isSuccess) {
paidTierNotAvailableDialogState.displayGooglePlayBillingErrorDialog = true
} else {
onNextClicked()
}
}
}
PaidTierNotAvailableDialogs(
state = paidTierNotAvailableDialogState,
onOpenPlayStore = onOpenPlayStore,
onLearnMoreAboutWhyUserCanNotUpgrade = onLearnMoreAboutWhyUserCanNotUpgrade,
onMakeGooglePlayServicesAvailable = onMakeGooglePlayServicesAvailable,
googlePlayServicesAvailability = googlePlayServicesAvailability
)
Buttons.LargeTonal(
onClick = onNextClicked,
onClick = onSubscribeButtonClick,
enabled = isNextEnabled,
modifier = Modifier
.testTag("subscribe-button")
.fillMaxWidth()
.padding(vertical = if (hasCurrentBackupTier) 10.dp else 16.dp)
.padding(vertical = 16.dp)
) {
val text: String = if (currentBackupTier == null) {
if (selectedBackupTier == MessageBackupTier.PAID && availableBackupTypes.map { it.tier }.contains(selectedBackupTier)) {
val paidTier = availableBackupTypes.first { it.tier == MessageBackupTier.PAID } as MessageBackupsType.Paid
if (selectedBackupTier == MessageBackupTier.PAID && (googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS || !googlePlayBillingAvailability.isSuccess)) {
stringResource(R.string.MessageBackupsTypeSelectionScreen__more_about_this_plan)
} else if (selectedBackupTier == MessageBackupTier.PAID && allBackupTypes.map { it.tier }.contains(selectedBackupTier)) {
val paidTier = allBackupTypes.first { it.tier == MessageBackupTier.PAID } as MessageBackupsType.Paid
val context = LocalContext.current
val price = remember(paidTier) {
@@ -183,6 +211,8 @@ fun MessageBackupsTypeSelectionScreen(
} else {
stringResource(R.string.MessageBackupsTypeSelectionScreen__subscribe)
}
} else if (selectedBackupTier == MessageBackupTier.PAID && (googlePlayServicesAvailability != GooglePlayServicesAvailability.SUCCESS || !googlePlayBillingAvailability.isSuccess)) {
stringResource(R.string.MessageBackupsTypeSelectionScreen__more_about_this_plan)
} else {
stringResource(R.string.MessageBackupsTypeSelectionScreen__change_backup_type)
}
@@ -200,7 +230,54 @@ fun MessageBackupsTypeSelectionScreen(
}
}
@SignalPreview
@Stable
class PaidTierNotAvailableDialogState {
var displayGooglePlayBillingErrorDialog: Boolean by mutableStateOf(false)
var displayGooglePlayApiErrorDialog: Boolean by mutableStateOf(false)
}
@Composable
fun PaidTierNotAvailableDialogs(
state: PaidTierNotAvailableDialogState,
googlePlayServicesAvailability: GooglePlayServicesAvailability,
onLearnMoreAboutWhyUserCanNotUpgrade: () -> Unit,
onMakeGooglePlayServicesAvailable: () -> Unit,
onOpenPlayStore: () -> Unit
) {
if (state.displayGooglePlayApiErrorDialog) {
GooglePlayServicesAvailabilityDialog(
onDismissRequest = { state.displayGooglePlayApiErrorDialog = false },
googlePlayServicesAvailability = googlePlayServicesAvailability,
onLearnMoreClick = onLearnMoreAboutWhyUserCanNotUpgrade,
onMakeServicesAvailableClick = onMakeGooglePlayServicesAvailable
)
}
if (state.displayGooglePlayBillingErrorDialog) {
UserNotSignedInDialog(
onDismissRequest = { state.displayGooglePlayBillingErrorDialog = false },
onOpenPlayStore = onOpenPlayStore
)
}
}
@Composable
private fun UserNotSignedInDialog(
onDismissRequest: () -> Unit,
onOpenPlayStore: () -> Unit
) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.GooglePlayServicesAvailability__service_disabled_title),
body = "To subscribe to Signal Secure Backups, please sign into the Google Play store.",
onConfirm = onOpenPlayStore,
onDismiss = onDismissRequest,
onDismissRequest = onDismissRequest,
confirm = "Open Play Store",
dismiss = stringResource(android.R.string.cancel)
)
}
@DayNightPreviews
@Composable
private fun MessageBackupsTypeSelectionScreenPreview() {
var selectedBackupsType by remember { mutableStateOf(MessageBackupTier.FREE) }
@@ -209,18 +286,23 @@ private fun MessageBackupsTypeSelectionScreenPreview() {
MessageBackupsTypeSelectionScreen(
stage = MessageBackupsStage.TYPE_SELECTION,
selectedBackupTier = selectedBackupsType,
availableBackupTypes = testBackupTypes(),
allBackupTypes = testBackupTypes(),
onMessageBackupsTierSelected = { selectedBackupsType = it },
onNavigationClick = {},
onReadMoreClicked = {},
onNextClicked = {},
onLearnMoreAboutWhyUserCanNotUpgrade = {},
onMakeGooglePlayServicesAvailable = {},
onOpenPlayStore = {},
currentBackupTier = null,
googlePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
googlePlayBillingAvailability = BillingResponseCode.OK,
isNextEnabled = true
)
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun MessageBackupsTypeSelectionScreenWithCurrentTierPreview() {
var selectedBackupsType by remember { mutableStateOf(MessageBackupTier.FREE) }
@@ -229,12 +311,17 @@ private fun MessageBackupsTypeSelectionScreenWithCurrentTierPreview() {
MessageBackupsTypeSelectionScreen(
stage = MessageBackupsStage.TYPE_SELECTION,
selectedBackupTier = selectedBackupsType,
availableBackupTypes = testBackupTypes(),
allBackupTypes = testBackupTypes(),
onMessageBackupsTierSelected = { selectedBackupsType = it },
onNavigationClick = {},
onReadMoreClicked = {},
onNextClicked = {},
onLearnMoreAboutWhyUserCanNotUpgrade = {},
onMakeGooglePlayServicesAvailable = {},
onOpenPlayStore = {},
currentBackupTier = MessageBackupTier.PAID,
googlePlayServicesAvailability = GooglePlayServicesAvailability.SUCCESS,
googlePlayBillingAvailability = BillingResponseCode.OK,
isNextEnabled = true
)
}
@@ -325,8 +412,12 @@ private fun getFormattedPricePerMonth(messageBackupsType: MessageBackupsType): S
return when (messageBackupsType) {
is MessageBackupsType.Free -> stringResource(id = R.string.MessageBackupsTypeSelectionScreen__free)
is MessageBackupsType.Paid -> {
val formattedAmount = FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
stringResource(id = R.string.MessageBackupsTypeSelectionScreen__s_month, formattedAmount)
if (messageBackupsType.pricePerMonth.amount == BigDecimal.ZERO) {
stringResource(R.string.MessageBackupsTypeSelectionScreen__paid)
} else {
val formattedAmount = FiatMoneyUtil.format(LocalContext.current.resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
stringResource(id = R.string.MessageBackupsTypeSelectionScreen__s_month, formattedAmount)
}
}
}
}

View File

@@ -25,8 +25,8 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withLink
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BiometricDeviceAuthentication
@@ -188,7 +188,7 @@ fun VerifyBackupPinScreen(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun VerifyBackupKeyScreen() {
Previews.Preview {

View File

@@ -0,0 +1,103 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.util
import okio.ByteString
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.whispersystems.signalservice.api.backup.MediaName
fun Frame.getAllReferencedArchiveAttachmentInfos(): Set<ArchiveAttachmentInfo> {
val infos: MutableSet<ArchiveAttachmentInfo> = mutableSetOf()
when {
this.account != null -> infos += this.account.getAllReferencedArchiveAttachmentInfos()
this.chat != null -> infos += this.chat.getAllReferencedArchiveAttachmentInfos()
this.chatItem != null -> infos += this.chatItem.getAllReferencedArchiveAttachmentInfos()
}
return infos.toSet()
}
private fun AccountData.getAllReferencedArchiveAttachmentInfos(): Set<ArchiveAttachmentInfo> {
val info = this.accountSettings?.defaultChatStyle?.wallpaperPhoto?.toArchiveAttachmentInfo()
return if (info != null) {
setOf(info)
} else {
emptySet()
}
}
private fun Chat.getAllReferencedArchiveAttachmentInfos(): Set<ArchiveAttachmentInfo> {
val info = this.style?.wallpaperPhoto?.toArchiveAttachmentInfo()
return if (info != null) {
setOf(info)
} else {
emptySet()
}
}
private fun ChatItem.getAllReferencedArchiveAttachmentInfos(): Set<ArchiveAttachmentInfo> {
var out: MutableSet<ArchiveAttachmentInfo>? = null
// The user could have many chat items, and most will not have attachments. To avoid allocating unnecessary sets, we do this little trick.
// (Note: emptySet() returns a constant under the hood, so that's fine)
fun appendToOutput(item: ArchiveAttachmentInfo) {
if (out == null) {
out = mutableSetOf()
}
out.add(item)
}
this.contactMessage?.contact?.avatar?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
this.directStoryReplyMessage?.textReply?.longText?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
this.standardMessage?.attachments?.mapNotNull { it.pointer?.toArchiveAttachmentInfo() }?.forEach { appendToOutput(it) }
this.standardMessage?.quote?.attachments?.mapNotNull { it.thumbnail?.pointer?.toArchiveAttachmentInfo(forQuote = true) }?.forEach { appendToOutput(it) }
this.standardMessage?.linkPreview?.mapNotNull { it.image?.toArchiveAttachmentInfo() }?.forEach { appendToOutput(it) }
this.standardMessage?.longText?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
this.stickerMessage?.sticker?.data_?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
this.viewOnceMessage?.attachment?.pointer?.toArchiveAttachmentInfo()?.let { appendToOutput(it) }
this.revisions.forEach { revision ->
revision.getAllReferencedArchiveAttachmentInfos().forEach { appendToOutput(it) }
}
return out ?: emptySet()
}
private fun FilePointer.toArchiveAttachmentInfo(forQuote: Boolean = false): ArchiveAttachmentInfo? {
if (this.locatorInfo?.key == null) {
return null
}
if (this.locatorInfo.plaintextHash == null) {
return null
}
return ArchiveAttachmentInfo(
plaintextHash = this.locatorInfo.plaintextHash,
remoteKey = this.locatorInfo.key,
cdn = this.locatorInfo.mediaTierCdnNumber ?: Cdn.CDN_0.cdnNumber,
contentType = this.contentType,
forQuote = forQuote
)
}
data class ArchiveAttachmentInfo(
val plaintextHash: ByteString,
val remoteKey: ByteString,
val cdn: Int,
val contentType: String?,
val forQuote: Boolean
) {
val fullSizeMediaName: MediaName get() = MediaName.fromPlaintextHashAndRemoteKey(plaintextHash.toByteArray(), remoteKey.toByteArray())
val thumbnailMediaName: MediaName get() = MediaName.fromPlaintextHashAndRemoteKeyForThumbnail(plaintextHash.toByteArray(), remoteKey.toByteArray())
}

View File

@@ -12,9 +12,11 @@ import com.bumptech.glide.load.Key
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.load.BadgeSpriteTransformation
import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.serialization.UriSerializer
import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -28,12 +30,13 @@ typealias OnBadgeClicked = (Badge, Boolean, Boolean) -> Unit
*/
@Stable
@Parcelize
@Serializable
data class Badge(
val id: String,
val category: Category,
val name: String,
val description: String,
val imageUrl: Uri,
@Serializable(with = UriSerializer::class) val imageUrl: Uri,
val imageDensity: String,
val expirationTimestamp: Long,
val visible: Boolean,

View File

@@ -12,8 +12,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -49,7 +49,7 @@ private fun Banner(contentPadding: PaddingValues, actionListener: (Boolean) -> U
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreview() {
Previews.Preview {

View File

@@ -11,8 +11,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -65,7 +65,7 @@ private fun Banner(contentPadding: PaddingValues, onLearnMoreClicked: () -> Unit
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreview() {
Previews.Preview {

View File

@@ -11,8 +11,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentManager
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -56,7 +56,7 @@ private fun Banner(contentPadding: PaddingValues, onLearnMoreClicked: () -> Unit
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreview() {
Previews.Preview {

View File

@@ -12,8 +12,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -60,7 +60,7 @@ private fun Banner(contentPadding: PaddingValues, onUpdateClicked: () -> Unit =
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreview() {
Previews.Preview {

View File

@@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -67,7 +67,7 @@ private fun Banner(contentPadding: PaddingValues, onDismissListener: () -> Unit
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreview() {
Previews.Preview {

View File

@@ -12,8 +12,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -56,7 +56,7 @@ private fun Banner(contentPadding: PaddingValues, onUpdateNow: () -> Unit = {})
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreview() {
Previews.Preview {

View File

@@ -11,8 +11,8 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -62,7 +62,7 @@ private fun Banner(contentPadding: PaddingValues, suggestionsSize: Int, onAddMem
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreviewSingular() {
Previews.Preview {
@@ -70,7 +70,7 @@ private fun BannerPreviewSingular() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreviewPlural() {
Previews.Preview {

View File

@@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -87,7 +87,7 @@ private fun Banner(contentPadding: PaddingValues, daysUntilExpiry: Int, onUpdate
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreviewExpireToday() {
Previews.Preview {
@@ -98,7 +98,7 @@ private fun BannerPreviewExpireToday() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreviewExpireTomorrow() {
Previews.Preview {
@@ -109,7 +109,7 @@ private fun BannerPreviewExpireTomorrow() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreviewExpireLater() {
Previews.Preview {

View File

@@ -15,8 +15,8 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -66,7 +66,7 @@ private fun Banner(contentPadding: PaddingValues, suggestionsSize: Int, onViewCl
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreviewSingular() {
Previews.Preview {
@@ -74,7 +74,7 @@ private fun BannerPreviewSingular() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreviewPlural() {
Previews.Preview {

View File

@@ -12,8 +12,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
@@ -41,7 +41,7 @@ private fun Banner(contentPadding: PaddingValues) {
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreview() {
Previews.Preview {

View File

@@ -13,8 +13,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -59,7 +59,7 @@ private fun Banner(contentPadding: PaddingValues) {
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreview() {
Previews.Preview {

View File

@@ -11,8 +11,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.banner.Banner
import org.thoughtcrime.securesms.banner.ui.compose.Action
@@ -65,7 +65,7 @@ private fun Banner(contentPadding: PaddingValues, usernameSyncState: UsernameSyn
)
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreviewUsernameCorrupted() {
Previews.Preview {
@@ -73,7 +73,7 @@ private fun BannerPreviewUsernameCorrupted() {
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun BannerPreviewLinkCorrupted() {
Previews.Preview {

View File

@@ -34,8 +34,8 @@ import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.util.isNotNullOrBlank
import org.thoughtcrime.securesms.R
@@ -195,7 +195,7 @@ enum class Importance {
}
@Composable
@SignalPreview
@DayNightPreviews
private fun BubblesOptOutPreview() {
Previews.Preview {
DefaultBanner(
@@ -212,7 +212,7 @@ private fun BubblesOptOutPreview() {
}
@Composable
@SignalPreview
@DayNightPreviews
private fun ForcedUpgradePreview() {
Previews.Preview {
DefaultBanner(
@@ -228,7 +228,7 @@ private fun ForcedUpgradePreview() {
}
@Composable
@SignalPreview
@DayNightPreviews
private fun FullyLoadedErrorPreview() {
val actions = listOf(
Action(R.string.ExpiredBuildReminder_update_now) { },

View File

@@ -26,8 +26,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock
@@ -145,7 +145,7 @@ private fun UpgradeToEnableOptimizedStorageSheetContent(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun UpgradeToEnableOptimizedStorageSheetContentPreview() {
Previews.BottomSheetPreview {

View File

@@ -19,6 +19,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.gms.common.GoogleApiAvailability
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.asFlowable
@@ -59,6 +60,7 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
private val viewModel: MessageBackupsFlowViewModel by viewModel {
MessageBackupsFlowViewModel(
initialTierSelection = MessageBackupTier.PAID,
googlePlayApiAvailability = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()),
startScreen = MessageBackupsStage.TYPE_SELECTION
)
}
@@ -93,12 +95,18 @@ abstract class UpgradeToPaidTierBottomSheet : ComposeBottomSheetDialogFragment()
}
}
override fun onResume() {
super.onResume()
viewModel.refreshCurrentTier()
viewModel.setGooglePlayApiAvailability(GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(requireContext()))
}
@Composable
override fun SheetContent() {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val paidBackupType = state.availableBackupTypes.firstOrNull { it.tier == MessageBackupTier.PAID } as? MessageBackupsType.Paid
val freeBackupType = state.availableBackupTypes.firstOrNull { it.tier == MessageBackupTier.FREE } as? MessageBackupsType.Free
val paidBackupType = state.allBackupTypes.firstOrNull { it.tier == MessageBackupTier.PAID } as? MessageBackupsType.Paid
val freeBackupType = state.allBackupTypes.firstOrNull { it.tier == MessageBackupTier.FREE } as? MessageBackupsType.Free
if (paidBackupType != null && freeBackupType != null) {
UpgradeSheetContent(

View File

@@ -27,8 +27,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeBlock
@@ -143,7 +143,7 @@ private fun UpgradeToStartMediaBackupSheetContent(
}
}
@SignalPreview
@DayNightPreviews
@Composable
private fun UpgradeToStartMediaBackupSheetContentPreview() {
Previews.Preview {

View File

@@ -24,6 +24,8 @@ import java.net.URLDecoder
object CallLinks {
private const val ROOT_KEY = "key"
private const val EPOCH = "epoch"
private const val LEGACY_HTTPS_LINK_PREFIX = "https://signal.link/call#key="
private const val LEGACY_SGNL_LINK_PREFIX = "sgnl://signal.link/call#key="
private const val HTTPS_LINK_PREFIX = "https://signal.link/call/#key="
private const val SNGL_LINK_PREFIX = "sgnl://signal.link/call/#key="
@@ -60,9 +62,16 @@ object CallLinks {
}
}
private fun isPrefixedCallLink(url: String): Boolean {
return url.startsWith(HTTPS_LINK_PREFIX) ||
url.startsWith(SNGL_LINK_PREFIX) ||
url.startsWith(LEGACY_HTTPS_LINK_PREFIX) ||
url.startsWith(LEGACY_SGNL_LINK_PREFIX)
}
@JvmStatic
fun isCallLink(url: String): Boolean {
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
if (!isPrefixedCallLink(url)) {
return false
}
@@ -76,7 +85,7 @@ object CallLinks {
@JvmStatic
fun parseUrl(url: String): CallLinkParseResult? {
if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
if (!isPrefixedCallLink(url)) {
Log.w(TAG, "Invalid url prefix.")
return null
}

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