Compare commits

..

521 Commits

Author SHA1 Message Date
Alex Hart 30cc3ff9fc Bump version to 7.4.0 2024-04-10 16:31:53 -03:00
Alex Hart 6f5f299035 Update baseline profile. 2024-04-10 16:31:47 -03:00
Alex Hart 02eed02cb8 Update translations and other static files. 2024-04-10 16:29:24 -03:00
Greyson Parrelli c1d29b5c39 Set internalUser=true for nightly builds. 2024-04-10 14:54:35 -04:00
Greyson Parrelli db4442939d Remove Environment.IS_PNP 2024-04-10 14:52:59 -04:00
tedgravlin 6ece776382 Fix navbar color in multiple instances. 2024-04-10 14:29:58 -03:00
Alex Hart 0eda714755 Send recipients when sending group story sync. 2024-04-10 14:21:34 -03:00
Greyson Parrelli 831d099503 Inline the nicknames feature flag. 2024-04-10 13:18:01 -04:00
Alex Hart fa23e4ca70 Convert members collection to set to avoid duplicate entries. 2024-04-10 13:45:46 -03:00
Greyson Parrelli 982f602178 Regularly analyze database tables to improve index usage. 2024-04-09 16:55:25 -04:00
Greyson Parrelli 713298109a Specify indexes for mention table queries. 2024-04-09 16:18:21 -04:00
Greyson Parrelli 8793981804 Add a log section for the database schema. 2024-04-09 16:18:21 -04:00
Greyson Parrelli 9bd4e9524c Convert MentionTable to kotlin. 2024-04-09 16:18:21 -04:00
Cody Henthorne 791dc2724f Attempt to fix bad notification for call service shutdown. 2024-04-09 16:18:21 -04:00
Cody Henthorne ba3473c61a Fix scroll to message when bubble is under toolbar. 2024-04-09 16:18:21 -04:00
moiseev-signal 3ea194255d Add getUsername default method to CredentialsProvider 2024-04-09 16:18:21 -04:00
Cody Henthorne ea081e981f Treat unregistered user during send as general failure. 2024-04-09 16:18:21 -04:00
Alex Konradi 2ce6ea9a2a Use existing libsignal proguard rules. 2024-04-09 16:18:20 -04:00
Alex Konradi 295c9310e9 Map libsignal CDSI errors to existing exceptions. 2024-04-09 16:18:20 -04:00
Greyson Parrelli 7447ed2eac Add the ability to jump to a specific date in search. 2024-04-09 16:18:20 -04:00
Cody Henthorne d5bf16b91a Fix incorrect thread body adjustments containing media, mentions, and styling. 2024-04-09 16:18:06 -04:00
Cody Henthorne 76665c1f0d Prevent excessive video toggling in group calls due to server instability. 2024-04-09 16:18:06 -04:00
Cody Henthorne dd28523b05 Transition full screen call UX to terminal state when call handled by linked device. 2024-04-09 16:18:06 -04:00
Cody Henthorne 16588c401e Reduce verbosity of WebRtcViewModel event logging during calls. 2024-04-09 16:18:06 -04:00
Greyson Parrelli dbf8a7ca87 Rotate libsignal-net flag. 2024-04-09 16:18:06 -04:00
moiseev-signal e92c76434e Upgrade to libsignal-client 0.44.0 2024-04-09 16:18:06 -04:00
Greyson Parrelli 7adb581271 Bump version to 7.3.1 2024-04-09 16:17:21 -04:00
Greyson Parrelli 869476a41b Update translations and other static files. 2024-04-09 16:16:47 -04:00
Greyson Parrelli 8daf1bca20 Improve handling of unknown groups. 2024-04-09 15:56:15 -04:00
Greyson Parrelli d044b3c931 Remove most lazy properties from Recipient. 2024-04-09 15:02:36 -04:00
Cody Henthorne 0fcb19e1cc Fix group recipient resolve race that can cause unknown group recipients in live cache. 2024-04-09 14:59:47 -04:00
Nicholas Tinsley 2a6977da75 Nickname screen copy update. 2024-04-04 09:45:26 -04:00
Nicholas Tinsley 26bd435bf6 Update nickname delete dialog copy. 2024-04-03 16:48:26 -04:00
Greyson Parrelli 91f8d6075c Bump version to 7.3.0 2024-04-03 15:55:34 -04:00
Greyson Parrelli 9ed9a330f4 Update baseline profile. 2024-04-03 15:54:13 -04:00
Greyson Parrelli 8bbf6b790f Update translations and other static files. 2024-04-03 15:47:24 -04:00
Greyson Parrelli a277e9b307 Fix compilation of benchmark build. 2024-04-03 15:47:24 -04:00
Cody Henthorne f8e6bcf290 Add username_edit release note cta action. 2024-04-03 14:07:56 -04:00
Greyson Parrelli 3ba2b46bb0 Convert Recipient to kotlin. 2024-04-03 14:02:55 -04:00
Greyson Parrelli b50eab230d Update strings for 'system contact' -> 'phone contact'. 2024-04-03 14:02:13 -04:00
Alex Hart 3f91824325 Fix bug preventing the review sheet from opening. 2024-04-03 14:02:13 -04:00
Alex Hart 879e05148b Fix database revocation for call links. 2024-04-03 14:02:13 -04:00
moiseev-signal 78e36b85d4 Make sure not more than one libsignal Network instance is ever created
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2024-04-03 14:02:13 -04:00
Alex Hart 544cc06f13 Add chevron to conversation heading. 2024-04-03 14:02:13 -04:00
Cody Henthorne 133b7ef3f1 Fix multiple exception crash in rx message send flow. 2024-04-03 14:02:13 -04:00
Cody Henthorne 08a407dc23 Prevent thread starvation during message sending. 2024-04-03 14:02:13 -04:00
Greyson Parrelli 6c697fad8b Stop reading the PNP capability. 2024-04-03 14:02:13 -04:00
Greyson Parrelli c904a7aa97 Delete LegacyAttachmentUploadJob.
It's been over 4 months since it was replaced. That's beyond the 90 day
build expiration + 1 day job lifespan. Should be safe to remove.
2024-04-03 14:02:13 -04:00
Greyson Parrelli ad131d7c65 Enqueue AccountConsistency check when prekey syncs fail. 2024-04-03 14:02:13 -04:00
Alex Hart e12d2d1e98 Fix local pip movement when in RTL language. 2024-04-03 14:02:12 -04:00
adel-signal f01e044662 Update to new calling turn info endpoint, add support for turn server ips.
Co-authored-by: Adel Lahlou <adel@signal.com>
2024-04-03 14:02:12 -04:00
Jim Gustafson 03d3ae7043 Update to RingRTC v2.39.3 2024-04-03 14:02:12 -04:00
Greyson Parrelli 6b60a22879 Bump version to 7.2.4 2024-04-03 13:32:38 -04:00
Greyson Parrelli bbded8caa8 Update translations and other static files. 2024-04-03 13:32:08 -04:00
Greyson Parrelli 3a6352d2a3 Don't show profile name in parens if it's the same as display name. 2024-04-03 13:19:37 -04:00
Greyson Parrelli 8293d6bc4c Allow last-name-only nicknames to be saved. 2024-04-03 11:54:07 -04:00
Greyson Parrelli 56bdb28c2f Fix bug around entering text in the middle of a full note.
There's likely other weirdness, but this at least addresses the most
commond variation, where entering text in the middle of a full note
would start chopping stuff off the end.
2024-04-03 11:20:35 -04:00
Greyson Parrelli b081fb1e13 Improve recipient shortname selection. 2024-04-03 10:45:47 -04:00
Greyson Parrelli 58c1f64dfe Allow familyName-only nicknames in storage service. 2024-04-03 10:44:04 -04:00
Greyson Parrelli 92b7147dcd Always take the remote nickname. 2024-04-03 10:39:43 -04:00
Greyson Parrelli fa3a85c948 Bump version to 7.2.3 2024-04-02 15:30:07 -04:00
Greyson Parrelli 9da4513694 Update translations and other static files. 2024-04-02 15:29:14 -04:00
Greyson Parrelli de520036a9 Allow last-name-only nicknames. 2024-04-02 15:19:44 -04:00
Greyson Parrelli 97ca15a1c0 Allow multi-line entry in note field. 2024-04-02 14:50:13 -04:00
Greyson Parrelli 713a34a5e7 Ensure that conversation count check is on background thread. 2024-04-02 14:36:07 -04:00
Alex Hart d688280a30 Fix search for users without thread. 2024-04-02 15:27:57 -03:00
Greyson Parrelli ebbf8fad4b Bump version to 7.2.2 2024-04-01 20:17:55 -04:00
Greyson Parrelli 5891c6fb2d Update translations and other static files. 2024-04-01 20:17:23 -04:00
Greyson Parrelli 7c96319fb6 Fix potential NPE in forwarding flow. 2024-04-01 20:17:23 -04:00
Greyson Parrelli 0d652ccfd6 Listen for recipient name changes in conversation item. 2024-04-01 19:58:05 -04:00
Greyson Parrelli d3718aa7ef Make nickname FF hot-swappable and default to true. 2024-04-01 19:17:07 -04:00
Cody Henthorne fcdcb9fd33 Fix linked device nickname change not syncing bug. 2024-04-01 19:06:08 -04:00
Nicholas Tinsley a8f925def0 Move delete button in Nickname activity. 2024-04-01 17:49:48 -04:00
Nicholas Tinsley 53cb125712 Prevent NPE in video editor. 2024-04-01 14:33:21 -04:00
Nicholas Tinsley 2a5793d96e Allow saving empty notes with empty nicknames. 2024-04-01 14:13:39 -04:00
Nicholas Tinsley d460fa7ed4 Disable view-once toggle in media caption editor.
Fixes #13492.
2024-04-01 11:37:34 -04:00
Nicholas Tinsley 5272b13c41 Bump version to 7.2.1 2024-03-29 17:35:46 -04:00
Nicholas Tinsley a66ac42038 Update translations and other static files. 2024-03-29 17:27:15 -04:00
Nicholas Tinsley 0014a2cba7 Hide camera switch icon during calls for devices with 1 or fewer cameras. 2024-03-29 17:21:27 -04:00
Nicholas Tinsley 7a9c01e6e5 Remove vestigial call camera toggle button. 2024-03-29 17:21:27 -04:00
Nicholas Tinsley 16402e43a5 Make in-app camera compatible with multi-window. 2024-03-29 17:21:27 -04:00
Nicholas Tinsley b1944da58d Show nickname for 1:1 chat bottom sheet. 2024-03-29 17:21:27 -04:00
Nicholas Tinsley 9081d3c826 Revise recipient bottom sheet. 2024-03-29 17:21:27 -04:00
Nicholas Tinsley d4ae0ca4cb Update conversation settings string. 2024-03-29 17:21:27 -04:00
Nicholas Tinsley 88f6ab915e Do not show nickname field for Note to Self. 2024-03-29 17:21:27 -04:00
Nicholas Tinsley 939024faff Don't show profile name parentheses if we don't have one. 2024-03-29 12:43:58 -04:00
Nicholas Tinsley 4c6e7991df Preserve username exceptions in ProGuard. 2024-03-29 12:40:12 -04:00
Nicholas Tinsley 036d91c039 Align linked device megaphone lifespan.
Thank you to Signal.DE from the community forum.
2024-03-28 15:59:08 -04:00
oguzhandogdu 869c922532 Remove second bullet span function 2024-03-28 14:57:26 -04:00
oguzhandogdu 217d15a853 Add gap width to bullet span 2024-03-28 14:57:26 -04:00
Nicholas Tinsley 931ffd0ba3 Bump version to 7.2.0 2024-03-27 16:01:18 -04:00
Nicholas Tinsley fecac297fa Update translations and other static files. 2024-03-27 15:57:11 -04:00
Nicholas Tinsley b0ea8d7df5 Prevent crash on devices with camera killswitches.
Addresses #13450.
2024-03-27 15:54:35 -04:00
Nicholas Tinsley f126df2120 Put custom controller behind feature flag. 2024-03-27 15:54:35 -04:00
Nicholas Tinsley 42450024fc Add profile name to about sheet. 2024-03-27 15:54:35 -04:00
Nicholas Tinsley 101db6e164 Apply SignalTheme to NicknameActivity. 2024-03-27 15:54:35 -04:00
Nicholas Tinsley 13bef94bf7 Update icons in conversation settings. 2024-03-27 15:54:35 -04:00
Nicholas Tinsley 02792c5a6f Remove extraneous time unit conversion. 2024-03-27 15:54:35 -04:00
Alex Hart 303929090b Implement the majority of the new nicknames and notes feature. 2024-03-27 15:54:35 -04:00
Alex Hart 7a24554b68 Update ContactRecord proto with new nickname fields. 2024-03-27 15:54:35 -04:00
Greyson Parrelli 5b10aa6fa7 Handle 428 for captcha submissions. 2024-03-27 15:54:35 -04:00
Alex Konradi e6eefac609 Upgrade to libsignal 0.42.0 2024-03-27 15:54:35 -04:00
Alex Hart 5f5a80dcbe Stub out MoreOptionsSheet and RestoreFromBackupFragment. 2024-03-27 15:54:35 -04:00
Cody Henthorne 7802448b24 Fix unblock icon tint in dark theme. 2024-03-27 15:54:35 -04:00
Clark 16d231f718 Persist blur hash with undownloaded attachments. 2024-03-27 15:54:35 -04:00
Cody Henthorne 62ca6cdd2f Fix can't receive audio and video pip render bug. 2024-03-27 15:54:35 -04:00
Cody Henthorne 7d81ed1150 Fix call controls disappearing when returning from system pip. 2024-03-27 15:54:35 -04:00
Greyson Parrelli 27812bb1ec Don't save duplicate queries in Spinner. 2024-03-27 15:54:35 -04:00
Greyson Parrelli 6854f7eb2a Add an 'internal details' screen for message details. 2024-03-27 15:54:35 -04:00
Clark de86c5622d Integrate more variation in backup test generation. 2024-03-27 15:54:35 -04:00
Jim Gustafson 6bf1a4295f Update to RingRTC v2.39.2 2024-03-27 15:54:35 -04:00
Alex Hart 7de2f0f460 Add nickname and notes fields to the RecipientTable. 2024-03-27 15:54:35 -04:00
Greyson Parrelli 50149a3803 Show a megaphone when a device is about to unlink. 2024-03-27 15:54:35 -04:00
Greyson Parrelli d7ee9639fd Be more lenient with quality matches when forwarding attachments. 2024-03-19 14:49:56 -04:00
Nicholas Tinsley 7d5627b17b Fix in-app camera rotation in multiview. 2024-03-19 14:48:38 -04:00
Greyson Parrelli e24c951d83 Convert MiscellaneousValues to kotlin. 2024-03-19 14:47:58 -04:00
Alex Hart e6a11c1ccf Revert "Fix pip placement in large calls."
This reverts commit aaeba4efe1.
2024-03-19 14:47:58 -04:00
Greyson Parrelli 3f66981359 Do not show username megaphone after a fresh install. 2024-03-19 14:47:58 -04:00
Cody Henthorne 874f808d56 Add process read sync tests. 2024-03-19 14:47:58 -04:00
Greyson Parrelli 450dc2f368 Improve logging around APNG animation disabling. 2024-03-19 14:47:58 -04:00
Alex Hart 7a69df42a7 Add receive support for new call log event data. 2024-03-19 14:47:58 -04:00
Greyson Parrelli 1ce1e30d32 Carry over the sent media quality when forwarding a video. 2024-03-19 14:47:58 -04:00
Greyson Parrelli 011f1d592e Fix bug with quote deduping. 2024-03-19 14:47:58 -04:00
Greyson Parrelli 1d29b0166d Backfill missing attachment hashes. 2024-03-19 14:47:58 -04:00
Greyson Parrelli 6df1a68213 Refactor and improve attachment deduping logic. 2024-03-19 14:47:58 -04:00
Nicholas Tinsley b7ee6bfcb3 Don't show transfer overlay for scheduled messages. 2024-03-19 14:47:58 -04:00
Nicholas Tinsley c0cb2b5e12 Allow seeking in video timeline. 2024-03-19 14:47:58 -04:00
Alex Hart b38865bdc7 Implement UI element refresh on transfer or restore screen. 2024-03-19 14:47:58 -04:00
Alex Hart 6f46331772 Add call log event proto updates. 2024-03-19 14:47:58 -04:00
Clark 989bd662c6 Add tests to generate backup with large amount of messages and chats. 2024-03-19 14:47:58 -04:00
Nicholas Tinsley 359e593481 Add support for hardware camera features in the in-app camera. 2024-03-19 14:47:58 -04:00
Nicholas Tinsley b7e0fe22db Add CameraX Extensions and update CameraX to 1.3.2. 2024-03-19 14:47:58 -04:00
Alex Hart 61cfbd6852 Allow ringer to ring in certain dnd situations. 2024-03-19 14:47:58 -04:00
Rashad Sookram 02c0d3ed6e Update to RingRTC v2.39.1 2024-03-19 14:47:58 -04:00
Cody Henthorne e4d6c3aeb2 Do not send viewed receipt for release channel. 2024-03-19 14:47:58 -04:00
Chris Eager 0c6761fcfd Update Option.RECAPTCHA to Option.CAPTCHA 2024-03-19 14:47:58 -04:00
Greyson Parrelli 8f884fdd5c Fix potential crash when parsing PreKeySyncJobData.
Honestly at this point I have no idea how this is happening.
Maybe somehow getting old data that was empty but not null?
A mystery for the ages.
2024-03-19 14:47:58 -04:00
Greyson Parrelli 07cea1818e Ensure that protocol stores are reset after setting ACI/PNI. 2024-03-19 14:47:58 -04:00
Cody Henthorne 132bc15373 Fix ANR when changing the configuration of a foldable. 2024-03-19 14:47:58 -04:00
Clark d993748753 Generate backup protos with message backup instrumentation tests. 2024-03-19 14:47:58 -04:00
Greyson Parrelli 3372565a39 Improve logging around consistency checks. 2024-03-19 14:47:58 -04:00
Alex Hart 134ac2b2fd Fix display name resolution for my story. 2024-03-19 14:47:58 -04:00
Alex Hart 0e0e91b4fe Hide invite banner when entering conversation search. 2024-03-19 14:47:58 -04:00
Greyson Parrelli 25b50bdb8f Rotate the libsignal-cdsi feature flag. 2024-03-19 14:47:58 -04:00
Alex Konradi 1988085171 Don't strip libsignal.net classes. 2024-03-19 14:47:58 -04:00
Nicholas Tinsley f892e9baff Update disappearing messages text. 2024-03-19 14:47:58 -04:00
Alex Konradi 4828d84caf Update libsignal to 0.41.2 2024-03-19 14:47:58 -04:00
Greyson Parrelli aeae6ac292 Remove deprecated blocked field from DeviceContact. 2024-03-19 14:47:58 -04:00
Alex Hart 0544c1f249 Display group call permissions dialog when trying to start a call in annoucment group when not an admin. 2024-03-19 14:47:58 -04:00
Greyson Parrelli 5027159ed8 Improve handling of unregistered states in profile screen. 2024-03-19 14:47:58 -04:00
Cody Henthorne ce778be895 Resume call PIP on app foreground. 2024-03-19 14:47:58 -04:00
Cody Henthorne 9e349d2b30 Mute video when closing system PIP during a call. 2024-03-19 14:47:58 -04:00
Fumiaki Yoshimatsu 72f19758db Fix chat search when using Japanese IMEs.
Resolves #13467
2024-03-19 14:47:58 -04:00
Greyson Parrelli 55bce1fa12 Fix potential NPE when pinning a PNI chat. 2024-03-19 14:47:58 -04:00
AsamK 5e1ebaa5d4 Fix various storage service issues.
Resolves #13466
2024-03-19 14:47:58 -04:00
Clark 742c348998 Add test restore flow to staging reg. 2024-03-19 14:47:58 -04:00
Clark 9d46b52786 Backup attachments as Attachment locators. 2024-03-19 14:47:57 -04:00
Clark ef374952ab Add tests for update messages except for groups and calls. 2024-03-19 14:47:57 -04:00
Clark f8ef4d5985 Add tests for text messages with mentions, quotes, reactions, and ranges. 2024-03-19 14:47:57 -04:00
Cody Henthorne 85929809f0 Bump version to 7.1.3 2024-03-19 14:36:32 -04:00
Cody Henthorne 068540120e Updated baseline profile. 2024-03-19 14:33:44 -04:00
Cody Henthorne 471c4fc200 Update translations and other static files. 2024-03-19 14:28:49 -04:00
Nicholas Tinsley 398c67362d Improve layout for view once toast for older devices. 2024-03-19 14:24:26 -04:00
Nicholas Tinsley 4ceeda5f02 Fix video review page for API <28. 2024-03-19 14:24:26 -04:00
Nicholas Tinsley 2bf6b993fe Somewhat reduce emoji keyboard jankiness in media review fragment. 2024-03-19 14:24:26 -04:00
Nicholas Tinsley 68363c5b82 Disable emoji button for view-once media. 2024-03-19 14:24:26 -04:00
Nicholas Tinsley 9f47a41017 Restore pinch to zoom gesture in in-app camera. 2024-03-19 13:46:19 -04:00
Nicholas Tinsley ba70101efd Add view-once button to media caption. 2024-03-19 12:00:36 -04:00
Greyson Parrelli 3aa54c9982 Remove some unused permissions. 2024-03-18 19:21:47 -04:00
Greyson Parrelli 825ca0d737 Remove more SMS vestiges. 2024-03-18 19:21:08 -04:00
Clark Chen 6754fef164 Bump version to 7.1.2 2024-03-11 18:50:24 -04:00
Clark Chen 4c079a8c25 Update translations and other static files. 2024-03-11 18:32:59 -04:00
Nicholas Tinsley 6e09d101b5 Debounce camera switcher button. 2024-03-11 18:26:45 -04:00
Nicholas Tinsley 39aa583297 Respect newlines in media review UI. 2024-03-11 18:26:45 -04:00
Nicholas Tinsley b08db7a8c5 Fix unexpected trimming behavior with long videos. 2024-03-11 18:26:45 -04:00
Alex Hart 865bf0d056 Fix nav bar getting out of sync with keyboard pager. 2024-03-11 18:26:45 -04:00
Nicholas Tinsley d52c520c02 Explicitly pause video player when not focused. 2024-03-11 18:26:45 -04:00
Nicholas Tinsley 1eabf11cdb Fix tap-to-focus UI for in-app camera. 2024-03-11 18:26:45 -04:00
Cody Henthorne cfb16d3f17 Fix link rendering under spoilers in read more view. 2024-03-11 18:26:45 -04:00
Alex Hart d5707638a6 Apply proper theming to FindByActivity. 2024-03-11 18:26:45 -04:00
Alex Hart 5cda5db7f7 Disable text field when view-once is selected. 2024-03-11 11:46:52 -03:00
Alex Hart 5c5d55d265 Introduce glyph fonts to correct spacing. 2024-03-11 11:14:05 -03:00
Alex Hart 4dd3b92eda Prevent crash when review banner wants to display self. 2024-03-11 09:52:24 -03:00
Cody Henthorne 112579079f Fix bad button text wrapping in message request view. 2024-03-08 16:57:15 -05:00
Greyson Parrelli 9897ba4b28 Properly pluralize a string. 2024-03-08 14:39:28 -05:00
Greyson Parrelli c64dfff4c7 Fix typo in string. 2024-03-07 22:40:26 -05:00
Alex Hart 915b3f0cd3 Bump version to 7.1.1 2024-03-07 17:02:41 -04:00
Alex Hart c295d11fc4 Updated baseline profile. 2024-03-07 17:01:52 -04:00
Alex Hart bc47c5436d Update translations and other static files. 2024-03-07 16:56:59 -04:00
Alex Hart aaeba4efe1 Fix pip placement in large calls. 2024-03-07 16:53:36 -04:00
Alex Hart 3c0eb58381 Apply alpha to v2 conversation item footer content. 2024-03-07 16:53:36 -04:00
Alex Hart c4f22449f9 Hide thumbnails in specific cases in quote view. 2024-03-07 16:53:36 -04:00
Greyson Parrelli bca346ec2f Improve copy for unregistered users. 2024-03-07 16:53:36 -04:00
Alex Hart e0bd60f87c Adjust styling and sizing for rationale dialog. 2024-03-07 16:53:36 -04:00
Alex Hart aeedab1531 Adjust spacing for contact and verified images on conversation settings page. 2024-03-07 16:53:35 -04:00
Greyson Parrelli c959f41c68 Improve message send performance. 2024-03-07 16:53:35 -04:00
Alex Hart 9ba755da16 Add section header to find by username / ph row. 2024-03-07 16:53:35 -04:00
Alex Hart 34026c5538 Add proper tinting to refresh and invite rows. 2024-03-07 10:04:16 -04:00
Nicholas Tinsley ea64425456 Media sending design improvements. 2024-03-07 09:38:13 -04:00
Alex Hart eb34a20195 Bump version to 7.1.0 2024-03-06 20:50:34 -04:00
Alex Hart 445513cc32 Updated baseline profile. 2024-03-06 20:50:06 -04:00
Alex Hart e431518a9d Update translations and other static files. 2024-03-06 20:45:13 -04:00
Alex Hart 61df88e094 Fix TestUsers construction in benchmark. 2024-03-06 20:42:01 -04:00
Greyson Parrelli 891c130e12 Sync the PNI identity used in sent transcripts. 2024-03-06 20:42:01 -04:00
Greyson Parrelli b4ced5278e Fix recipient merging case that causes a change number event. 2024-03-06 20:42:01 -04:00
Greyson Parrelli 10364e9342 Disable next button in FindByActivity when input is blank. 2024-03-06 20:42:01 -04:00
Alex Hart 74dc222a54 Add Recency support for contact search ordering. 2024-03-06 20:42:01 -04:00
Greyson Parrelli 2e4ac7ede1 Always perform CDSI lookups when starting new chats. 2024-03-06 20:42:01 -04:00
Cody Henthorne 184c1b67cc Add learned profile name event. 2024-03-06 20:42:01 -04:00
Alex Hart f702338129 Fix hide story action state. 2024-03-06 20:42:01 -04:00
Alex Hart 4b4b263423 Usernames 1.01 Fast-Follow Part 1. 2024-03-06 20:42:01 -04:00
Nicholas Tinsley 83c16a46de Update assets for image editor. 2024-03-06 20:42:01 -04:00
Clark 6383896a79 Fix incoming group updates showing as updated. 2024-03-06 20:42:01 -04:00
Nicholas Tinsley 5fa1560a10 Add stroke to draw tool color bar. 2024-03-06 20:42:01 -04:00
Nicholas Tinsley 9bd6ad36cc Round corners of selected region in video trimmer. 2024-03-06 20:42:01 -04:00
Nicholas Tinsley 83cc7d5181 Adjust media tool button animation. 2024-03-06 20:42:01 -04:00
Nicholas Tinsley 44150673e9 Adjust size of quality selector buttons. 2024-03-06 20:42:01 -04:00
Nicholas Tinsley 5092d723a8 Update media send symbols. 2024-03-06 20:42:01 -04:00
Cody Henthorne 218964cbda Add archive media apis. 2024-03-06 20:42:01 -04:00
Nicholas Tinsley ccc9752485 Hoist video editor state out of VideoEditorFragment. 2024-03-06 20:42:01 -04:00
Cody Henthorne 619038f27d Improve local fanout send performance. 2024-03-06 20:42:01 -04:00
Alex Hart 9f197b12ed Add support for call log mark as read. 2024-03-06 20:42:01 -04:00
Jim Gustafson 690608cdf3 Update to RingRTC v2.39.0
Co-authored-by: Alex Hart <alex@signal.org>
2024-03-06 20:42:01 -04:00
Alex Hart 4035932340 Fix find-by slide animations. 2024-03-06 20:42:01 -04:00
Clark fc9d94701c Disable job manager in instrumentation tests by default. 2024-03-06 20:42:01 -04:00
Alex Hart 6d54ae5f3d Update call link info sheet to match new designs. 2024-03-06 20:42:01 -04:00
Nicholas Tinsley c53abe0941 Video Sending Redesign 2024-03-06 20:42:01 -04:00
Greyson Parrelli 276e253fdf Fix individual send metrics. 2024-03-06 20:42:01 -04:00
Greyson Parrelli f160e960be Stop setting pq flag since it's no longer read. 2024-03-06 20:42:01 -04:00
Greyson Parrelli 41b57b9207 Remove unnecessary uniqueness constraint on prekey tables. 2024-03-06 20:42:01 -04:00
Alex Konradi 56eae8c7bf Add libsignal-net CDSI implementation. 2024-03-06 20:42:01 -04:00
Alex Hart 46c8b3b690 Replace full text with call link name as title in call info sheet. 2024-03-06 20:42:01 -04:00
Greyson Parrelli 58b11f3c47 Do a CDS refresh when a new chat is created. 2024-03-06 20:42:01 -04:00
Alex Hart 40b4b316b3 Add new options to share call link details from details fragment. 2024-03-06 20:42:01 -04:00
Clark 7a31f69aea Add tests for import/export of call logs. 2024-03-06 20:42:01 -04:00
Greyson Parrelli 648c99e81d Ensure we are updating last resort metadata after change number. 2024-03-06 20:42:01 -04:00
Greyson Parrelli 56b482a26f Allow scanning QR code from 'Find by username' screen. 2024-03-06 20:42:01 -04:00
Cody Henthorne c6df4af53a Update bank transfer timeline strings. 2024-03-06 20:42:01 -04:00
Clark 32fe927bfc Add import/export tests for backup of recipients and threads. 2024-03-06 20:42:01 -04:00
Ehren Kret 5740b768d0 Use full organization name in README. 2024-03-06 20:42:01 -04:00
Ehren Kret d8e74c730a Update README copyright year. 2024-03-06 20:42:00 -04:00
Clark 58846bbf42 Add import/export test for initially account data. 2024-03-06 20:42:00 -04:00
Greyson Parrelli 78d30fc479 Remove deprecated SVR2 enclaves. 2024-03-06 20:42:00 -04:00
Cody Henthorne 86afa988a0 Add ability to scan username qr from gallery. 2024-03-06 20:42:00 -04:00
Greyson Parrelli 6104ef62df Detect username QR codes in our camera-first capture flow. 2024-03-06 20:42:00 -04:00
Cody Henthorne 3f89acf9bd Disable parallel gradle in GH actions. 2024-03-06 20:42:00 -04:00
Cody Henthorne 591d499462 Show system contact icon in more places. 2024-03-06 20:42:00 -04:00
Greyson Parrelli c31a7152bc Update built-in emoji to v15.1 2024-03-06 20:42:00 -04:00
Greyson Parrelli 343cc3ca67 Update third party licenses. 2024-03-06 20:42:00 -04:00
Cody Henthorne 23e18cee22 Add nobody can find me by number setting warning. 2024-03-06 20:42:00 -04:00
Cody Henthorne 5b2c458bcf Show username in all display name locations if only option. 2024-03-06 20:42:00 -04:00
Cody Henthorne c10c64a6a6 Prepopulate find number with local user country code. 2024-03-06 20:42:00 -04:00
Cody Henthorne 957221e118 Fix cut off icon in conversation header. 2024-03-06 20:42:00 -04:00
Cody Henthorne e18e4454e4 Fix multi-invite group create dialog. 2024-03-06 20:42:00 -04:00
Greyson Parrelli e1067e30de Add support for endpoint checking prekey consistency. 2024-03-06 20:42:00 -04:00
Greyson Parrelli 09b0f15294 Remove unused capabilities. 2024-03-06 20:42:00 -04:00
Greyson Parrelli b1d6ff4bbd Remove the PNP build variant. 2024-03-06 20:42:00 -04:00
Cody Henthorne a49e9dd96d Fix crash adjusting constraints during large calls. 2024-03-06 20:42:00 -04:00
Clark 5e428e2c4d Backup and restore mentions. 2024-03-06 20:42:00 -04:00
Clark 0f6ff3c101 Integrate backup file validation to backup playground. 2024-03-06 20:42:00 -04:00
Clark 1ade8b502f Convert and store new group changes in MessageExtras. 2024-03-06 20:42:00 -04:00
Alex Konradi cc25f0685c Update libsignal version to v0.40.1 2024-03-06 20:42:00 -04:00
Nicholas Tinsley e91ed88785 CameraX Custom Controller.
Addresses #12817, #13316, #13389
2024-03-06 20:42:00 -04:00
Jon Chambers 39bc6d5eb3 Remove legacy signed prekey endpoint. 2024-02-23 16:42:58 -05:00
Nicholas Tinsley b7f472b0cd Update Google services libraries. 2024-02-23 16:42:58 -05:00
Cody Henthorne 942f4a45bf Fix reply icon not mirroring in RTL. 2024-02-23 16:42:58 -05:00
Cody Henthorne 767896b14c Fix infinite pending link preview on large previews. 2024-02-23 16:42:58 -05:00
Nicholas Tinsley 8c35628863 Adjust copy depending on PNP settings. 2024-02-23 16:42:58 -05:00
Cody Henthorne d555370076 Fix edit call link copy in bottom sheet. 2024-02-23 16:42:58 -05:00
Cody Henthorne bbbe76697d Fix invalid date on group member search results. 2024-02-23 16:42:58 -05:00
Nicholas Tinsley fc1d60e65b Fallback to matching video decoder by MIME type. 2024-02-23 16:42:57 -05:00
Nicholas Tinsley 9dc856202a Don't use 0 milliseconds as sending retry interval. 2024-02-23 16:42:57 -05:00
Jim Gustafson 6bc41776b1 Update to RingRTC v2.38.0 2024-02-23 16:42:57 -05:00
Greyson Parrelli 940cee0f30 Bump version to 7.0.2 2024-02-23 16:26:12 -05:00
Greyson Parrelli cdb6c16473 Update translations and other static files. 2024-02-23 16:25:05 -05:00
Greyson Parrelli c4842ae7c5 Attempt to prevent message retry loops. 2024-02-23 15:36:23 -05:00
Greyson Parrelli dc32e51ac2 Make a specific crash more clear to improve debuggability. 2024-02-23 15:36:23 -05:00
Greyson Parrelli 43caaf7efc Update a specific recipient case to merge rather than just steal PNI. 2024-02-23 15:36:23 -05:00
Greyson Parrelli dcd0d433b0 Fix potential charset crash on some devices. 2024-02-23 15:36:23 -05:00
Cody Henthorne 763e891dfd Show username in group invite flow. 2024-02-23 15:36:23 -05:00
Cody Henthorne c04f761f5a Show rate limit specific error message on username reservation. 2024-02-23 15:36:23 -05:00
Cody Henthorne b147882e4f Fix text input selection handle colors. 2024-02-23 15:36:23 -05:00
Cody Henthorne c9f5f91aad Fix black bars on username scan crosshair. 2024-02-23 15:36:23 -05:00
Cody Henthorne 0a3de42729 Fix username QR image generation for multiline usernames. 2024-02-23 15:36:23 -05:00
Greyson Parrelli 64fc0209f4 Remove unused endpoint. 2024-02-23 15:36:23 -05:00
Cody Henthorne 418ad51e77 Fix share your username popup icon tint. 2024-02-23 15:36:23 -05:00
Cody Henthorne 16faf41a84 Fix profile name not updating correctly. 2024-02-23 15:36:23 -05:00
Cody Henthorne d5cf8d36b3 Fix username recovery UX bugs. 2024-02-23 15:36:23 -05:00
Greyson Parrelli 755fafb0b6 Always use US locale when logging rounded numbers. 2024-02-21 13:08:43 -05:00
Greyson Parrelli 8fc9893ecd Improve logging around retries archiving sessions. 2024-02-21 13:04:42 -05:00
Greyson Parrelli 9071fd0024 Bump version to 7.0.1 2024-02-20 21:45:57 -05:00
Greyson Parrelli 9a3233bb28 Update translations and other static files. 2024-02-20 21:45:30 -05:00
Greyson Parrelli e77bc9170a Fix RTL rendering of username edit screen. 2024-02-20 21:36:24 -05:00
Greyson Parrelli 23d6a71a3b Update username validation to use libsignal. 2024-02-20 21:36:24 -05:00
Greyson Parrelli 67c3f41dff Fix crash when attempting to start a call via username. 2024-02-20 16:02:57 -05:00
Greyson Parrelli e22fa499c2 Reduce username debounce rate to 500ms. 2024-02-20 15:25:27 -05:00
Cody Henthorne fdef13ae92 Apply LQA feedback. 2024-02-20 15:02:39 -05:00
Greyson Parrelli c0a6f2316c Do not acknowledge retry receipts sent to PNI. 2024-02-20 12:24:32 -05:00
Greyson Parrelli 7d6a87c825 Attempt to fix missing session crash for resends. 2024-02-20 12:13:54 -05:00
Greyson Parrelli 6c863fe99c Clean up capability logging. 2024-02-18 18:34:18 -05:00
Greyson Parrelli 5cf8242ea0 Bump version to 7.0.0 2024-02-16 16:44:19 -05:00
Greyson Parrelli a804e8a27c Update translations and other static files. 2024-02-16 16:42:53 -05:00
Greyson Parrelli 3b598e2f07 Update string for username creation. 2024-02-16 15:53:05 -05:00
Greyson Parrelli 03c5a254e8 Fix issue with receiving server delivery receipts at PNI. 2024-02-15 22:05:57 -05:00
Greyson Parrelli bdb34e16c6 Update UI and strings for the duplicate name review screen. 2024-02-15 21:43:36 -05:00
Cody Henthorne e7c018283a Perform directory refresh after a PNI invite accept. 2024-02-15 21:43:36 -05:00
Cody Henthorne ebd8d85a3d Implement UX feedback in new conversation start flows. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 3f8a9e1be2 Reduce max discriminator length to 9. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 0d5961baf9 Update string on find by username screen. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 872ee805d1 Assume PNP capability is true. 2024-02-15 21:43:36 -05:00
Nicholas Tinsley b19bcd88b9 Null check VideoPlayer during view binding. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 5c9d65386b Fix username deletion sync issue. 2024-02-15 21:43:36 -05:00
Greyson Parrelli a86a0938ce Fix avatar fallback photo for self when useSelfProfileAvatar=true. 2024-02-15 21:43:36 -05:00
Greyson Parrelli a886e5f9a0 Directly show about sheet when you show a recipient sheet for yourself. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 83c1bd61cb Add bottom sheet handle to RecipientBottomSheet. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 4ce1789110 Do not show the discriminator field until one is chosen. 2024-02-15 21:43:36 -05:00
Greyson Parrelli f484fdbbac Remove unused 'registration' variant of username screen. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 57ac7cb328 Show some more info in the about sheet. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 47cdc50a81 Add confirmation dialog when changing username would reset link. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 555ddb5b20 Show dialog for successfully resetting your username link. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 8e8ba23da7 Do not show the QR code shortuct if you have no username. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 54a1b97167 Update username description string in edit profile screen. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 7530d44d28 Refactor username link share screen to enable previews. 2024-02-15 21:43:36 -05:00
Greyson Parrelli 252aa3714e Sync the 'hasCompletedUsernameOnboarding' flag. 2024-02-15 21:43:36 -05:00
Greyson Parrelli ce09e9a217 Update UI for PNP launch megaphone. 2024-02-15 21:43:35 -05:00
Greyson Parrelli 8797236b5a Add migration for ensuring we set the latest pnp settings. 2024-02-15 21:43:35 -05:00
Greyson Parrelli 6097e6c305 Default discoverability to 'off' until registration is complete. 2024-02-15 21:43:35 -05:00
Greyson Parrelli 879fca0e11 Interpret unknown phone number sharing setting as 'off'. 2024-02-15 21:43:35 -05:00
Greyson Parrelli c359ddf3c8 Inline the pnp feature flag. 2024-02-15 21:43:35 -05:00
Greyson Parrelli 8ad77ac7aa Inline the username flag. 2024-02-15 21:43:35 -05:00
Cody Henthorne bd3b779282 Bump version to 6.47.4 2024-02-15 21:43:16 -05:00
Cody Henthorne 42b805eb91 Updated baseline profile. 2024-02-15 21:37:08 -05:00
Cody Henthorne 107f2cd3b1 Update translations and other static files. 2024-02-15 21:31:52 -05:00
Nicholas Tinsley c713ccf76c Don't mark outgoing media as upload only. 2024-02-15 21:26:30 -05:00
Cody Henthorne dd9c65012b Fix NPE when find by phone row not supported. 2024-02-15 19:36:26 -05:00
Cody Henthorne 31e872a34e Bump version to 6.47.3 2024-02-14 20:05:57 -05:00
Cody Henthorne 81579dc9bf Update translations and other static files. 2024-02-14 20:00:20 -05:00
Greyson Parrelli 3c6c03cd75 Fix bug when syncing username deletions. 2024-02-14 11:59:15 -05:00
Greyson Parrelli ba41df19bb Fix thread merges where one thread is inactive. 2024-02-14 11:15:49 -05:00
Cody Henthorne 0cc7178cdc Bump version to 6.47.2 2024-02-13 15:49:18 -05:00
Cody Henthorne 239e4a7e66 Updated baseline profile. 2024-02-13 15:46:30 -05:00
Cody Henthorne 0b031d35e3 Update translations and other static files. 2024-02-13 15:43:43 -05:00
Nicholas Tinsley 32e81049f5 Cycle audio remux feature flag. 2024-02-13 15:39:04 -05:00
Nicholas Tinsley 6a65a1c149 Default to 1x camera in video recording.
Addresses #11955, #12482, #13017

Shout out to @tedgravlin for #13411.
2024-02-13 15:39:04 -05:00
Cody Henthorne 36ea2a7f5d Fix active call manager.
For real this time.
2024-02-13 15:39:04 -05:00
Cody Henthorne cc40c9d09f Bump version to 6.47.1 2024-02-12 16:32:01 -05:00
Cody Henthorne fb9e731e00 Updated baseline profile. 2024-02-12 16:27:07 -05:00
Cody Henthorne 264607e0f3 Update translations and other static files. 2024-02-12 16:21:55 -05:00
Greyson Parrelli cdfc42cc38 Fix possible crash in story replies. 2024-02-12 15:01:58 -05:00
Cody Henthorne 19cfae1da5 Remove duplicate future code. 2024-02-12 15:00:27 -05:00
Jim Gustafson 577b11a349 Update to RingRTC v2.37.1 2024-02-12 09:15:55 -05:00
Clark Chen 671dfceac3 Bump version to 6.47.0 2024-02-09 19:43:41 -05:00
Clark Chen 5626fb74ae Update translations and other static files. 2024-02-09 19:28:27 -05:00
Greyson Parrelli 88d6c91517 Fix some bugs related to how the conversation header renders. 2024-02-09 18:41:26 -05:00
Cody Henthorne aa76cefb1c Update spam UX and reporting flows. 2024-02-09 18:41:26 -05:00
Cody Henthorne a4fde60c1c Fix crash in ActiveCallManager for lower Android versions. 2024-02-09 15:19:25 -05:00
Nicholas Tinsley 8e86612fc2 Derive PIN keyboard type if none set.
Addresses #13338.
2024-02-09 15:11:38 -05:00
Clark f6ded23383 Ignore message latency when latency is before device boot time. 2024-02-09 14:55:58 -05:00
Nicholas Tinsley 2ab689c59b Add plural string for chat deletion. 2024-02-09 13:32:05 -05:00
Greyson Parrelli 155f6a88f8 Ensure one-time kyber prekeys are generated during change number. 2024-02-09 13:32:05 -05:00
Greyson Parrelli 3d84fc9c98 Use 'deleted account' as name for certain unregisterd user cases. 2024-02-09 13:32:05 -05:00
Greyson Parrelli 976e146248 Only count active threads when determining possible communication. 2024-02-09 13:32:05 -05:00
Greyson Parrelli d9b0723194 Make message footer clickable when there's an error. 2024-02-09 13:32:05 -05:00
Greyson Parrelli 0630a6910a Update strings and previews for pnp settings. 2024-02-09 13:32:05 -05:00
Greyson Parrelli bc930345b9 Remove blockSSE feature flag. 2024-02-08 14:10:20 -05:00
Greyson Parrelli d7d7963101 Defer to the latest revision for backup inclusion. 2024-02-08 14:10:20 -05:00
Greyson Parrelli fa2551dfcf Always show the new 'find by phone number' item. 2024-02-08 14:10:20 -05:00
Greyson Parrelli b260a47b49 Fix issue where media sent transcripts didn't trigger thread updates. 2024-02-08 14:10:20 -05:00
Greyson Parrelli 47e55fc621 Do not show add to contacts if there's no e164. 2024-02-08 14:10:20 -05:00
Alex Hart 700fe5e463 Add Find By Username and Find By Phone Number interstitials.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2024-02-08 14:10:20 -05:00
Greyson Parrelli ca3d239ce2 Bump version to 6.46.7 2024-02-08 12:54:26 -05:00
Greyson Parrelli 12385b9c5a Update stores to use a constant accountId for PNIs. 2024-02-08 12:53:22 -05:00
Clark Chen 65b26adb3d Bump version to 6.46.6 2024-02-06 15:26:38 -05:00
Clark Chen 31b8927291 Update translations and other static files. 2024-02-06 15:16:55 -05:00
Cody Henthorne 94ffbb3e8e Fix NPE crash when scanning safety numbers. 2024-02-06 10:32:20 -05:00
Greyson Parrelli 1b7616b4db Fix issue with PNIs in contact sync. 2024-02-06 09:47:12 -05:00
Clark Chen 71850e1e35 Bump version to 6.46.5 2024-02-05 18:56:05 -05:00
Clark Chen 380fb60791 Update translations and other static files. 2024-02-05 18:36:27 -05:00
Greyson Parrelli 2cf9fa0524 Fix crash when opening group story replies. 2024-02-05 16:30:54 -05:00
Nicholas Tinsley c8c4fdc65e Bump version to 6.46.4 2024-02-05 10:59:36 -05:00
Nicholas Tinsley 9fb16be74f Updated baseline profile. 2024-02-05 10:56:19 -05:00
Nicholas Tinsley 2d4d4aab66 Update translations and other static files. 2024-02-05 10:53:17 -05:00
Nicholas Tinsley f18070b78c Revert "Don't recreate attachment InputStream if we don't have to."
This reverts commit 467dae8132.
2024-02-05 10:49:05 -05:00
Nicholas Tinsley adf1d8a43a Bump version to 6.46.3 2024-02-03 14:08:31 -05:00
Nicholas Tinsley f360934ddd Updated baseline profile. 2024-02-03 14:04:58 -05:00
Nicholas Tinsley b986c4d54c Update translations and other static files. 2024-02-03 14:02:12 -05:00
Nicholas Tinsley 4545e70384 Remix audio remuxing into its own feature flag. 2024-02-03 13:57:49 -05:00
Nicholas Tinsley 3c5cbc3114 Bump version to 6.46.2 2024-02-03 12:45:27 -05:00
Nicholas Tinsley 879b0ad95d Updated baseline profile. 2024-02-03 12:41:31 -05:00
Nicholas Tinsley 9982810edb Update translations and other static files. 2024-02-03 12:38:24 -05:00
Greyson Parrelli 699b788187 Fix possible stack overflow in backup export. 2024-02-03 12:26:38 -05:00
Nicholas Tinsley bd61044643 Bump version to 6.46.1 2024-02-02 15:24:32 -05:00
Nicholas Tinsley 6804d58323 Updated baseline profile. 2024-02-02 15:17:04 -05:00
Nicholas Tinsley bfe57b4b8f Update translations and other static files. 2024-02-02 15:12:26 -05:00
Greyson Parrelli e3fe852a34 Do not backup past revision if latest revision is soon-to-expire. 2024-02-02 15:07:30 -05:00
Cody Henthorne 673fe2625c Show iDEAL 0,01 temporary charge warning dialog for monthly subscription. 2024-02-02 15:07:30 -05:00
Cody Henthorne e798feb1d7 Use paymentId from create PayPal intent instead of confirm. 2024-02-02 15:07:30 -05:00
Nicholas Tinsley fa937f9f43 Bump version to 6.46.0 2024-01-31 22:42:48 -05:00
Nicholas Tinsley ec5f3fc333 Updated baseline profile. 2024-01-31 22:37:58 -05:00
Nicholas Tinsley d85c150a8c Update translations and other static files. 2024-01-31 22:35:13 -05:00
Nicholas Tinsley a5faf0e098 Match output video file size limit in video editor. 2024-01-31 22:13:46 -05:00
Alex Hart 1234c63836 Add scaffolding for backupsV2. 2024-01-31 22:13:46 -05:00
Cody Henthorne 91920319c7 Cycle calling without a service remote config. 2024-01-31 22:13:46 -05:00
Nicholas Tinsley 950d9d5a4c Validate image preview as URI.
Resolves #13392.
2024-01-31 22:13:46 -05:00
Nicholas Tinsley 467dae8132 Don't recreate attachment InputStream if we don't have to. 2024-01-31 22:13:46 -05:00
Nicholas Tinsley d1ef9d5dcf Align video compression limit with attachment limit. 2024-01-31 22:13:46 -05:00
Nicholas Tinsley a60419a442 Add remote config for GIF keyboard page. 2024-01-31 22:13:46 -05:00
Nicholas Tinsley f97a034c34 Null check multi share attachment quality guard. 2024-01-31 22:13:46 -05:00
Greyson Parrelli 4d0fbe2343 Add a dark theme for spinner. 2024-01-31 22:13:46 -05:00
Jameson Williams 1d5e108cd4 QA target can pass on ARM MacBook
The Conscrypt library does not have a native library suitable for
running on the ARM MacBooks. As a result, unit tests that rely on
Conscrypt also don't work on this hardware.

Fortunately, though, the tests will pass without Conscrypt anyway. As a
workaround, we can avoid using Conscrypt only when running the unit test
suite on these machines.

See also: https://github.com/google/conscrypt/issues/1034

Resolves #13382.
2024-01-31 22:13:46 -05:00
Greyson Parrelli 716afc98ac Sync PNI verification status to storage service. 2024-01-31 22:13:46 -05:00
Greyson Parrelli 459607adae Clear local state after requesting to ignore battery optimizations. 2024-01-31 22:13:46 -05:00
Cody Henthorne 784b705265 Fix spoiler in notification with emoji prefix. 2024-01-31 22:13:46 -05:00
Alex Hart 326b95bd10 Upgrade Compose and AndroidX libraries. 2024-01-31 22:13:46 -05:00
Greyson Parrelli 466acaf504 Fix possible exception when rendering SSE event.
Fixes #13390
2024-01-31 22:13:46 -05:00
Maksym Moroz 838165c3e6 Register gradle tasks instead of creating eagerly.
Signed-off-by: Maksym Moroz <maksymmoroz@duck.com>

Resolves #13391
2024-01-31 22:13:46 -05:00
Jim Gustafson e1d7ad7d03 Update to RingRTC v2.37.0 2024-01-31 22:13:46 -05:00
Greyson Parrelli 3776e86b83 Update the types of messages we backup. 2024-01-31 22:13:46 -05:00
Greyson Parrelli ea60858a07 Improve logging around 'contact joined' messages. 2024-01-31 22:13:46 -05:00
Greyson Parrelli d4488c72fb Simplify contact splitting when reading from storage service. 2024-01-31 22:13:46 -05:00
Greyson Parrelli 9146f2fb30 Remove some unused methods.
Fixes ##13384
2024-01-31 22:12:17 -05:00
Greyson Parrelli 92993f967e Clear MSL after SN change. 2024-01-31 22:12:17 -05:00
Greyson Parrelli 2c2735af6d Do not create new threads to show error messages. 2024-01-31 22:12:17 -05:00
Alex Hart 80c0e19692 Improve sizing of shareable QR image. 2024-01-31 22:12:17 -05:00
Alex Hart c9e2162afc Remove old username education fragment. 2024-01-31 22:12:17 -05:00
Nicholas Tinsley 9a52f4e3ff Remux audio if possible when transcoding.
Addresses #11712, #12674, #12945, #13084, #13346.
2024-01-31 22:12:17 -05:00
Greyson Parrelli c0235d4cc2 Ensure contacts are split after profile 404. 2024-01-31 22:12:17 -05:00
Cody Henthorne f1704fbb57 Fix file attachment dedup logic. 2024-01-31 22:12:17 -05:00
Alex Hart 38d5d3ad1b Add polish to usernames UX. 2024-01-31 22:12:17 -05:00
Jameson Williams ec96b4e3aa Update Glide to use ksp, drop kapt.
Resolves #13381
2024-01-31 22:12:17 -05:00
Cody Henthorne aa33fd44b8 Remove SMS export. 2024-01-31 22:12:17 -05:00
Clark 98865d61dd Convert gv2 update messages to backup distinct protos. 2024-01-31 22:12:17 -05:00
Cody Henthorne 0036b8e2d6 Migrate legacy png and webp to signal symbols. 2024-01-31 22:12:17 -05:00
Alex Hart c021d26103 Add polish to PNP registration settings. 2024-01-31 22:12:17 -05:00
Alex Hart 8bca5b4901 Update PnP description text for everyone listing. 2024-01-31 22:12:17 -05:00
Greyson Parrelli d4db6c8912 Keep some recipient logs longer. 2024-01-31 22:12:17 -05:00
Jameson Williams c93b4909f4 Remove Jetifier from build.
materialish-progress and subsampling-scale-image-view were bringing in
Android Support libraries as transitive dependencies. This required
Jetifier to be run as part of the build. More recent versions of these
dependencies have been released, which now use AndroidX directly. By
upgrading these dependencies, Jetifier is no longer needed to build
Signal.

Resolves #13378
2024-01-31 22:12:17 -05:00
Maksym Moroz 67d3c8e777 Clean up app module build.gradle.kts
Closes #13379

Signed-off-by: Maksym Moroz <maksymmoroz@duck.com>
2024-01-31 22:12:17 -05:00
Nicholas Tinsley 8d44222097 Enable logcat in the video sample app. 2024-01-31 22:12:17 -05:00
Nicholas Tinsley 9cb2024334 Option to persist task in video sample app. 2024-01-31 22:12:17 -05:00
Alex Hart e71bb33b23 Fix conversation bubbles becoming too long.
Co-authored-by: Cody Henthorne <cody@signal.org>
2024-01-31 22:12:17 -05:00
Alex Hart 4ada7c9be9 Implement several polish items for Call Links. 2024-01-31 22:12:16 -05:00
Nicholas Tinsley 56a2d8891f Bump version to 6.45.2 2024-01-31 22:10:10 -05:00
Nicholas Tinsley 4585e90a00 Updated baseline profile. 2024-01-31 21:25:05 -05:00
Nicholas Tinsley 86d2ddc168 Update translations and other static files. 2024-01-31 21:18:34 -05:00
Greyson Parrelli 9d1514308a Fix potential crash during contact splits. 2024-01-31 19:39:39 -05:00
Greyson Parrelli 7abff55981 Bump version to 6.45.1 2024-01-25 19:10:08 -05:00
Greyson Parrelli f9d7eba7d4 Update translations and other static files. 2024-01-25 19:09:11 -05:00
Greyson Parrelli 9ce021afa2 Improve SenderKeyDistributionMessage envelope validation. 2024-01-25 18:59:57 -05:00
Nicholas Tinsley 6fc9055221 Calculate video encoding settings based on trimmed duration. 2024-01-25 18:59:57 -05:00
Greyson Parrelli a3438d3345 Improve DecryptionErrorMessage envelope validation. 2024-01-25 14:42:50 -05:00
Nicholas Tinsley d2cbf11264 Don't fail video send on postprocess failure. 2024-01-25 13:32:03 -05:00
Greyson Parrelli c584156c86 Bump version to 6.45.0 2024-01-23 13:27:17 -05:00
Greyson Parrelli 78e04f3ad8 Update baseline profile. 2024-01-23 13:18:57 -05:00
Greyson Parrelli 6302725678 Update translations and other static files. 2024-01-23 13:18:57 -05:00
Nicholas Tinsley 431e65808d Rotate custom video muxer flag. 2024-01-23 13:18:57 -05:00
Clark 653914f47e Allow multiple message latency percentiles. 2024-01-23 13:18:57 -05:00
Cody Henthorne 96823e944d Introduce ActiveCallManager to prevent android service crashes during call handling. 2024-01-23 13:18:57 -05:00
Nicholas Tinsley ee19520e1b Improve video sample app UI. 2024-01-23 13:18:56 -05:00
Nicholas Tinsley 6f16b3fee7 Add video sample app test dependencies. 2024-01-23 13:18:56 -05:00
Nicholas Tinsley 89ee7e8e19 Update AndroidX test library verisons. 2024-01-23 13:18:56 -05:00
Nicholas Tinsley 3c7996aa99 Rotate instant video feature flag. 2024-01-23 13:18:56 -05:00
Nicholas Tinsley 3a314c565c Deduplicate call participants in call info screen.. 2024-01-23 13:18:56 -05:00
Nicholas Tinsley 8c9b668cd7 Rotate incrementalMac proto field. 2024-01-23 13:18:56 -05:00
Nicholas Tinsley 7666462de2 Support Dolby Vision HDR videos in transcoder. 2024-01-23 13:18:56 -05:00
Nicholas Tinsley 54cf11a78b Request notification permissions for video sample app. 2024-01-23 11:52:04 -05:00
Greyson Parrelli 16b78f0843 Update username recovery flow. 2024-01-23 11:52:04 -05:00
Greyson Parrelli 5e97a6b192 Do not show username education if username is set. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley 595cced5b7 Display overflow button with reactions OR raise hand. 2024-01-23 11:52:04 -05:00
Alex Hart d17f12dd76 Update call event state after revoking a call link. 2024-01-23 11:52:04 -05:00
Alex Hart 8b5498cfbd Fix if check for call links. 2024-01-23 11:52:04 -05:00
Clark Chen e4b755ced8 Correctly count message events for notification heuristic. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley 5b7eb9c332 Update libsignal-client to 0.39.2 2024-01-23 11:52:04 -05:00
Alex Hart cd8e07c102 Do not jump to top of the list when no search query present in story privacy. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley a36f31c2d0 Allow full disablement of StreamingTranscoder output size limit. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley ac0812a6dd Update AndroidX Media3 to 1.2.1. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley 69c864f984 Allow slight variations in video output frame count. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley 3c9a7fd329 Reorganize video converter library packages 2024-01-23 11:52:04 -05:00
Alex Hart f81dc11f61 Prevent crash if a call doesn't have a message associated with it. 2024-01-23 11:52:04 -05:00
Greyson Parrelli ce9a8f62d4 Check for network connectivity before making any substantial username alteration request. 2024-01-23 11:52:04 -05:00
Alex Hart 7e00d50078 Fix add to groups display without query. 2024-01-23 11:52:04 -05:00
Alex Hart 4af3f5038f Fix call labeling in settings page. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley 7bb1c58452 Refactor Mp4FaststartPostProcessor. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley 8e7383be05 Make Mp4Writer output compatible with Quicktime. 2024-01-23 11:52:04 -05:00
Cody Henthorne bc5d27ed90 Prevent calling service crash loop when system restarts us in background. 2024-01-23 11:52:04 -05:00
Greyson Parrelli ae884d79a1 Remove system contact links from undiscoverable contacts. 2024-01-23 11:52:04 -05:00
Pratyush Venkatakrishnan cf89c988cf Set structured name when creating contacts.
Fixes #12305. When creating copies of contacts, set the structured name
fields instead of the unstructured name fields. This fixes bugs where
certain types of names are displayed incorrectly because the structured
name is inaccurately reconstructed from the unstructured name.

Closes #13366
2024-01-23 11:52:04 -05:00
Cody Henthorne c54313c32e Add another verification code test case. 2024-01-23 11:52:04 -05:00
Cody Henthorne 3e001ddf1b Fix poor calling audio on certain devices. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley c41795e7f0 Extract postprocessor lambda into interface. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley 52120afdbd Remove dependency on Guava limited stream. 2024-01-23 11:52:04 -05:00
Alex Hart 73d98da32b Polish about sheet UX. 2024-01-23 11:52:04 -05:00
Alex Hart 99f936ff97 Fix about sheet groups in common placeholder count. 2024-01-23 11:52:04 -05:00
Cody Henthorne 15afaeabe3 Use WebpSanitizer. 2024-01-23 11:52:04 -05:00
Greyson Parrelli debf964b5f Add more info to internal conversation details. 2024-01-23 11:52:04 -05:00
Nicholas Tinsley 393730cea9 Fix video sample app output. 2024-01-23 11:52:04 -05:00
Greyson Parrelli 2194fbd535 Add support for fetching archive media metadata. 2024-01-23 11:52:04 -05:00
Alex Hart cf59249d3d Rewrite chat colors delegation. 2024-01-23 11:52:04 -05:00
Alex Hart 2c554a3a20 Implement new ways to connect splash and megaphone. 2024-01-23 11:52:04 -05:00
Alex Hart 7b9554a42c Align profile screen with figma. 2024-01-23 11:52:03 -05:00
Alex Hart dd527ce33c Align pnp privacy settings screens with figma. 2024-01-23 11:52:03 -05:00
Nicholas Tinsley ddcc06c6b7 Update video transcoding sample cleanup file extension. 2024-01-23 11:52:03 -05:00
Alex Hart a827033f25 Ensure incoming story reactions are marked as such. 2024-01-23 11:52:03 -05:00
Alex Hart 01841a4aa8 Set body on OutgoingMessage when adding to a group story. 2024-01-23 11:52:03 -05:00
Alex Hart bb52e7159c Pluralize groups in common. 2024-01-23 11:52:03 -05:00
Ruben De Smet 3988b46a60 Fix typo for generateLastRes*t*ortKyberPreKey. 2024-01-23 11:52:03 -05:00
Nicholas Tinsley caa5e233df Expose StreamingTranscoder configuration options in sample app. 2024-01-23 11:52:03 -05:00
Nicholas Tinsley c7609f9a2a StreamingTranscoder sample app. 2024-01-23 11:52:03 -05:00
Greyson Parrelli 750fd4efe1 Improve safety of update and delete database methods. 2024-01-23 11:52:03 -05:00
Greyson Parrelli e361795184 Improve logging and the naming of some fields. 2024-01-23 11:52:03 -05:00
Greyson Parrelli 64fff2adb2 Do not show contact info for non-discoverable contacts. 2024-01-23 11:52:03 -05:00
farewelltospring 846e398577 Add emoji to content description in EmojiImageView.
Fixes #13358
2024-01-23 11:52:03 -05:00
Greyson Parrelli c1e9ee7a66 Allow changing the PNP config remotely. 2024-01-23 11:52:03 -05:00
Greyson Parrelli 8dc9e09f31 Bump version to 6.44.3 2024-01-23 11:44:33 -05:00
Greyson Parrelli d1930d4936 Update baseline profile. 2024-01-23 11:44:31 -05:00
Greyson Parrelli 14539eb036 Update translations and other static files. 2024-01-23 11:28:39 -05:00
Greyson Parrelli 45f1853c44 Fix issue opening files in external apps. 2024-01-23 11:16:09 -05:00
Cody Henthorne 6eaebd112b Bump version to 6.44.2 2024-01-16 12:00:33 -05:00
Cody Henthorne f0503faeff Updated baseline profile. 2024-01-16 11:54:11 -05:00
Cody Henthorne 64052d9dd2 Update translations and other static files. 2024-01-16 11:48:28 -05:00
Greyson Parrelli db4634a0dd Fix potential NPE during group send. 2024-01-16 10:13:21 -05:00
1624 changed files with 63762 additions and 46855 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ jobs:
uses: gradle/wrapper-validation-action@v1
- name: Build with Gradle
run: ./gradlew qa --parallel
run: ./gradlew qa
- name: Archive reports for failed build
if: ${{ failure() }}
+2 -2
View File
@@ -38,7 +38,7 @@ jobs:
- name: Build with Gradle
if: steps.cache-base.outputs.cache-hit != 'true'
run: ./gradlew assemblePlayProdRelease --parallel
run: ./gradlew assemblePlayProdRelease
- name: Copy base apk
if: steps.cache-base.outputs.cache-hit != 'true'
@@ -50,7 +50,7 @@ jobs:
clean: 'false'
- name: Build with Gradle
run: ./gradlew assemblePlayProdRelease --parallel
run: ./gradlew assemblePlayProdRelease
- name: Copy PR apk
run: mv app/build/outputs/apk/playProd/release/*arm64*.apk diffuse-new.apk
+2 -2
View File
@@ -1,4 +1,4 @@
# Signal Android
# Signal Android
Signal is a simple, powerful, and secure messenger.
@@ -54,7 +54,7 @@ The form and manner of this distribution makes it eligible for export under the
## License
Copyright 2013-2023 Signal
Copyright 2013-2024 Signal Messenger, LLC
Licensed under the GNU AGPLv3: https://www.gnu.org/licenses/agpl-3.0.html
-2
View File
@@ -1,2 +0,0 @@
*.db
*.db.gz
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-106
View File
@@ -1,106 +0,0 @@
import sys
import re
import argparse
import sqlite3
import gzip
from progressbar import ProgressBar, Counter, Timer
from lxml import etree
parser = argparse.ArgumentParser(prog='apntool', description="""Process Android's apn xml files and drop them into an
easily queryable SQLite db. Tested up to version 9 of
their APN file.""")
parser.add_argument('-v', '--version', action='version', version='%(prog)s v1.1')
parser.add_argument('-i', '--input', help='the xml file to parse', default='apns.xml', required=False)
parser.add_argument('-o', '--output', help='the sqlite db output file', default='apns.db', required=False)
parser.add_argument('--quiet', help='do not show progress or verbose instructions', action='store_true', required=False)
parser.add_argument('--no-gzip', help="do not gzip after creation", action='store_true', required=False)
args = parser.parse_args()
def normalized(target):
o2_typo = re.compile(r"02\.co\.uk")
port_typo = re.compile(r"(\d+\.\d+\.\d+\.\d+)\.(\d+)")
leading_zeros = re.compile(r"(/|\.|^)0+(\d+)")
subbed = o2_typo.sub(r'o2.co.uk', target)
subbed = port_typo.sub(r'\1:\2', subbed)
subbed = leading_zeros.sub(r'\1\2', subbed)
return subbed
try:
connection = sqlite3.connect(args.output)
cursor = connection.cursor()
cursor.execute('SELECT SQLITE_VERSION()')
version = cursor.fetchone()
if not args.quiet:
print("SQLite version: %s" % version)
print("Opening %s" % args.input)
cursor.execute("PRAGMA legacy_file_format=ON")
cursor.execute("PRAGMA journal_mode=DELETE")
cursor.execute("PRAGMA page_size=32768")
cursor.execute("VACUUM")
cursor.execute("DROP TABLE IF EXISTS apns")
cursor.execute("""CREATE TABLE apns(_id INTEGER PRIMARY KEY, mccmnc TEXT, mcc TEXT, mnc TEXT, carrier TEXT,
apn TEXT, mmsc TEXT, port INTEGER, type TEXT, protocol TEXT, bearer TEXT, roaming_protocol TEXT,
carrier_enabled INTEGER, mmsproxy TEXT, mmsport INTEGER, proxy TEXT, mvno_match_data TEXT,
mvno_type TEXT, authtype INTEGER, user TEXT, password TEXT, server TEXT)""")
apns = etree.parse(args.input)
root = apns.getroot()
pbar = None
if not args.quiet:
pbar = ProgressBar(widgets=['Processed: ', Counter(), ' apns (', Timer(), ')'], maxval=len(list(root))).start()
count = 0
for apn in root.iter("apn"):
if apn.get("mmsc") is None:
continue
sqlvars = ["?" for x in apn.attrib.keys()] + ["?"]
mccmnc = "%s%s" % (apn.get("mcc"), apn.get("mnc"))
normalized_mmsc = normalized(apn.get("mmsc"))
if normalized_mmsc != apn.get("mmsc"):
print("normalize MMSC: %s => %s" % (apn.get("mmsc"), normalized_mmsc))
apn.set("mmsc", normalized_mmsc)
if not apn.get("mmsproxy") is None:
normalized_mmsproxy = normalized(apn.get("mmsproxy"))
if normalized_mmsproxy != apn.get("mmsproxy"):
print("normalize proxy: %s => %s" % (apn.get("mmsproxy"), normalized_mmsproxy))
apn.set("mmsproxy", normalized_mmsproxy)
values = [apn.get(attrib) for attrib in apn.attrib.keys()] + [mccmnc]
keys = apn.attrib.keys() + ["mccmnc"]
cursor.execute("SELECT 1 FROM apns WHERE mccmnc = ? AND apn = ?", [mccmnc, apn.get("apn")])
if cursor.fetchone() is None:
statement = "INSERT INTO apns (%s) VALUES (%s)" % (", ".join(keys), ", ".join(sqlvars))
cursor.execute(statement, values)
count += 1
if not args.quiet:
pbar.update(count)
if not args.quiet:
pbar.finish()
connection.commit()
print("Successfully written to %s" % args.output)
if not args.no_gzip:
gzipped_file = "%s.gz" % (args.output,)
with open(args.output, 'rb') as orig:
with gzip.open(gzipped_file, 'wb') as gzipped:
gzipped.writelines(orig)
print("Successfully gzipped to %s" % gzipped_file)
if not args.quiet:
print("\nTo include this in the distribution, copy it to the project's assets/databases/ directory.")
print("If you support API 10 or lower, you must use the gzipped version to avoid corruption.")
except sqlite3.Error as e:
if connection:
connection.rollback()
print("Error: %s" % e.args[0])
sys.exit(1)
finally:
if connection:
connection.close()
-3
View File
@@ -1,3 +0,0 @@
argparse>=1.2.1
lxml>=3.3.3
progressbar-latest>=2.4
+27 -48
View File
@@ -21,8 +21,8 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1375
val canonicalVersionName = "6.44.1"
val canonicalVersionCode = 1408
val canonicalVersionName = "7.4.0"
val postFixSize = 100
val abiPostFix: Map<String, Int> = mapOf(
@@ -40,8 +40,6 @@ val selectableVariants = listOf(
"nightlyProdPerf",
"nightlyProdRelease",
"nightlyStagingRelease",
"nightlyPnpPerf",
"nightlyPnpRelease",
"playProdDebug",
"playProdSpinner",
"playProdCanary",
@@ -54,8 +52,6 @@ val selectableVariants = listOf(
"playStagingSpinner",
"playStagingPerf",
"playStagingInstrumentation",
"playPnpDebug",
"playPnpSpinner",
"playStagingRelease",
"websiteProdSpinner",
"websiteProdRelease"
@@ -98,7 +94,6 @@ android {
kotlinOptions {
jvmTarget = signalKotlinJvmTarget
freeCompilerArgs = listOf("-Xallow-result-return-type")
}
keystores["debug"]?.let { properties ->
@@ -164,8 +159,8 @@ android {
versionCode = canonicalVersionCode * postFixSize
versionName = canonicalVersionName
minSdkVersion(signalMinSdkVersion)
targetSdkVersion(signalTargetSdkVersion)
minSdk = signalMinSdkVersion
targetSdk = signalTargetSdkVersion
multiDexEnabled = true
@@ -202,10 +197,9 @@ android {
buildConfigField("String[]", "SIGNAL_SVR2_IPS", rootProject.extra["svr2_ips"] as String)
buildConfigField("String", "SIGNAL_AGENT", "\"OWA\"")
buildConfigField("String", "CDSI_MRENCLAVE", "\"0f6fd79cdfdaa5b2e6337f534d3baf999318b0c462a7ac1f41297a3e4b424a57\"")
buildConfigField("String", "SVR2_MRENCLAVE_DEPRECATED", "\"6ee1042f9e20f880326686dd4ba50c25359f01e9f733eeba4382bca001d45094\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"a6622ad4656e1abcd0bc0ff17c229477747d2ded0495c4ebee7ed35c1789fa97\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
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/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0I=\"")
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\"")
buildConfigField("String[]", "LANGUAGES", "new String[]{ ${languageList().map { "\"$it\"" }.joinToString(separator = ", ")} }")
@@ -214,6 +208,7 @@ android {
buildConfigField("String", "GIPHY_API_KEY", "\"3o6ZsYH6U6Eri53TXy\"")
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/registration/generate.html\"")
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/challenge/generate.html\"")
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.PRODUCTION")
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"unset\"")
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"unset\"")
@@ -221,6 +216,7 @@ android {
buildConfigField("String", "BADGE_STATIC_ROOT", "\"https://updates2.signal.org/static/badges/\"")
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_live_6cmGZopuTsV8novGgJJW9JpC00vLIgtQ1D\"")
buildConfigField("boolean", "TRACING_ENABLED", "false")
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "false")
ndk {
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
@@ -383,27 +379,19 @@ android {
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
buildConfigField("String", "SVR2_MRENCLAVE_DEPRECATED", "\"a8a261420a6bb9b61aa25bf8a79e8bd20d7652531feb3381cbffd446d270be95\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCM=\"")
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\"")
buildConfigField("String", "MOBILE_COIN_ENVIRONMENT", "\"testnet\"")
buildConfigField("String", "SIGNAL_CAPTCHA_URL", "\"https://signalcaptchas.org/staging/registration/generate.html\"")
buildConfigField("String", "RECAPTCHA_PROOF_URL", "\"https://signalcaptchas.org/staging/challenge/generate.html\"")
buildConfigField("org.signal.libsignal.net.Network.Environment", "LIBSIGNAL_NET_ENV", "org.signal.libsignal.net.Network.Environment.STAGING")
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Staging\"")
buildConfigField("String", "STRIPE_PUBLISHABLE_KEY", "\"pk_test_sngOd8FnXNkpce9nPXawKrJD00kIDngZkD\"")
}
create("pnp") {
dimension = "environment"
initWith(getByName("staging"))
applicationIdSuffix = ".pnp"
buildConfigField("String", "BUILD_ENVIRONMENT_TYPE", "\"Pnp\"")
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
}
}
@@ -415,9 +403,7 @@ android {
}
applicationVariants.all {
val variant = this
variant.outputs
outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output ->
if (output.baseName.contains("nightly")) {
@@ -430,10 +416,10 @@ android {
output.versionNameOverride = tag
output.outputFileName = output.outputFileName.replace(".apk", "-${output.versionNameOverride}.apk")
} else {
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
}
} else {
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk")
output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
val abiName: String = output.getFilter("ABI") ?: "universal"
val postFix: Int = abiPostFix[abiName]!!
@@ -447,25 +433,20 @@ android {
}
}
android.variantFilter {
val distribution: String = flavors[0].name
val environment: String = flavors[1].name
val buildType: String = buildType.name
val fullName: String = distribution + environment.capitalize() + buildType.capitalize()
if (!selectableVariants.contains(fullName)) {
ignore = true
androidComponents {
beforeVariants { variant ->
variant.enable = variant.name in selectableVariants
}
}
android.buildTypes.forEach {
val path: String = if (it.name == "release") {
"$projectDir/src/release/java"
} else {
"$projectDir/src/debug/java"
}
val releaseDir = "$projectDir/src/release/java"
val debugDir = "$projectDir/src/debug/java"
sourceSets.findByName(it.name)!!.java.srcDir(path)
android.buildTypes.configureEach {
val path = if (name == "release") releaseDir else debugDir
sourceSets.named(name) {
java.srcDir(path)
}
}
}
@@ -484,10 +465,8 @@ dependencies {
implementation(project(":donations"))
implementation(project(":contacts"))
implementation(project(":qr"))
implementation(project(":sms-exporter"))
implementation(project(":sticky-header-grid"))
implementation(project(":photoview"))
implementation(project(":glide-webp"))
implementation(project(":core-ui"))
implementation(libs.androidx.fragment.ktx)
@@ -507,18 +486,22 @@ dependencies {
implementation(libs.androidx.exifinterface)
implementation(libs.androidx.compose.rxjava3)
implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.multidex)
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.reactivestreams.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.extensions)
implementation(libs.androidx.camera.lifecycle)
implementation(libs.androidx.camera.view)
implementation(libs.androidx.concurrent.futures)
@@ -557,10 +540,6 @@ dependencies {
implementation(libs.android.tooltips) {
exclude(group = "com.android.support", module = "appcompat-v7")
}
implementation(libs.android.smsmms) {
exclude(group = "com.squareup.okhttp", module = "okhttp")
exclude(group = "com.squareup.okhttp", module = "okhttp-urlconnection")
}
implementation(libs.stream)
implementation(libs.lottie)
implementation(libs.signal.android.database.sqlcipher)
-1
View File
@@ -2,7 +2,6 @@
-dontobfuscate
-keepattributes SourceFile,LineNumberTable
-keep class org.whispersystems.** { *; }
-keep class org.signal.libsignal.protocol.** { *; }
-keep class org.thoughtcrime.securesms.** { *; }
-keep class org.signal.donations.json.** { *; }
-keepclassmembers class ** {
@@ -35,4 +35,18 @@ class SignalInstrumentationApplicationContext : ApplicationContext() {
LogDatabase.getInstance(this).logs.trimToSize()
}
}
override fun beginJobLoop() = Unit
/**
* Some of the jobs can interfere with some of the instrumentation tests.
*
* For example, we may try to create a release channel recipient while doing
* an import/backup test.
*
* This can be used to start the job loop if needed for tests that rely on it.
*/
fun beginJobLoopForTests() {
super.beginJobLoop()
}
}
@@ -255,7 +255,7 @@ class BackupTest {
SignalStore.donationsValues().setSubscriber(Subscriber(SubscriberId.generate(), "USD"))
SignalStore.donationsValues().setDisplayBadgesOnProfile(false)
SignalStore.phoneNumberPrivacy().phoneNumberListingMode = PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
SignalStore.settings().isLinkPreviewsEnabled = false
@@ -0,0 +1,48 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.whispersystems.signalservice.api.util.toByteArray
import java.util.UUID
import kotlin.random.Random
object TestRecipientUtils {
private var upperGenAci = 13131313L
private var lowerGenAci = 0L
private var upperGenPni = 12121212L
private var lowerGenPni = 0L
private var groupMasterKeyRandom = Random(12345)
fun generateProfileKey(): ByteArray {
return ProfileKeyUtil.createNew().serialize()
}
fun nextPni(): ByteArray {
synchronized(this) {
lowerGenPni++
var uuid = UUID(upperGenPni, lowerGenPni)
return uuid.toByteArray()
}
}
fun nextAci(): ByteArray {
synchronized(this) {
lowerGenAci++
var uuid = UUID(upperGenAci, lowerGenAci)
return uuid.toByteArray()
}
}
fun generateGroupMasterKey(): ByteArray {
val masterKey = ByteArray(32)
groupMasterKeyRandom.nextBytes(masterKey)
return masterKey
}
}
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
import android.net.Uri
import android.view.View
import androidx.lifecycle.Observer
import com.bumptech.glide.RequestManager
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Rule
@@ -29,7 +30,6 @@ 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.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
@@ -209,8 +209,9 @@ class V2ConversationItemShapeTest {
override val selectedItems: Set<MultiselectPart> = emptySet()
override val isMessageRequestAccepted: Boolean = true
override val searchQuery: String? = null
override val glideRequests: GlideRequests = mockk()
override val requestManager: RequestManager = mockk()
override val isParentInScroll: Boolean = false
override fun getChatColorsData(): ChatColorsDrawable.ChatColorsData = ChatColorsDrawable.ChatColorsData(null, null)
override fun onStartExpirationTimeout(messageRecord: MessageRecord) = Unit
@@ -321,5 +322,11 @@ class V2ConversationItemShapeTest {
override fun onItemClick(item: MultiselectPart?) = Unit
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) = Unit
override fun onShowSafetyTips(forGroup: Boolean) = Unit
override fun onReportSpamLearnMoreClicked() = Unit
override fun onMessageRequestAcceptOptionsClicked() = Unit
}
}
@@ -51,18 +51,16 @@ class AttachmentTableTest {
SignalDatabase.attachments.updateAttachmentData(
attachment,
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
false
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
)
SignalDatabase.attachments.updateAttachmentData(
attachment2,
createMediaStream(byteArrayOf(1, 2, 3)),
false
createMediaStream(byteArrayOf(1, 2, 3))
)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
assertNotEquals(attachment1Info, attachment2Info)
}
@@ -79,18 +77,16 @@ class AttachmentTableTest {
SignalDatabase.attachments.updateAttachmentData(
attachment,
createMediaStream(byteArrayOf(1, 2, 3, 4, 5)),
true
createMediaStream(byteArrayOf(1, 2, 3, 4, 5))
)
SignalDatabase.attachments.updateAttachmentData(
attachment2,
createMediaStream(byteArrayOf(1, 2, 3, 4)),
true
createMediaStream(byteArrayOf(1, 2, 3, 4))
)
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
val attachment1Info = SignalDatabase.attachments.getDataFileInfo(attachment.attachmentId)
val attachment2Info = SignalDatabase.attachments.getDataFileInfo(attachment2.attachmentId)
assertNotEquals(attachment1Info, attachment2Info)
}
@@ -121,15 +117,14 @@ class AttachmentTableTest {
val highDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(highQualityPreUpload)
// WHEN
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData), false)
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData))
// THEN
val previousInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(previousDatabaseAttachmentId, AttachmentTable.DATA_FILE)!!
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val previousInfo = SignalDatabase.attachments.getDataFileInfo(previousDatabaseAttachmentId)!!
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
assertNotEquals(standardInfo, highInfo)
standardInfo.file assertIs previousInfo.file
highInfo.file assertIsNot standardInfo.file
highInfo.file.exists() assertIs true
}
@@ -158,9 +153,9 @@ class AttachmentTableTest {
val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload)
// THEN
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val secondHighInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(secondHighDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val standardInfo = SignalDatabase.attachments.getDataFileInfo(standardDatabaseAttachment.attachmentId)!!
val highInfo = SignalDatabase.attachments.getDataFileInfo(highDatabaseAttachment.attachmentId)!!
val secondHighInfo = SignalDatabase.attachments.getDataFileInfo(secondHighDatabaseAttachment.attachmentId)!!
highInfo.file assertIsNot standardInfo.file
secondHighInfo.file assertIs highInfo.file
@@ -0,0 +1,804 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.Base64
import org.signal.core.util.update
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.MediaStream
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.mms.SentMediaQuality
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.MediaUtil
import org.whispersystems.signalservice.api.push.ServiceId
import java.io.File
import java.util.UUID
import kotlin.random.Random
import kotlin.time.Duration.Companion.days
/**
* Collection of [AttachmentTable] tests focused around deduping logic.
*/
@RunWith(AndroidJUnit4::class)
class AttachmentTableTest_deduping {
companion object {
val DATA_A = byteArrayOf(1, 2, 3)
val DATA_A_COMPRESSED = byteArrayOf(4, 5, 6)
val DATA_A_HASH = byteArrayOf(1, 1, 1)
val DATA_B = byteArrayOf(7, 8, 9)
}
@Before
fun setUp() {
SignalStore.account().setAci(ServiceId.ACI.from(UUID.randomUUID()))
SignalStore.account().setPni(ServiceId.PNI.from(UUID.randomUUID()))
SignalStore.account().setE164("+15558675309")
SignalDatabase.attachments.deleteAllAttachments()
}
/**
* Creates two different files with different data. Should not dedupe.
*/
@Test
fun differentFiles() {
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_B)
assertDataFilesAreDifferent(id1, id2)
}
}
/**
* Inserts files with identical data but with transform properties that make them incompatible. Should not dedupe.
*/
@Test
fun identicalFiles_incompatibleTransforms() {
// Non-matching qualities
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Non-matching video trim flag
test {
val id1 = insertWithData(DATA_A, TransformProperties())
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Non-matching video trim start time
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Non-matching video trim end time
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 1))
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 0, videoTrimEndTimeUs = 2))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Non-matching mp4 fast start
test {
val id1 = insertWithData(DATA_A, TransformProperties(mp4FastStart = true))
val id2 = insertWithData(DATA_A, TransformProperties(mp4FastStart = false))
assertDataFilesAreDifferent(id1, id2)
assertDataHashStartMatches(id1, id2)
}
}
/**
* Inserts files with identical data and compatible transform properties. Should dedupe.
*/
@Test
fun identicalFiles_compatibleTransforms() {
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
}
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
}
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
val id2 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
}
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
val id2 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
}
}
/**
* Walks through various scenarios where files are compressed and uploaded.
*/
@Test
fun compressionAndUploads() {
// Matches after the first is compressed, skip transform properly set
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
}
// Matches after the first is uploaded, skip transform and ending hash properly set
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
upload(id1)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
}
// Mimics sending two files at once. Ensures all fields are kept in sync as we compress and upload.
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, false)
assertSkipTransform(id2, false)
compress(id1, DATA_A_COMPRESSED)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
upload(id1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
}
// Re-use the upload when uploaded recently
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
}
// Do not re-use old uploads
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis() - 100.days.inWholeMilliseconds)
val id2 = insertWithData(DATA_A)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, true)
assertDoesNotHaveRemoteFields(id2)
}
// This isn't so much "desirable behavior" as it is documenting how things work.
// If an attachment is compressed but not uploaded yet, it will have a DATA_HASH_START that doesn't match the actual file content.
// This means that if we insert a new attachment with data that matches the compressed data, we won't find a match.
// This is ok because we don't allow forwarding unsent messages, so the chances of the user somehow sending a file that matches data we compressed are very low.
// What *is* more common is that the user may send DATA_A again, and in this case we will still catch the dedupe (which is already tested above).
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
val id2 = insertWithData(DATA_A_COMPRESSED)
assertDataFilesAreDifferent(id1, id2)
}
// This represents what would happen if you forward an already-send compressed attachment. We should match, skip transform, and skip upload.
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
}
// This represents what would happen if you edited a video, sent it, then forwarded it. We should match, skip transform, and skip upload.
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
}
// This represents what would happen if you edited a video, sent it, then forwarded it, but *edited the forwarded video*. We should not dedupe.
test {
val id1 = insertWithData(DATA_A, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(videoTrim = true, videoTrimStartTimeUs = 1, videoTrimEndTimeUs = 2))
assertDataFilesAreDifferent(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id2, false)
assertDoesNotHaveRemoteFields(id2)
}
// This represents what would happen if you sent an image using standard quality, then forwarded it using high quality.
// Since you're forwarding, it doesn't matter if the new thing has a higher quality, we should still match and skip transform.
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
}
// This represents what would happen if you sent an image using high quality, then forwarded it using standard quality.
// Since you're forwarding, it doesn't matter if the new thing has a lower quality, we should still match and skip transform.
test {
val id1 = insertWithData(DATA_A, TransformProperties(sentMediaQuality = SentMediaQuality.HIGH.code))
compress(id1, DATA_A_COMPRESSED)
upload(id1, uploadTimestamp = System.currentTimeMillis())
val id2 = insertWithData(DATA_A_COMPRESSED, TransformProperties(sentMediaQuality = SentMediaQuality.STANDARD.code))
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertSkipTransform(id1, true)
assertSkipTransform(id1, true)
assertRemoteFieldsMatch(id1, id2)
}
// Make sure that files marked as unhashable are all updated together
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
upload(id1)
upload(id2)
clearHashes(id1)
clearHashes(id2)
val file = dataFile(id1)
SignalDatabase.attachments.markDataFileAsUnhashable(file)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id1)!!
assertTrue(dataFileInfo.hashEnd!!.startsWith("UNHASHABLE-"))
}
}
/**
* Various deletion scenarios to ensure that duped files don't deleted while there's still references.
*/
@Test
fun deletions() {
// Delete original then dupe
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
val dataFile = dataFile(id1)
assertDataFilesAreTheSame(id1, id2)
delete(id1)
assertDeleted(id1)
assertRowAndFileExists(id2)
assertTrue(dataFile.exists())
delete(id2)
assertDeleted(id2)
assertFalse(dataFile.exists())
}
// Delete dupe then original
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
val dataFile = dataFile(id1)
assertDataFilesAreTheSame(id1, id2)
delete(id2)
assertDeleted(id2)
assertRowAndFileExists(id1)
assertTrue(dataFile.exists())
delete(id1)
assertDeleted(id1)
assertFalse(dataFile.exists())
}
// Delete original after it was compressed
test {
val id1 = insertWithData(DATA_A)
compress(id1, DATA_A_COMPRESSED)
val id2 = insertWithData(DATA_A)
delete(id1)
assertDeleted(id1)
assertRowAndFileExists(id2)
assertSkipTransform(id2, true)
}
// Quotes are weak references and should not prevent us from deleting the file
test {
val id1 = insertWithData(DATA_A)
val id2 = insertQuote(id1)
val dataFile = dataFile(id1)
delete(id1)
assertDeleted(id1)
assertRowExists(id2)
assertFalse(dataFile.exists())
}
}
@Test
fun quotes() {
// Basic quote deduping
test {
val id1 = insertWithData(DATA_A)
val id2 = insertQuote(id1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
}
// Making sure remote fields carry
test {
val id1 = insertWithData(DATA_A)
val id2 = insertQuote(id1)
upload(id1)
assertDataFilesAreTheSame(id1, id2)
assertDataHashStartMatches(id1, id2)
assertDataHashEndMatches(id1, id2)
assertRemoteFieldsMatch(id1, id2)
}
// 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)
}
}
/**
* Suite of tests around the migration where we hash all of the attachments and potentially dedupe them.
*/
@Test
fun migration() {
// Verifying that getUnhashedDataFile only returns if there's actually missing hashes
test {
val id = insertWithData(DATA_A)
upload(id)
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
}
// Verifying that getUnhashedDataFile finds the missing hash
test {
val id = insertWithData(DATA_A)
upload(id)
clearHashes(id)
assertNotNull(SignalDatabase.attachments.getUnhashedDataFile())
}
// Verifying that getUnhashedDataFile doesn't return if the file isn't done downloading
test {
val id = insertWithData(DATA_A)
upload(id)
setTransferState(id, AttachmentTable.TRANSFER_PROGRESS_PENDING)
clearHashes(id)
assertNull(SignalDatabase.attachments.getUnhashedDataFile())
}
// If two attachments share the same file, when we backfill the hash, make sure both get their hashes set
test {
val id1 = insertWithData(DATA_A)
val id2 = insertWithData(DATA_A)
upload(id1)
upload(id2)
clearHashes(id1)
clearHashes(id2)
val file = dataFile(id1)
SignalDatabase.attachments.setHashForDataFile(file, DATA_A_HASH)
assertDataHashEnd(id1, DATA_A_HASH)
assertDataHashEndMatches(id1, id2)
}
// Creates a situation where two different attachments have the same data but wrote to different files, and verifies the migration dedupes it
test {
val id1 = insertWithData(DATA_A)
upload(id1)
clearHashes(id1)
val id2 = insertWithData(DATA_A)
upload(id2)
clearHashes(id2)
assertDataFilesAreDifferent(id1, id2)
val file1 = dataFile(id1)
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
assertDataHashEnd(id1, DATA_A_HASH)
assertDataFilesAreDifferent(id1, id2)
val file2 = dataFile(id2)
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertFalse(file2.exists())
}
// We've got three files now with the same data, with two of them sharing a file. We want to make sure *both* entries that share the same file get deduped.
test {
val id1 = insertWithData(DATA_A)
upload(id1)
clearHashes(id1)
val id2 = insertWithData(DATA_A)
val id3 = insertWithData(DATA_A)
upload(id2)
upload(id3)
clearHashes(id2)
clearHashes(id3)
assertDataFilesAreDifferent(id1, id2)
assertDataFilesAreTheSame(id2, id3)
val file1 = dataFile(id1)
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
assertDataHashEnd(id1, DATA_A_HASH)
val file2 = dataFile(id2)
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
assertDataFilesAreTheSame(id1, id2)
assertDataHashEndMatches(id1, id2)
assertDataHashEndMatches(id2, id3)
assertFalse(file2.exists())
}
// We don't want to mess with files that are still downloading, so this makes sure that even if data matches, we don't dedupe and don't delete the file
test {
val id1 = insertWithData(DATA_A)
upload(id1)
clearHashes(id1)
val id2 = insertWithData(DATA_A)
// *not* uploaded
clearHashes(id2)
assertDataFilesAreDifferent(id1, id2)
val file1 = dataFile(id1)
SignalDatabase.attachments.setHashForDataFile(file1, DATA_A_HASH)
assertDataHashEnd(id1, DATA_A_HASH)
val file2 = dataFile(id2)
SignalDatabase.attachments.setHashForDataFile(file2, DATA_A_HASH)
assertDataFilesAreDifferent(id1, id2)
assertTrue(file2.exists())
}
}
private class TestContext {
fun insertWithData(data: ByteArray, transformProperties: TransformProperties = TransformProperties.empty()): AttachmentId {
val uri = BlobProvider.getInstance().forData(data).createForSingleSessionInMemory()
val attachment = UriAttachmentBuilder.build(
id = Random.nextLong(),
uri = uri,
contentType = MediaUtil.IMAGE_JPEG,
transformProperties = transformProperties
)
return SignalDatabase.attachments.insertAttachmentForPreUpload(attachment).attachmentId
}
fun insertQuote(attachmentId: AttachmentId): AttachmentId {
val originalAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.self())
val messageId = SignalDatabase.messages.insertMessageOutbox(
message = OutgoingMessage(
threadRecipient = Recipient.self(),
sentTimeMillis = System.currentTimeMillis(),
body = "some text",
outgoingQuote = QuoteModel(
id = 123,
author = Recipient.self().id,
text = "Some quote text",
isOriginalMissing = false,
attachments = listOf(originalAttachment),
mentions = emptyList(),
type = QuoteModel.Type.NORMAL,
bodyRanges = null
)
),
threadId = threadId,
forceSms = false,
insertListener = null
)
val attachments = SignalDatabase.attachments.getAttachmentsForMessage(messageId)
return attachments[0].attachmentId
}
fun compress(attachmentId: AttachmentId, newData: ByteArray, mp4FastStart: Boolean = false) {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
SignalDatabase.attachments.updateAttachmentData(databaseAttachment, newData.asMediaStream())
SignalDatabase.attachments.markAttachmentAsTransformed(attachmentId, withFastStart = mp4FastStart)
}
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createPointerAttachment(attachmentId, uploadTimestamp), uploadTimestamp)
}
fun delete(attachmentId: AttachmentId) {
SignalDatabase.attachments.deleteAttachment(attachmentId)
}
fun dataFile(attachmentId: AttachmentId): File {
return SignalDatabase.attachments.getDataFileInfo(attachmentId)!!.file
}
fun setTransferState(attachmentId: AttachmentId, transferState: Int) {
// messageId doesn't actually matter -- that's for notifying listeners
SignalDatabase.attachments.setTransferState(messageId = -1, attachmentId = attachmentId, transferState = transferState)
}
fun clearHashes(id: AttachmentId) {
SignalDatabase.attachments.writableDatabase
.update(AttachmentTable.TABLE_NAME)
.values(
AttachmentTable.DATA_HASH_START to null,
AttachmentTable.DATA_HASH_END to null
)
.where("${AttachmentTable.ID} = ?", id)
.run()
}
fun assertDeleted(attachmentId: AttachmentId) {
assertNull("$attachmentId exists, but it shouldn't!", SignalDatabase.attachments.getAttachment(attachmentId))
}
fun assertRowAndFileExists(attachmentId: AttachmentId) {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
assertNotNull("$attachmentId does not exist!", databaseAttachment)
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(attachmentId)
assertTrue("The file for $attachmentId does not exist!", dataFileInfo!!.file.exists())
}
fun assertRowExists(attachmentId: AttachmentId) {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)
assertNotNull("$attachmentId does not exist!", databaseAttachment)
}
fun assertDataFilesAreTheSame(lhs: AttachmentId, rhs: AttachmentId) {
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
assert(lhsInfo.file.exists())
assert(rhsInfo.file.exists())
assertEquals(lhsInfo.file, rhsInfo.file)
assertEquals(lhsInfo.length, rhsInfo.length)
assertArrayEquals(lhsInfo.random, rhsInfo.random)
}
fun assertDataFilesAreDifferent(lhs: AttachmentId, rhs: AttachmentId) {
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
assert(lhsInfo.file.exists())
assert(rhsInfo.file.exists())
assertNotEquals(lhsInfo.file, rhsInfo.file)
}
fun assertDataHashStartMatches(lhs: AttachmentId, rhs: AttachmentId) {
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
assertNotNull(lhsInfo.hashStart)
assertEquals("DATA_HASH_START's did not match!", lhsInfo.hashStart, rhsInfo.hashStart)
}
fun assertDataHashEndMatches(lhs: AttachmentId, rhs: AttachmentId) {
val lhsInfo = SignalDatabase.attachments.getDataFileInfo(lhs)!!
val rhsInfo = SignalDatabase.attachments.getDataFileInfo(rhs)!!
assertNotNull(lhsInfo.hashEnd)
assertEquals("DATA_HASH_END's did not match!", lhsInfo.hashEnd, rhsInfo.hashEnd)
}
fun assertDataHashEnd(id: AttachmentId, byteArray: ByteArray) {
val dataFileInfo = SignalDatabase.attachments.getDataFileInfo(id)!!
assertArrayEquals(byteArray, Base64.decode(dataFileInfo.hashEnd!!))
}
fun assertRemoteFieldsMatch(lhs: AttachmentId, rhs: AttachmentId) {
val lhsAttachment = SignalDatabase.attachments.getAttachment(lhs)!!
val rhsAttachment = SignalDatabase.attachments.getAttachment(rhs)!!
assertEquals(lhsAttachment.remoteLocation, rhsAttachment.remoteLocation)
assertEquals(lhsAttachment.remoteKey, rhsAttachment.remoteKey)
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
assertEquals(lhsAttachment.cdnNumber, rhsAttachment.cdnNumber)
}
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
assertEquals(0, databaseAttachment.uploadTimestamp)
assertNull(databaseAttachment.remoteLocation)
assertNull(databaseAttachment.remoteDigest)
assertNull(databaseAttachment.remoteKey)
assertEquals(0, databaseAttachment.cdnNumber)
}
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
val transformProperties = SignalDatabase.attachments.getTransformProperties(attachmentId)!!
assertEquals("Incorrect skipTransform!", transformProperties.skipTransform, state)
}
private fun ByteArray.asMediaStream(): MediaStream {
return MediaStream(this.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
}
private fun createPointerAttachment(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): PointerAttachment {
val location = "somewhere-${Random.nextLong()}"
val key = "somekey-${Random.nextLong()}"
val digest = Random.nextBytes(32)
val incrementalDigest = Random.nextBytes(16)
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
return PointerAttachment(
"image/jpeg",
AttachmentTable.TRANSFER_PROGRESS_DONE,
databaseAttachment.size, // size
null,
3, // cdnNumber
location,
key,
digest,
incrementalDigest,
5, // incrementalMacChunkSize
null,
databaseAttachment.voiceNote,
databaseAttachment.borderless,
databaseAttachment.videoGif,
databaseAttachment.width,
databaseAttachment.height,
uploadTimestamp,
databaseAttachment.caption,
databaseAttachment.stickerLocator,
databaseAttachment.blurHash
)
}
}
private fun test(content: TestContext.() -> Unit) {
SignalDatabase.attachments.deleteAllAttachments()
val context = TestContext()
context.content()
}
}
@@ -7,7 +7,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.signal.core.util.delete
import org.signal.core.util.deleteAll
import org.signal.core.util.readToList
import org.signal.core.util.requireLong
import org.signal.core.util.withinTransaction
@@ -33,8 +33,8 @@ class GroupTableTest {
fun setUp() {
groupTable = SignalDatabase.groups
groupTable.writableDatabase.delete(GroupTable.TABLE_NAME).run()
groupTable.writableDatabase.delete(GroupTable.MembershipTable.TABLE_NAME).run()
groupTable.writableDatabase.deleteAll(GroupTable.TABLE_NAME)
groupTable.writableDatabase.deleteAll(GroupTable.MembershipTable.TABLE_NAME)
}
@Test
@@ -10,7 +10,7 @@ import org.signal.core.util.forEach
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.updateAll
import org.thoughtcrime.securesms.crash.CrashConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.testing.assertIs
@@ -220,7 +220,7 @@ class LogDatabaseTest {
)
db.writableDatabase
.update(LogDatabase.CrashTable.TABLE_NAME)
.updateAll(LogDatabase.CrashTable.TABLE_NAME)
.values(LogDatabase.CrashTable.LAST_PROMPTED_AT to currentTime)
.run()
@@ -11,8 +11,6 @@ import org.signal.core.util.CursorUtil
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import java.util.UUID
@@ -59,7 +57,7 @@ class RecipientTableTest {
SignalDatabase.recipients.setProfileName(hiddenRecipient, ProfileName.fromParts("Hidden", "Person"))
SignalDatabase.recipients.markHidden(hiddenRecipient)
val results = SignalDatabase.recipients.querySignalContacts("Hidden", false)!!
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Hidden", false))!!
assertEquals(0, results.count)
}
@@ -130,7 +128,7 @@ class RecipientTableTest {
SignalDatabase.recipients.setProfileName(blockedRecipient, ProfileName.fromParts("Blocked", "Person"))
SignalDatabase.recipients.setBlocked(blockedRecipient, true)
val results = SignalDatabase.recipients.querySignalContacts("Blocked", false)!!
val results = SignalDatabase.recipients.querySignalContacts(RecipientTable.ContactSearchQuery("Blocked", false))!!
assertEquals(0, results.count)
}
@@ -167,8 +165,6 @@ class RecipientTableTest {
@Test
fun givenARecipientWithPniAndAci_whenIMarkItUnregistered_thenIExpectItToBeSplit() {
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
SignalDatabase.recipients.markUnregistered(mainId)
@@ -185,12 +181,10 @@ class RecipientTableTest {
@Test
fun givenARecipientWithPniAndAci_whenISplitItForStorageSync_thenIExpectItToBeSplit() {
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
val mainRecord = SignalDatabase.recipients.getRecord(mainId)
SignalDatabase.recipients.splitForStorageSync(mainRecord.storageId!!)
SignalDatabase.recipients.splitForStorageSyncIfNecessary(mainRecord.aci!!)
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
@@ -18,6 +18,7 @@ import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.exists
import org.signal.core.util.orNull
import org.signal.core.util.readToSingleBoolean
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
@@ -40,8 +41,6 @@ import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
@@ -57,7 +56,6 @@ class RecipientTableTest_getAndPossiblyMerge {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
}
@Test
@@ -109,6 +107,18 @@ class RecipientTableTest_getAndPossiblyMerge {
val record = SignalDatabase.recipients.getRecord(id)
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
}
test("e164+pni+aci insert, pni verified") {
val id = process(E164_A, PNI_A, ACI_A, pniVerified = true)
expect(E164_A, PNI_A, ACI_A)
expectPniVerified()
val record = SignalDatabase.recipients.getRecord(id)
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered)
process(E164_A, PNI_A, ACI_A, pniVerified = false)
expectPniVerified()
}
}
@Test
@@ -164,6 +174,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, ACI_A)
expectNoSessionSwitchoverEvent()
expectPniVerified()
}
test("no match, all fields") {
@@ -225,6 +236,8 @@ class RecipientTableTest_getAndPossiblyMerge {
given(E164_A, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A, pniVerified = true)
expect(E164_A, PNI_A, ACI_A)
expectPniVerified()
}
test("e164 and aci matches, all provided, new pni") {
@@ -694,6 +707,8 @@ class RecipientTableTest_getAndPossiblyMerge {
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectPniVerified()
}
test("merge, e164+pni & aci, pni session, pni verified") {
@@ -706,6 +721,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
expectPniVerified()
}
test("merge, e164+pni & e164+pni+aci, change number") {
@@ -760,6 +776,18 @@ class RecipientTableTest_getAndPossiblyMerge {
expectThreadMergeEvent(E164_A)
}
test("merge, e164+pni & e164+aci, pni+aci provided, change number") {
given(E164_A, PNI_A, null)
given(E164_B, null, ACI_A)
process(null, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
expectChangeNumberEvent()
}
test("merge, e164 + pni reassigned, aci abandoned") {
given(E164_A, PNI_A, ACI_A)
given(E164_B, PNI_B, ACI_B)
@@ -772,6 +800,17 @@ class RecipientTableTest_getAndPossiblyMerge {
expectChangeNumberEvent()
}
test("merge, e164 follows pni+aci") {
given(E164_A, PNI_A, null)
given(null, null, ACI_A)
process(null, PNI_A, ACI_A, pniVerified = true)
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
expectPniVerified()
}
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
given(E164_SELF, null, ACI_SELF)
given(null, null, ACI_A)
@@ -874,8 +913,8 @@ class RecipientTableTest_getAndPossiblyMerge {
// Thread validation
assertEquals(threadIdAci, retrievedThreadId)
Assert.assertNull(SignalDatabase.threads.getThreadIdFor(recipientIdE164))
Assert.assertNull(SignalDatabase.threads.getThreadRecord(threadIdE164))
assertNull(SignalDatabase.threads.getThreadIdFor(recipientIdE164))
assertNull(SignalDatabase.threads.getThreadRecord(threadIdE164))
// SMS validation
val sms1: MessageRecord = SignalDatabase.messages.getMessageRecord(smsId1)!!
@@ -919,10 +958,10 @@ class RecipientTableTest_getAndPossiblyMerge {
// Identity validation
assertEquals(identityKeyAci, SignalDatabase.identities.getIdentityStoreRecord(ACI_A.toString())!!.identityKey)
Assert.assertNull(SignalDatabase.identities.getIdentityStoreRecord(E164_A))
assertNull(SignalDatabase.identities.getIdentityStoreRecord(E164_A))
// Session validation
Assert.assertNotNull(SignalDatabase.sessions.load(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1)))
assertNotNull(SignalDatabase.sessions.load(ACI_SELF, SignalProtocolAddress(ACI_A.toString(), 1)))
// Reaction validation
val reactionsSms: List<ReactionRecord> = SignalDatabase.reactions.getReactions(MessageId(smsId1))
@@ -1037,6 +1076,10 @@ class RecipientTableTest_getAndPossiblyMerge {
if (!test.sessionSwitchoverExpected) {
test.expectNoSessionSwitchoverEvent()
}
if (!test.pniVerifiedExpected) {
test.expectPniNotVerified()
}
} catch (e: Throwable) {
if (e.javaClass != exception) {
val error = java.lang.AssertionError("[$name] ${e.message}")
@@ -1056,6 +1099,7 @@ class RecipientTableTest_getAndPossiblyMerge {
var changeNumberExpected = false
var threadMergeExpected = false
var sessionSwitchoverExpected = false
var pniVerifiedExpected = false
init {
// Need to delete these first to prevent foreign key crash
@@ -1207,6 +1251,24 @@ class RecipientTableTest_getAndPossiblyMerge {
assertNull("Unexpected thread merge event!", getLatestThreadMergeEvent(outputRecipientId))
}
fun expectPniVerified() {
assertTrue("Expected PNI to be verified!", isPniVerified(outputRecipientId))
pniVerifiedExpected = true
}
fun expectPniNotVerified() {
assertFalse("Expected PNI to be not be verified!", isPniVerified(outputRecipientId))
}
private fun isPniVerified(recipientId: RecipientId): Boolean {
return SignalDatabase.rawDatabase
.select(RecipientTable.PNI_SIGNATURE_VERIFIED)
.from(RecipientTable.TABLE_NAME)
.where("${RecipientTable.ID} = ?", recipientId)
.run()
.readToSingleBoolean(false)
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientTable.TABLE_NAME,
@@ -290,7 +290,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
from = sender,
timestamp = wallClock,
groupId = groupId,
groupContext = groupContext
groupContext = groupContext,
serverGuid = null
)
}
@@ -37,7 +37,7 @@ import java.util.Optional
*
* Handles setting up a mock web server for API calls, and provides mockable versions of [SignalServiceNetworkAccess].
*/
class InstrumentationApplicationDependencyProvider(application: Application, default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
class InstrumentationApplicationDependencyProvider(val application: Application, private val default: ApplicationDependencyProvider) : ApplicationDependencies.Provider by default {
private val serviceTrustStore: TrustStore
private val uncensoredConfiguration: SignalServiceConfiguration
@@ -52,7 +52,7 @@ class MessageContentProcessor__recipientStatusTest {
processor.process(
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0])),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
)
@@ -64,7 +64,7 @@ class MessageContentProcessor__recipientStatusTest {
processor.process(
envelope = MessageContentFuzzer.envelope(envelopeTimestamp),
content = MessageContentFuzzer.syncSentTextMessage(initialTextMessage, deliveredTo = listOf(harness.others[0], harness.others[1]), recipientUpdate = true),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, groupId = groupId),
serverDeliveredTimestamp = MessageContentFuzzer.fuzzServerDeliveredTimestamp(envelopeTimestamp)
)
@@ -0,0 +1,225 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.messages
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.unmockkStatic
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.jobs.ThreadUpdateJob
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.GroupTestingUtils
import org.thoughtcrime.securesms.testing.MessageContentFuzzer
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIs
import java.util.UUID
@Suppress("ClassName")
@RunWith(AndroidJUnit4::class)
class SyncMessageProcessorTest_readSyncs {
@get:Rule
val harness = SignalActivityRule(createGroup = true)
private lateinit var alice: RecipientId
private lateinit var bob: RecipientId
private lateinit var group: GroupTestingUtils.TestGroupInfo
private lateinit var processor: MessageContentProcessor
@Before
fun setUp() {
alice = harness.others[0]
bob = harness.others[1]
group = harness.group!!
processor = MessageContentProcessor(harness.context)
val threadIdSlot = slot<Long>()
mockkStatic(ThreadUpdateJob::class)
every { ThreadUpdateJob.enqueue(capture(threadIdSlot)) } answers {
SignalDatabase.threads.update(threadIdSlot.captured, false)
}
}
@After
fun tearDown() {
unmockkStatic(ThreadUpdateJob::class)
}
@Test
fun handleSynchronizeReadMessage() {
val messageHelper = MessageHelper()
val message1Timestamp = messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(alice to message1Timestamp, alice to message2Timestamp)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
@Test
fun handleSynchronizeReadMessageMissingTimestamp() {
val messageHelper = MessageHelper()
messageHelper.incomingText().timestamp
val message2Timestamp = messageHelper.incomingText().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(alice to message2Timestamp)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
@Test
fun handleSynchronizeReadWithEdits() {
val messageHelper = MessageHelper()
val message1Timestamp = messageHelper.incomingText().timestamp
messageHelper.syncReadMessage(alice to message1Timestamp)
val editMessage1Timestamp1 = messageHelper.incomingEditText(message1Timestamp).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(editMessage1Timestamp1).timestamp
val message2Timestamp = messageHelper.incomingMedia().timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(alice to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
@Test
fun handleSynchronizeReadWithEditsInGroup() {
val messageHelper = MessageHelper()
val message1Timestamp = messageHelper.incomingText(sender = alice, destination = group.recipientId).timestamp
messageHelper.syncReadMessage(alice to message1Timestamp)
val editMessage1Timestamp1 = messageHelper.incomingEditText(targetTimestamp = message1Timestamp, sender = alice, destination = group.recipientId).timestamp
val editMessage1Timestamp2 = messageHelper.incomingEditText(targetTimestamp = editMessage1Timestamp1, sender = alice, destination = group.recipientId).timestamp
val message2Timestamp = messageHelper.incomingMedia(sender = bob, destination = group.recipientId).timestamp
val threadId = SignalDatabase.threads.getThreadIdFor(group.recipientId)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 2
messageHelper.syncReadMessage(bob to message2Timestamp, alice to editMessage1Timestamp1, alice to editMessage1Timestamp2)
threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
threadRecord.unreadCount assertIs 0
}
private inner class MessageHelper(var startTime: Long = System.currentTimeMillis()) {
fun incomingText(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun incomingMedia(sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.fuzzStickerMediaMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun incomingEditText(targetTimestamp: Long = System.currentTimeMillis(), sender: RecipientId = alice, destination: RecipientId = harness.self.id): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.editTextMessage(
targetTimestamp = targetTimestamp,
editedDataMessage = MessageContentFuzzer.fuzzTextMessage(
sentTimestamp = messageData.timestamp,
groupContextV2 = if (destination == group.recipientId) group.groupV2Context else null
).dataMessage!!
),
metadata = MessageContentFuzzer.envelopeMetadata(
source = sender,
destination = harness.self.id,
groupId = if (destination == group.recipientId) group.groupId else null
),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
fun syncReadMessage(vararg reads: Pair<RecipientId, Long>): MessageData {
startTime += 1000
val messageData = MessageData(timestamp = startTime)
processor.process(
envelope = MessageContentFuzzer.envelope(messageData.timestamp, serverGuid = messageData.serverGuid),
content = MessageContentFuzzer.syncReadsMessage(reads.toList()),
metadata = MessageContentFuzzer.envelopeMetadata(harness.self.id, harness.self.id, sourceDeviceId = 2),
serverDeliveredTimestamp = messageData.timestamp + 10
)
return messageData
}
}
private data class MessageData(val serverGuid: UUID = UUID.randomUUID(), val timestamp: Long)
}
@@ -57,27 +57,10 @@ class UsernameEditFragmentTest {
InstrumentationApplicationDependencyProvider.clearHandlers()
}
@Test
fun testUsernameCreationInRegistration() {
val scenario = createScenario(true)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.toolbar)).check { view, noViewFoundException ->
noViewFoundException.assertIsNull()
val toolbar = view as Toolbar
toolbar.navigationIcon.assertIsNull()
}
onView(withText(R.string.UsernameEditFragment__add_a_username)).check(matches(isDisplayed()))
onView(withContentDescription(R.string.load_more_header__loading)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)))
}
@Ignore("Flakey espresso test.")
@Test
fun testUsernameCreationOutsideOfRegistration() {
val scenario = createScenario()
val scenario = createScenario(UsernameEditMode.NORMAL)
scenario.moveToState(Lifecycle.State.RESUMED)
@@ -108,7 +91,7 @@ class UsernameEditFragmentTest {
}
)
val scenario = createScenario(isInRegistration = true)
val scenario = createScenario(UsernameEditMode.NORMAL)
scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.username_text)).perform(typeText(nickname))
@@ -132,8 +115,8 @@ class UsernameEditFragmentTest {
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
}
private fun createScenario(isInRegistration: Boolean = false): FragmentScenario<UsernameEditFragment> {
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle()
private fun createScenario(mode: UsernameEditMode = UsernameEditMode.NORMAL): FragmentScenario<UsernameEditFragment> {
val fragmentArgs = UsernameEditFragmentArgs.Builder().setMode(mode).build().toBundle()
return launchFragmentInContainer(
fragmentArgs = fragmentArgs,
themeResId = R.style.Signal_DayNight_NoActionBar
@@ -12,8 +12,6 @@ import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.storage.SignalContactRecord
@@ -29,11 +27,10 @@ class ContactRecordProcessorTest {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
}
@Test
fun process_splitContact_normalSplit() {
fun process_splitContact_normalSplit_twoRecords() {
// GIVEN
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
setStorageId(originalId, STORAGE_ID_A)
@@ -69,6 +66,35 @@ class ContactRecordProcessorTest {
assertNotEquals(byAci, byE164)
}
@Test
fun process_splitContact_normalSplit_oneRecord() {
// GIVEN
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
setStorageId(originalId, STORAGE_ID_A)
val remote = buildRecord(
STORAGE_ID_B,
ContactRecord(
aci = ACI_A.toString(),
unregisteredAtTimestamp = 100
)
)
// WHEN
val subject = ContactRecordProcessor()
subject.process(listOf(remote), StorageSyncHelper.KEY_GENERATOR)
// THEN
val byAci: RecipientId = SignalDatabase.recipients.getByAci(ACI_A).get()
val byE164: RecipientId = SignalDatabase.recipients.getByE164(E164_A).get()
val byPni: RecipientId = SignalDatabase.recipients.getByPni(PNI_A).get()
assertEquals(originalId, byAci)
assertEquals(byE164, byPni)
assertNotEquals(byAci, byE164)
}
@Test
fun process_splitContact_doNotSplitIfAciRecordIsRegistered() {
// GIVEN
@@ -40,7 +40,7 @@ class AliceClient(val serviceId: ServiceId, val e164: String, val trustRoot: ECK
ApplicationDependencies.getIncomingMessageObserver()
.processEnvelope(bufferedStore, envelope, serverDeliveredTimestamp)
?.mapNotNull { it.run() }
?.forEach { ApplicationDependencies.getJobManager().add(it) }
?.forEach { it.enqueue() }
bufferedStore.flushToDisk()
val end = System.currentTimeMillis()
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.testing
import okio.ByteString.Companion.toByteString
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.Member
import org.signal.storageservice.protos.groups.local.DecryptedGroup
@@ -9,6 +10,7 @@ import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.internal.push.GroupContextV2
import kotlin.random.Random
/**
@@ -46,5 +48,8 @@ object GroupTestingUtils {
return member(aci = requireAci())
}
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId)
data class TestGroupInfo(val groupId: GroupId.V2, val masterKey: GroupMasterKey, val recipientId: RecipientId) {
val groupV2Context: GroupContextV2
get() = GroupContextV2(masterKey = masterKey.serialize().toByteString(), revision = 0)
}
}
@@ -12,6 +12,7 @@ import org.whispersystems.signalservice.internal.push.AttachmentPointer
import org.whispersystems.signalservice.internal.push.BodyRange
import org.whispersystems.signalservice.internal.push.Content
import org.whispersystems.signalservice.internal.push.DataMessage
import org.whispersystems.signalservice.internal.push.EditMessage
import org.whispersystems.signalservice.internal.push.Envelope
import org.whispersystems.signalservice.internal.push.GroupContextV2
import org.whispersystems.signalservice.internal.push.SyncMessage
@@ -33,22 +34,22 @@ object MessageContentFuzzer {
/**
* Create an [Envelope].
*/
fun envelope(timestamp: Long): Envelope {
fun envelope(timestamp: Long, serverGuid: UUID = UUID.randomUUID()): Envelope {
return Envelope.Builder()
.timestamp(timestamp)
.serverTimestamp(timestamp + 5)
.serverGuid(UUID.randomUUID().toString())
.serverGuid(serverGuid.toString())
.build()
}
/**
* Create metadata to match an [Envelope].
*/
fun envelopeMetadata(source: RecipientId, destination: RecipientId, groupId: GroupId.V2? = null): EnvelopeMetadata {
fun envelopeMetadata(source: RecipientId, destination: RecipientId, sourceDeviceId: Int = 1, groupId: GroupId.V2? = null): EnvelopeMetadata {
return EnvelopeMetadata(
sourceServiceId = Recipient.resolved(source).requireServiceId(),
sourceE164 = null,
sourceDeviceId = 1,
sourceDeviceId = sourceDeviceId,
sealedSender = true,
groupId = groupId?.decodedId,
destinationServiceId = Recipient.resolved(destination).requireServiceId()
@@ -60,10 +61,11 @@ object MessageContentFuzzer {
* - An expire timer value
* - Bold style body ranges
*/
fun fuzzTextMessage(groupContextV2: GroupContextV2? = null): Content {
fun fuzzTextMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
timestamp = sentTimestamp
body = string()
if (random.nextBoolean()) {
expireTimer = random.nextInt(0..28.days.inWholeSeconds.toInt())
@@ -87,6 +89,20 @@ object MessageContentFuzzer {
.build()
}
/**
* Create an edit message.
*/
fun editTextMessage(targetTimestamp: Long, editedDataMessage: DataMessage): Content {
return Content.Builder()
.editMessage(
EditMessage.Builder().buildWith {
targetSentTimestamp = targetTimestamp
dataMessage = editedDataMessage
}
)
.build()
}
/**
* Create a sync sent text message for the given [DataMessage].
*/
@@ -116,6 +132,24 @@ object MessageContentFuzzer {
).build()
}
/**
* Create a sync reads message for the given [RecipientId] and message timestamp pairings.
*/
fun syncReadsMessage(timestamps: List<Pair<RecipientId, Long>>): Content {
return Content
.Builder()
.syncMessage(
SyncMessage.Builder().buildWith {
read = timestamps.map { (senderId, timestamp) ->
SyncMessage.Read.Builder().buildWith {
this.senderAci = Recipient.resolved(senderId).requireAci().toString()
this.timestamp = timestamp
}
}
}
).build()
}
/**
* Create a random media message that may be:
* - A text body
@@ -184,22 +218,21 @@ object MessageContentFuzzer {
}
/**
* Create a random media message that can never contain a text body. It may be:
* - A sticker
* Create a random media message that contains a sticker.
*/
fun fuzzMediaMessageNoText(previousMessages: List<TestMessage> = emptyList()): Content {
fun fuzzStickerMediaMessage(sentTimestamp: Long? = null, groupContextV2: GroupContextV2? = null): Content {
return Content.Builder()
.dataMessage(
DataMessage.Builder().buildWith {
if (random.nextFloat() < 0.9) {
sticker = DataMessage.Sticker.Builder().buildWith {
packId = byteString(length = 24)
packKey = byteString(length = 128)
stickerId = random.nextInt()
data_ = attachmentPointer()
emoji = emojis.random(random)
}
timestamp = sentTimestamp
sticker = DataMessage.Sticker.Builder().buildWith {
packId = byteString(length = 24)
packKey = byteString(length = 128)
stickerId = random.nextInt()
data_ = attachmentPointer()
emoji = emojis.random(random)
}
groupV2 = groupContextV2
}
).build()
}
@@ -141,7 +141,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.testing
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
@@ -34,7 +36,8 @@ class SignalDatabaseRule(
private fun deleteAllThreads() {
if (deleteAllThreadsOnEachRun) {
SignalDatabase.threads.clearForTests()
SignalDatabase.threads.deleteAllConversations()
SignalDatabase.rawDatabase.deleteAll(ThreadTable.TABLE_NAME)
}
}
}
@@ -6,6 +6,6 @@ package org.thoughtcrime.securesms.util;
public final class FeatureFlagsAccessor {
public static void forceValue(String key, Object value) {
FeatureFlags.FORCED_VALUES.put(FeatureFlags.PHONE_NUMBER_PRIVACY, true);
FeatureFlags.FORCED_VALUES.put(key, value);
}
}
@@ -100,7 +100,7 @@ object TestUsers {
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true, true, true, true, true, true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()
@@ -118,7 +118,8 @@ class ConversationElementGenerator {
null,
null,
0,
false
false,
null
)
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
@@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import androidx.navigation.navGraphViewModels
import com.bumptech.glide.Glide
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.LifecycleDisposable
@@ -33,6 +34,7 @@ import org.thoughtcrime.securesms.conversation.colors.Colorizer
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2
import org.thoughtcrime.securesms.conversation.v2.items.ChatColorsDrawable
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
@@ -41,7 +43,6 @@ 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.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator
@@ -61,11 +62,13 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = ConversationAdapterV2(
lifecycleOwner = viewLifecycleOwner,
glideRequests = GlideApp.with(this),
requestManager = Glide.with(this),
clickListener = ClickListener(),
hasWallpaper = springboardViewModel.hasWallpaper.value,
colorizer = Colorizer(),
startExpirationTimeout = {}
startExpirationTimeout = {},
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) },
displayDialogFragment = {}
)
if (springboardViewModel.hasWallpaper.value) {
@@ -296,5 +299,17 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onShowSafetyTips(forGroup: Boolean) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onReportSpamLearnMoreClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onMessageRequestAcceptOptionsClicked() {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
}
}
@@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:usesCleartextTraffic="true"
tools:replace="android:usesCleartextTraffic"
+57 -81
View File
@@ -22,17 +22,10 @@
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
<uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.BROADCAST_WAP_PUSH"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.WRITE_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
@@ -50,16 +43,10 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.READ_CALL_STATE"/>
<!-- For sending/receiving events -->
<uses-permission android:name="android.permission.WRITE_CALENDAR"/>
<uses-permission android:name="android.permission.READ_CALENDAR"/>
<!-- Normal -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
@@ -76,17 +63,14 @@
<uses-permission android:name="android.permission.INSTALL_SHORTCUT"/>
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<!-- For fixing MMS -->
<!-- For device transfer -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<!-- Set image as wallpaper -->
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BROADCAST_STICKY" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.DISABLE_KEYGUARD" />
<uses-permission android:name="android.permission.RAISED_THREAD_PRIORITY" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
@@ -160,12 +144,6 @@
android:value=".MainActivity" />
</activity>
<activity android:name=".PromptMmsActivity"
android:label="Configure MMS Settings"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".DeviceProvisioningActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="true">
@@ -185,10 +163,6 @@
</intent-filter>
</activity>
<activity android:name=".preferences.MmsPreferencesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false" />
<activity android:name=".sharing.interstitial.ShareInterstitialActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:windowSoftInputMode="adjustResize"
@@ -693,7 +667,12 @@
<activity android:name=".NewConversationActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateAlwaysVisible"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".recipients.ui.findby.FindByActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
@@ -725,6 +704,7 @@
android:theme="@style/TextSecure.DarkNoActionBar"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:exported="false"/>
@@ -768,6 +748,13 @@
android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/>
<activity
android:name=".backup.v2.ui.MessageBackupsFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="adjustResize" />
<activity
android:name=".stories.settings.StorySettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -939,12 +926,12 @@
</activity>
<activity android:name="org.thoughtcrime.securesms.webrtc.VoiceCallShare"
android:exported="true"
android:excludeFromRecents="true"
android:permission="android.permission.CALL_PHONE"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
android:exported="true"
android:excludeFromRecents="true"
android:permission="android.permission.CALL_PHONE"
android:theme="@style/NoAnimation.Theme.BlackScreen"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
@@ -975,9 +962,8 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".profiles.username.AddAUsernameActivity"
android:theme="@style/Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateVisible|adjustResize"
<activity android:name=".backup.v2.ui.MessageBackupsTestRestoreActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:exported="false"/>
<activity android:name=".profiles.manage.EditProfileActivity"
@@ -985,6 +971,11 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".nicknames.NicknameActivity"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity
android:name=".payments.preferences.PaymentsActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
@@ -1072,13 +1063,6 @@
android:launchMode="singleTask"
android:exported="false"/>
<activity android:name=".megaphone.SmsExportMegaphoneActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
android:launchMode="singleTask"
android:exported="false"/>
<activity android:name=".ratelimit.RecaptchaProofActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
@@ -1095,15 +1079,18 @@
android:theme="@style/Theme.Signal.WallpaperCropper"
android:exported="false"/>
<activity android:name=".reactions.edit.EditReactionsActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrImageSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/TextSecure.DarkNoActionBar"
android:exported="false"/>
<activity android:name=".components.settings.app.usernamelinks.main.UsernameQrScannerActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:exported="false"/>
<activity android:name=".exporter.flow.SmsExportActivity"
<activity android:name=".reactions.edit.EditReactionsActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
@@ -1112,12 +1099,6 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<service
android:enabled="true"
android:name=".exporter.SignalSmsExportService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service
android:enabled="true"
android:name=".service.webrtc.WebRtcCallService"
@@ -1164,19 +1145,6 @@
</intent-filter>
</receiver>
<service android:name=".service.QuickResponseService"
android:permission="android.permission.SEND_RESPOND_VIA_MESSAGE"
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.RESPOND_VIA_MESSAGE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="sms" />
<data android:scheme="smsto" />
<data android:scheme="mms" />
<data android:scheme="mmsto" />
</intent-filter>
</service>
<service android:name=".service.AccountAuthenticatorService" android:exported="true">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
@@ -1214,13 +1182,6 @@
</intent-filter>
</service>
<receiver android:name=".service.SmsDeliveryListener"
android:exported="true">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.services.MESSAGE_SENT"/>
</intent-filter>
</receiver>
<receiver android:name=".notifications.MarkReadReceiver"
android:enabled="true"
android:exported="false">
@@ -1280,11 +1241,6 @@
android:exported="false"
android:grantUriPermissions="true" />
<provider android:name=".providers.MmsBodyProvider"
android:grantUriPermissions="true"
android:exported="false"
android:authorities="${applicationId}.mms" />
<provider android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
@@ -1332,6 +1288,12 @@
</intent-filter>
</receiver>
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
@@ -1396,6 +1358,20 @@
</intent-filter>
</receiver>
<service
android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService"
android:exported="false"
android:foregroundServiceType="camera|microphone" />
<receiver android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallServiceReceiver" android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.DENY"/>
</intent-filter>
<intent-filter>
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.HANGUP"/>
</intent-filter>
</receiver>
<uses-library android:name="org.apache.http.legacy" android:required="false"/>
</application>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 147 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 100 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -24,6 +24,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.multidex.MultiDexApplication;
import com.bumptech.glide.Glide;
import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.ConscryptSignal;
@@ -59,6 +60,7 @@ import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
@@ -74,7 +76,6 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.providers.BlobProvider;
@@ -82,6 +83,7 @@ import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
import org.thoughtcrime.securesms.service.AnalyzeDatabaseAlarmListener;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
@@ -178,7 +180,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete())
.addNonBlocking(() -> GlideApp.get(this))
.addNonBlocking(() -> Glide.get(this))
.addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager)
@@ -190,7 +192,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addNonBlocking(this::initializeCleanup)
.addNonBlocking(this::initializeGlideCodecs)
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
.addNonBlocking(() -> ApplicationDependencies.getJobManager().beginJobLoop())
.addNonBlocking(this::beginJobLoop)
.addNonBlocking(EmojiSource::refresh)
.addNonBlocking(() -> ApplicationDependencies.getGiphyMp4Cache().onAppStart(this))
.addNonBlocking(this::ensureProfileUploaded)
@@ -198,7 +200,6 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(() -> ApplicationDependencies.getDeletedCallEventManager().scheduleIfNecessary())
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
.addPostRender(this::initializeTrimThreadsByDateManager)
.addPostRender(RefreshSvrCredentialsJob::enqueueIfNecessary)
.addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
@@ -216,6 +217,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
.addPostRender(GroupRingCleanupJob::enqueue)
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -274,7 +276,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
public void checkBuildExpiration() {
if (Util.getTimeUntilBuildExpiry() <= 0 && !SignalStore.misc().isClientDeprecated()) {
Log.w(TAG, "Build expired!");
SignalStore.misc().markClientDeprecated();
SignalStore.misc().setClientDeprecated(true);
}
}
@@ -418,6 +420,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
LocalBackupListener.schedule(this);
RotateSenderCertificateListener.schedule(this);
RoutineMessageFetchReceiver.startOrUpdateAlarm(this);
AnalyzeDatabaseAlarmListener.schedule(this);
if (BuildConfig.MANAGES_APP_UPDATES) {
ApkUpdateRefreshListener.schedule(this);
@@ -463,6 +466,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
}
@VisibleForTesting
protected void beginJobLoop() {
ApplicationDependencies.getJobManager().beginJobLoop();
}
@WorkerThread
private void initializeBlobProvider() {
BlobProvider.getInstance().initialize(this);
@@ -18,6 +18,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityOptionsCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException;
@@ -32,7 +33,6 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FullscreenHelper;
@@ -96,7 +96,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
Resources resources = this.getResources();
GlideApp.with(this)
Glide.with(this)
.asBitmap()
.load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this))
@@ -8,6 +8,8 @@ import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer;
import com.bumptech.glide.RequestManager;
import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact;
@@ -26,7 +28,6 @@ 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.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator;
@@ -41,7 +42,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
@NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests,
@NonNull RequestManager requestManager,
@NonNull Locale locale,
@NonNull Set<MultiselectPart> batchSelected,
@NonNull Recipient recipients,
@@ -122,5 +123,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey);
void onShowSafetyTips(boolean forGroup);
void onReportSpamLearnMoreClicked();
void onMessageRequestAcceptOptionsClicked();
}
}
@@ -3,9 +3,10 @@ package org.thoughtcrime.securesms;
import androidx.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.Locale;
import java.util.Set;
@@ -14,7 +15,7 @@ public interface BindableConversationListItem extends Unbindable {
void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests, @NonNull Locale locale,
@NonNull RequestManager requestManager, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations);
@@ -11,16 +11,36 @@ import androidx.lifecycle.Lifecycle;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.GroupRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.push.ServiceId;
import java.util.List;
import java.util.Optional;
import okio.ByteString;
/**
* This should be used whenever we want to prompt the user to block/unblock a recipient.
*/
public final class BlockUnblockDialog {
private BlockUnblockDialog() { }
private BlockUnblockDialog() {}
public static void showReportSpamFor(@NonNull Context context,
@NonNull Lifecycle lifecycle,
@NonNull Recipient recipient,
@NonNull Runnable onReportSpam,
@Nullable Runnable onBlockAndReportSpam)
{
SimpleTask.run(lifecycle,
() -> buildReportSpamFor(context, recipient, onReportSpam, onBlockAndReportSpam),
AlertDialog.Builder::show);
}
public static void showBlockFor(@NonNull Context context,
@NonNull Lifecycle lifecycle,
@@ -137,4 +157,37 @@ public final class BlockUnblockDialog {
return builder;
}
@WorkerThread
private static AlertDialog.Builder buildReportSpamFor(@NonNull Context context,
@NonNull Recipient recipient,
@NonNull Runnable onReportSpam,
@Nullable Runnable onBlockAndReportSpam)
{
recipient = recipient.resolve();
AlertDialog.Builder builder = new MaterialAlertDialogBuilder(context)
.setTitle(R.string.BlockUnblockDialog_report_spam_title)
.setPositiveButton(R.string.BlockUnblockDialog_report_spam, (d, w) -> onReportSpam.run());
if (onBlockAndReportSpam != null) {
builder.setNeutralButton(android.R.string.cancel, null)
.setNegativeButton(R.string.BlockUnblockDialog_report_spam_and_block, (d, w) -> onBlockAndReportSpam.run());
} else {
builder.setNegativeButton(android.R.string.cancel, null);
}
if (recipient.isGroup()) {
Recipient adder = SignalDatabase.groups().getGroupInviter(recipient.requireGroupId());
if (adder != null) {
builder.setMessage(context.getString(R.string.BlockUnblockDialog_report_spam_group_named_adder, adder.getDisplayName(context)));
} else {
builder.setMessage(R.string.BlockUnblockDialog_report_spam_group_unknown_adder);
}
} else {
builder.setMessage(R.string.BlockUnblockDialog_report_spam_description);
}
return builder;
}
}
@@ -24,14 +24,17 @@ import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DisplayMetricsUtil;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -70,8 +73,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
@Override
protected void onCreate(Bundle icicle, boolean ready) {
if (!getIntent().hasExtra(ContactSelectionListFragment.DISPLAY_MODE)) {
boolean includeSms = Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
int displayMode = includeSms ? ContactSelectionDisplayMode.FLAG_ALL : ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
int displayMode = ContactSelectionDisplayMode.FLAG_PUSH | ContactSelectionDisplayMode.FLAG_ACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_INACTIVE_GROUPS | ContactSelectionDisplayMode.FLAG_SELF;
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayMode);
}
@@ -99,6 +101,10 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
private void initializeContactFilterView() {
this.contactFilterView = findViewById(R.id.contact_filter_edit_text);
if (getResources().getDisplayMetrics().heightPixels >= DimensionUnit.DP.toPixels(600)) {
this.contactFilterView.focusAndShowKeyboard();
}
}
private void initializeToolbar() {
@@ -27,6 +27,8 @@ class ContactSelectionListAdapter(
registerFactory(RefreshContactsModel::class.java, LayoutFactory({ RefreshContactsViewHolder(it, onClickCallbacks::onRefreshContactsClicked) }, R.layout.contact_selection_refresh_action_item))
registerFactory(MoreHeaderModel::class.java, LayoutFactory({ MoreHeaderViewHolder(it) }, R.layout.contact_search_section_header))
registerFactory(EmptyModel::class.java, LayoutFactory({ EmptyViewHolder(it) }, R.layout.contact_selection_empty_state))
registerFactory(FindByUsernameModel::class.java, LayoutFactory({ FindByUsernameViewHolder(it, onClickCallbacks::onFindByUsernameClicked) }, R.layout.contact_selection_find_by_username_item))
registerFactory(FindByPhoneNumberModel::class.java, LayoutFactory({ FindByPhoneNumberViewHolder(it, onClickCallbacks::onFindByPhoneNumberClicked) }, R.layout.contact_selection_find_by_phone_number_item))
}
class NewGroupModel : MappingModel<NewGroupModel> {
@@ -44,6 +46,16 @@ class ContactSelectionListAdapter(
override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true
}
class FindByUsernameModel : MappingModel<FindByUsernameModel> {
override fun areItemsTheSame(newItem: FindByUsernameModel): Boolean = true
override fun areContentsTheSame(newItem: FindByUsernameModel): Boolean = true
}
class FindByPhoneNumberModel : MappingModel<FindByPhoneNumberModel> {
override fun areItemsTheSame(newItem: FindByPhoneNumberModel): Boolean = true
override fun areContentsTheSame(newItem: FindByPhoneNumberModel): Boolean = true
}
class MoreHeaderModel : MappingModel<MoreHeaderModel> {
override fun areItemsTheSame(newItem: MoreHeaderModel): Boolean = true
@@ -92,13 +104,33 @@ class ContactSelectionListAdapter(
}
}
private class FindByPhoneNumberViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<FindByPhoneNumberModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: FindByPhoneNumberModel) = Unit
}
private class FindByUsernameViewHolder(itemView: View, onClickListener: () -> Unit) : MappingViewHolder<FindByUsernameModel>(itemView) {
init {
itemView.setOnClickListener { onClickListener() }
}
override fun bind(model: FindByUsernameModel) = Unit
}
class ArbitraryRepository : org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository {
enum class ArbitraryRow(val code: String) {
NEW_GROUP("new-group"),
INVITE_TO_SIGNAL("invite-to-signal"),
MORE_HEADING("more-heading"),
REFRESH_CONTACTS("refresh-contacts");
REFRESH_CONTACTS("refresh-contacts"),
FIND_BY_USERNAME("find-by-username"),
FIND_BY_PHONE_NUMBER("find-by-phone-number");
companion object {
fun fromCode(code: String) = values().first { it.code == code }
@@ -120,6 +152,8 @@ class ContactSelectionListAdapter(
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
ArbitraryRow.FIND_BY_PHONE_NUMBER -> FindByPhoneNumberModel()
ArbitraryRow.FIND_BY_USERNAME -> FindByUsernameModel()
}
}
}
@@ -128,5 +162,7 @@ class ContactSelectionListAdapter(
fun onNewGroupClicked()
fun onInviteToSignalClicked()
fun onRefreshContactsClicked()
fun onFindByPhoneNumberClicked()
fun onFindByUsernameClicked()
}
}
@@ -76,6 +76,7 @@ import org.thoughtcrime.securesms.profiles.manage.UsernameRepository.UsernameAci
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -141,6 +142,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
private ContactSearchMediator contactSearchMediator;
@Nullable private NewConversationCallback newConversationCallback;
@Nullable private FindByCallback findByCallback;
@Nullable private NewCallCallback newCallCallback;
@Nullable private ScrollCallback scrollCallback;
@Nullable private OnItemLongClickListener onItemLongClickListener;
@@ -161,6 +163,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
newConversationCallback = (NewConversationCallback) context;
}
if (context instanceof FindByCallback) {
findByCallback = (FindByCallback) context;
}
if (context instanceof NewCallCallback) {
newCallCallback = (NewCallCallback) context;
}
@@ -379,6 +385,16 @@ public final class ContactSelectionListFragment extends LoggingFragment {
newConversationCallback.onNewGroup(false);
}
@Override
public void onFindByPhoneNumberClicked() {
findByCallback.onFindByPhoneNumber();
}
@Override
public void onFindByUsernameClicked() {
findByCallback.onFindByUsername();
}
@Override
public void onInviteToSignalClicked() {
if (newConversationCallback != null) {
@@ -660,6 +676,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
}
}
public void addRecipientToSelectionIfAble(@NonNull RecipientId recipientId) {
listClickListener.onItemClick(new ContactSearchKey.RecipientSearchKey(recipientId, false));
}
private class ListClickListener {
public void onItemClick(ContactSearchKey contact) {
boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey;
@@ -870,10 +890,15 @@ public final class ContactSelectionListFragment extends LoggingFragment {
return ContactSearchConfiguration.build(builder -> {
builder.setQuery(contactSearchState.getQuery());
if (newConversationCallback != null) {
if (newConversationCallback != null && !hasQuery) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
}
if (findByCallback != null && !hasQuery) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_USERNAME.getCode());
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_PHONE_NUMBER.getCode());
}
if (transportType != null) {
if (!hasQuery && includeRecents) {
builder.addSection(new ContactSearchConfiguration.Section.Recents(
@@ -888,12 +913,14 @@ public final class ContactSelectionListFragment extends LoggingFragment {
));
}
boolean hideHeader = newCallCallback != null || (newConversationCallback != null && !hasQuery);
builder.addSection(new ContactSearchConfiguration.Section.Individuals(
includeSelf,
transportType,
newCallCallback == null,
!hideHeader,
null,
!hideLetterHeaders()
!hideLetterHeaders(),
newConversationCallback != null ? ContactSearchSortOrder.RECENCY : ContactSearchSortOrder.NATURAL
));
}
@@ -919,7 +946,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
builder.username(newRowMode);
}
if (newCallCallback != null || newConversationCallback != null) {
if ((newCallCallback != null || newConversationCallback != null) && !hasQuery) {
addMoreSection(builder);
builder.withEmptyState(emptyBuilder -> {
emptyBuilder.addSection(ContactSearchConfiguration.Section.Empty.INSTANCE);
@@ -1011,6 +1038,12 @@ public final class ContactSelectionListFragment extends LoggingFragment {
void onNewGroup(boolean forceV1);
}
public interface FindByCallback {
void onFindByUsername();
void onFindByPhoneNumber();
}
public interface NewCallCallback {
void onInvite();
}
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms;
import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
@@ -28,6 +29,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.qr.kitkat.ScanListener;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.signal.core.util.Base64;
@@ -48,6 +50,8 @@ public class DeviceActivity extends PassphraseRequiredActivity
private static final String TAG = Log.tag(DeviceActivity.class);
private static final String EXTRA_DIRECT_TO_SCANNER = "add";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
@@ -56,6 +60,13 @@ public class DeviceActivity extends PassphraseRequiredActivity
private DeviceLinkFragment deviceLinkFragment;
private MenuItem cameraSwitchItem = null;
public static Intent getIntentForScanner(Context context) {
Intent intent = new Intent(context, DeviceActivity.class);
intent.putExtra(EXTRA_DIRECT_TO_SCANNER, true);
return intent;
}
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
@@ -79,7 +90,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
this.deviceListFragment.setAddDeviceButtonListener(this);
this.deviceAddFragment.setScanListener(this);
if (getIntent().getBooleanExtra("add", false)) {
if (getIntent().getBooleanExtra(EXTRA_DIRECT_TO_SCANNER, false)) {
initFragment(R.id.fragment_container, deviceAddFragment, dynamicLanguage.getCurrentLocale());
} else {
initFragment(R.id.fragment_container, deviceListFragment, dynamicLanguage.getCurrentLocale());
@@ -221,6 +232,8 @@ public class DeviceActivity extends PassphraseRequiredActivity
protected void onPostExecute(Integer result) {
super.onPostExecute(result);
LinkedDeviceInactiveCheckJob.enqueue();
Context context = DeviceActivity.this;
switch (result) {
@@ -27,6 +27,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.loaders.DeviceListLoader;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicelist.Device;
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
@@ -166,6 +167,7 @@ public class DeviceListFragment extends ListFragment
super.onPostExecute(result);
if (result) {
getLoaderManager().restartLoader(0, null, DeviceListFragment.this);
LinkedDeviceInactiveCheckJob.enqueue();
} else {
Toast.makeText(getActivity(), R.string.DeviceListActivity_network_failed, Toast.LENGTH_LONG).show();
}
@@ -26,9 +26,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
.setPositiveButton(R.string.DeviceProvisioningActivity_continue, (dialog1, which) -> {
Intent intent = new Intent(DeviceProvisioningActivity.this, DeviceActivity.class);
intent.putExtra("add", true);
startActivity(intent);
startActivity(DeviceActivity.getIntentForScanner(this));
finish();
})
.setNegativeButton(android.R.string.cancel, (dialog12, which) -> {
@@ -38,7 +36,6 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
.setOnDismissListener(dialog13 -> finish())
.create();
dialog.setIcon(getResources().getDrawable(R.drawable.icon_dialog));
dialog.show();
}
}
@@ -54,7 +54,6 @@ public final class GroupMembersDialog {
}
private void contactClick(@NonNull Recipient recipient) {
RecipientBottomSheetDialogFragment.create(recipient.getId(), groupRecipient.requireGroupId())
.show(fragmentActivity.getSupportFragmentManager(), "BOTTOM");
RecipientBottomSheetDialogFragment.show(fragmentActivity.getSupportFragmentManager(), recipient.getId(), groupRecipient.requireGroupId());
}
}
@@ -17,17 +17,16 @@ import android.widget.Toast;
import androidx.annotation.AnimRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
@@ -38,7 +37,6 @@ import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
@@ -121,14 +119,9 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
smsSendButton.setOnClickListener(new SmsSendClickListener());
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
if (Util.isDefaultSmsProvider(this) && SignalStore.misc().getSmsExportPhase().isSmsSupported()) {
shareButton.setOnClickListener(new ShareClickListener());
smsButton.setOnClickListener(new SmsClickListener());
} else {
smsButton.setVisibility(View.GONE);
shareText.setText(R.string.InviteActivity_share);
shareButton.setOnClickListener(new ShareClickListener());
}
smsButton.setVisibility(View.GONE);
shareText.setText(R.string.InviteActivity_share);
shareButton.setOnClickListener(new ShareClickListener());
}
private Animation loadAnimation(@AnimRes int animResId) {
@@ -202,13 +195,6 @@ public class InviteActivity extends PassphraseRequiredActivity implements Contac
}
}
private class SmsClickListener implements OnClickListener {
@Override
public void onClick(View v) {
ViewUtil.animateIn(smsSendFrame, slideInAnimation);
}
}
private class SmsCancelClickListener implements OnClickListener {
@Override
public void onClick(View v) {
@@ -17,7 +17,6 @@ import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.logging.Log;
import org.signal.donations.StripeApi;
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment;
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment;
@@ -153,7 +152,7 @@ public class MainActivity extends PassphraseRequiredActivity implements VoiceNot
.setMessage(R.string.OldDeviceTransferLockedDialog__your_signal_account_has_been_transferred_to_your_new_device)
.setPositiveButton(R.string.OldDeviceTransferLockedDialog__done, (d, w) -> OldDeviceExitActivity.exit(this))
.setNegativeButton(R.string.OldDeviceTransferLockedDialog__cancel_and_activate_this_device, (d, w) -> {
SignalStore.misc().clearOldDeviceTransferLocked();
SignalStore.misc().setOldDeviceTransferLocked(false);
DeviceTransferBlockingInterceptor.getInstance().unblockNetwork();
})
.setCancelable(false)
@@ -50,6 +50,9 @@ import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientRepository;
import org.thoughtcrime.securesms.recipients.ui.findby.FindByActivity;
import org.thoughtcrime.securesms.recipients.ui.findby.FindByMode;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
@@ -70,14 +73,16 @@ import io.reactivex.rxjava3.disposables.Disposable;
* @author Moxie Marlinspike
*/
public class NewConversationActivity extends ContactSelectionActivity
implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener
implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener, ContactSelectionListFragment.FindByCallback
{
@SuppressWarnings("unused")
private static final String TAG = Log.tag(NewConversationActivity.class);
private ContactsManagementViewModel viewModel;
private ActivityResultLauncher<Intent> contactLauncher;
private ContactsManagementViewModel viewModel;
private ActivityResultLauncher<Intent> contactLauncher;
private ActivityResultLauncher<Intent> createGroupLauncher;
private ActivityResultLauncher<FindByMode> findByLauncher;
private final LifecycleDisposable disposables = new LifecycleDisposable();
@@ -99,13 +104,23 @@ public class NewConversationActivity extends ContactSelectionActivity
}
});
findByLauncher = registerForActivityResult(new FindByActivity.Contract(), result -> {
if (result != null) {
launch(result);
}
});
createGroupLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == RESULT_OK) {
finish();
}
});
viewModel = new ViewModelProvider(this, factory).get(ContactsManagementViewModel.class);
}
@Override
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Consumer<Boolean> callback) {
boolean smsSupported = SignalStore.misc().getSmsExportPhase().allowSmsFeatures();
if (recipientId.isPresent()) {
launch(Recipient.resolved(recipientId.get()));
} else {
@@ -116,33 +131,19 @@ public class NewConversationActivity extends ContactSelectionActivity
AlertDialog progress = SimpleProgressDialog.show(this);
SimpleTask.run(getLifecycle(), () -> {
Recipient resolved = Recipient.external(this, number);
if (!resolved.isRegistered() || !resolved.hasServiceId()) {
Log.i(TAG, "[onContactSelected] Not registered or no UUID. Doing a directory refresh.");
try {
ContactDiscovery.refresh(this, resolved, false, TimeUnit.SECONDS.toMillis(10));
resolved = Recipient.resolved(resolved.getId());
} catch (IOException e) {
Log.w(TAG, "[onContactSelected] Failed to refresh directory for new contact.");
return null;
}
}
return resolved;
}, resolved -> {
SimpleTask.run(getLifecycle(), () -> RecipientRepository.lookupNewE164(this, number), result -> {
progress.dismiss();
if (resolved != null) {
if (smsSupported || resolved.isRegistered() && resolved.hasServiceId()) {
if (result instanceof RecipientRepository.LookupResult.Success) {
Recipient resolved = Recipient.resolved(((RecipientRepository.LookupResult.Success) result).getRecipientId());
if (resolved.isRegistered() && resolved.getHasServiceId()) {
launch(resolved);
} else {
new MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, resolved.getDisplayName(this)))
.setPositiveButton(android.R.string.ok, null)
.show();
}
} else if (result instanceof RecipientRepository.LookupResult.NotFound || result instanceof RecipientRepository.LookupResult.InvalidEntry) {
new MaterialAlertDialogBuilder(this)
.setMessage(getString(R.string.NewConversationActivity__s_is_not_a_signal_user, number))
.setPositiveButton(android.R.string.ok, null)
.show();
} else {
new MaterialAlertDialogBuilder(this)
.setMessage(R.string.NetworkFailure__network_error_check_your_connection_and_try_again)
@@ -150,8 +151,6 @@ public class NewConversationActivity extends ContactSelectionActivity
.show();
}
});
} else if (smsSupported) {
launch(Recipient.external(this, number));
}
}
@@ -163,7 +162,12 @@ public class NewConversationActivity extends ContactSelectionActivity
}
private void launch(Recipient recipient) {
Disposable disposable = ConversationIntents.createBuilder(this, recipient.getId(), -1L)
launch(recipient.getId());
}
private void launch(RecipientId recipientId) {
Disposable disposable = ConversationIntents.createBuilder(this, recipientId, -1L)
.map(builder -> builder
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
.withDataUri(getIntent().getData())
@@ -206,7 +210,7 @@ public class NewConversationActivity extends ContactSelectionActivity
}
private void handleCreateGroup() {
startActivity(CreateGroupActivity.newIntent(this));
createGroupLauncher.launch(CreateGroupActivity.newIntent(this));
}
private void handleInvite() {
@@ -231,7 +235,17 @@ public class NewConversationActivity extends ContactSelectionActivity
@Override
public void onNewGroup(boolean forceV1) {
handleCreateGroup();
finish();
// finish();
}
@Override
public void onFindByUsername() {
findByLauncher.launch(FindByMode.USERNAME);
}
@Override
public void onFindByPhoneNumber() {
findByLauncher.launch(FindByMode.PHONE_NUMBER);
}
@Override
@@ -286,7 +300,7 @@ public class NewConversationActivity extends ContactSelectionActivity
return null;
}
if (recipient.isRegistered() || (SignalStore.misc().getSmsExportPhase().allowSmsFeatures())) {
if (recipient.isRegistered()) {
return new ActionItem(
R.drawable.ic_phone_right_24,
getString(R.string.NewConversationActivity__audio_call),
@@ -374,7 +374,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
Log.i(TAG, "onAuthenticationSucceeded");
fingerprintPrompt.setImageResource(R.drawable.ic_check_white_48dp);
fingerprintPrompt.setImageResource(R.drawable.symbol_check_white_48);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.green_500), PorterDuff.Mode.SRC_IN);
fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() {
@Override
@@ -388,7 +388,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
public void onAuthenticationFailed() {
Log.w(TAG, "onAuthenticationFailed()");
fingerprintPrompt.setImageResource(R.drawable.ic_close_white_48dp);
fingerprintPrompt.setImageResource(R.drawable.symbol_x_white_48);
fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
TranslateAnimation shake = new TranslateAnimation(0, 30, 0, 0);
@@ -1,31 +0,0 @@
package org.thoughtcrime.securesms;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import org.thoughtcrime.securesms.preferences.MmsPreferencesActivity;
public class PromptMmsActivity extends PassphraseRequiredActivity {
@Override
protected void onCreate(Bundle bundle, boolean ready) {
setContentView(R.layout.prompt_apn_activity);
initializeResources();
}
private void initializeResources() {
Button okButton = findViewById(R.id.ok_button);
Button cancelButton = findViewById(R.id.cancel_button);
okButton.setOnClickListener(v -> {
Intent intent = new Intent(PromptMmsActivity.this, MmsPreferencesActivity.class);
intent.putExtras(PromptMmsActivity.this.getIntent().getExtras());
startActivity(intent);
finish();
});
cancelButton.setOnClickListener(v -> finish());
}
}
@@ -137,6 +137,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
public static final String EXTRA_ENABLE_VIDEO_IF_AVAILABLE = WebRtcCallActivity.class.getCanonicalName() + ".ENABLE_VIDEO_IF_AVAILABLE";
public static final String EXTRA_STARTED_FROM_FULLSCREEN = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_FULLSCREEN";
public static final String EXTRA_STARTED_FROM_CALL_LINK = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
public static final String EXTRA_LAUNCH_IN_PIP = WebRtcCallActivity.class.getCanonicalName() + ".STARTED_FROM_CALL_LINK";
private CallParticipantsListUpdatePopupWindow participantUpdateWindow;
private CallStateUpdatePopupWindow callStateUpdatePopupWindow;
@@ -159,6 +160,9 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private LifecycleDisposable lifecycleDisposable;
private long lastCallLinkDisconnectDialogShowTime;
private ControlsAndInfoController controlsAndInfo;
private boolean enterPipOnResume;
private long lastProcessedIntentTimestamp;
private WebRtcViewModel previousEvent = null;
private Disposable ephemeralStateDisposable = Disposable.empty();
@@ -264,6 +268,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
}, TimeUnit.SECONDS.toMillis(1));
}
if (enterPipOnResume) {
enterPipOnResume = false;
enterPipModeIfPossible();
}
}
@Override
@@ -299,10 +308,16 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
requestNewSizesThrottle.clear();
}
ApplicationDependencies.getSignalCallManager().setEnableVideo(false);
if (!viewModel.isCallStarting()) {
CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot();
if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) {
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
if (state != null) {
if (state.getCallState().isPreJoinOrNetworkUnavailable()) {
ApplicationDependencies.getSignalCallManager().cancelPreJoin();
} else if (state.getCallState().getInOngoingCall() && isInPipMode()) {
ApplicationDependencies.getSignalCallManager().relaunchPipOnForeground();
}
}
}
}
@@ -361,6 +376,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
Log.d(TAG, "Intent: Action: " + intent.getAction());
Log.d(TAG, "Intent: EXTRA_STARTED_FROM_FULLSCREEN: " + intent.getBooleanExtra(EXTRA_STARTED_FROM_FULLSCREEN, false));
Log.d(TAG, "Intent: EXTRA_ENABLE_VIDEO_IF_AVAILABLE: " + intent.getBooleanExtra(EXTRA_ENABLE_VIDEO_IF_AVAILABLE, false));
Log.d(TAG, "Intent: EXTRA_LAUNCH_IN_PIP: " + intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false));
}
private void processIntent(@NonNull Intent intent) {
@@ -373,6 +389,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
} else if (END_CALL_ACTION.equals(intent.getAction())) {
handleEndCall();
}
if (System.currentTimeMillis() - lastProcessedIntentTimestamp > TimeUnit.SECONDS.toMillis(1)) {
enterPipOnResume = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false);
}
lastProcessedIntentTimestamp = System.currentTimeMillis();
}
private void initializePendingParticipantFragmentListener() {
@@ -765,7 +787,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
new MaterialAlertDialogBuilder(this)
.setTitle(R.string.RedPhone_number_not_registered)
.setIcon(R.drawable.ic_warning)
.setIcon(R.drawable.symbol_error_triangle_fill_24)
.setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
.setCancelable(true)
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
@@ -851,8 +873,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
}
private boolean isSystemPipEnabledAndAvailable() {
return Build.VERSION.SDK_INT >= 26 &&
getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
return Build.VERSION.SDK_INT >= 26 && getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
}
private void delayedFinish() {
@@ -865,7 +886,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event);
Log.i(TAG, "Got message from service: " + event.describeDifference(previousEvent));
previousEvent = event;
viewModel.setRecipient(event.getRecipient());
callScreen.setRecipient(event.getRecipient());
@@ -3,13 +3,14 @@ package org.thoughtcrime.securesms.attachments
import android.os.Parcelable
import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.parcelize.Parcelize
import org.signal.core.util.DatabaseId
@Parcelize
data class AttachmentId(
@JsonProperty("rowId")
@JvmField
val id: Long
) : Parcelable {
) : Parcelable, DatabaseId {
val isValid: Boolean
get() = id >= 0
@@ -17,4 +18,8 @@ data class AttachmentId(
override fun toString(): String {
return "AttachmentId::$id"
}
override fun serialize(): String {
return id.toString()
}
}
@@ -22,6 +22,9 @@ class DatabaseAttachment : Attachment {
@JvmField
val hasData: Boolean
@JvmField
val dataHash: String?
private val hasThumbnail: Boolean
val displayOrder: Int
@@ -53,7 +56,8 @@ class DatabaseAttachment : Attachment {
audioHash: AudioHash?,
transformProperties: TransformProperties?,
displayOrder: Int,
uploadTimestamp: Long
uploadTimestamp: Long,
dataHash: String?
) : super(
contentType = contentType!!,
transferState = transferProgress,
@@ -81,6 +85,7 @@ class DatabaseAttachment : Attachment {
this.attachmentId = attachmentId
this.mmsId = mmsId
this.hasData = hasData
this.dataHash = dataHash
this.hasThumbnail = hasThumbnail
this.displayOrder = displayOrder
}
@@ -88,6 +93,7 @@ class DatabaseAttachment : Attachment {
constructor(parcel: Parcel) : super(parcel) {
attachmentId = ParcelCompat.readParcelable(parcel, AttachmentId::class.java.classLoader, AttachmentId::class.java)!!
hasData = ParcelUtil.readBoolean(parcel)
dataHash = parcel.readString()
hasThumbnail = ParcelUtil.readBoolean(parcel)
mmsId = parcel.readLong()
displayOrder = parcel.readInt()
@@ -97,6 +103,7 @@ class DatabaseAttachment : Attachment {
super.writeToParcel(dest, flags)
dest.writeParcelable(attachmentId, 0)
ParcelUtil.writeBoolean(dest, hasData)
dest.writeString(dataHash)
ParcelUtil.writeBoolean(dest, hasThumbnail)
dest.writeLong(mmsId)
dest.writeInt(displayOrder)
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import androidx.annotation.VisibleForTesting
import org.signal.core.util.Base64.encodeWithPadding
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
@@ -14,7 +15,8 @@ import org.whispersystems.signalservice.internal.push.DataMessage
import java.util.Optional
class PointerAttachment : Attachment {
private constructor(
@VisibleForTesting
constructor(
contentType: String,
transferState: Int,
size: Long,
@@ -12,7 +12,7 @@ import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput;
import org.thoughtcrime.securesms.media.MediaInput;
import org.thoughtcrime.securesms.video.interfaces.MediaInput;
import java.io.IOException;
import java.nio.ByteBuffer;
@@ -8,12 +8,12 @@ import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.setPadding
import com.airbnb.lottie.SimpleColorFilter
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
@@ -132,12 +132,12 @@ object AvatarPickerItem {
}
is Avatar.Photo -> {
textView.visible = false
GlideApp.with(imageView).load(DecryptableStreamUriLoader.DecryptableUri(model.avatar.uri)).into(imageView)
Glide.with(imageView).load(DecryptableStreamUriLoader.DecryptableUri(model.avatar.uri)).into(imageView)
}
is Avatar.Resource -> {
imageView.setPadding((imageView.width * 0.2).toInt())
textView.visible = false
GlideApp.with(imageView).clear(imageView)
Glide.with(imageView).clear(imageView)
imageView.setImageResource(model.avatar.resourceId)
imageView.colorFilter = SimpleColorFilter(model.avatar.color.foregroundColor)
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
@@ -5,10 +5,10 @@ import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import androidx.core.content.res.use
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.visible
@@ -76,7 +76,7 @@ class AvatarView @JvmOverloads constructor(
/**
* Displays Note-to-Self
*/
fun displayChatAvatar(requestManager: GlideRequests, recipient: Recipient, isQuickContactEnabled: Boolean) {
fun displayChatAvatar(requestManager: RequestManager, recipient: Recipient, isQuickContactEnabled: Boolean) {
avatar.setAvatar(requestManager, recipient, isQuickContactEnabled)
}
@@ -160,7 +160,7 @@ public class FullBackupExporter extends FullBackupBase {
for (String table : tables) {
throwIfCanceled(cancellationSignal);
if (table.equals(MessageTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isNonExpiringMessage(cursor), null, count, estimatedCount, cancellationSignal);
count = exportTable(table, input, outputStream, cursor -> isNonExpiringMessage(input, cursor), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(ReactionTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, CursorUtil.requireLong(cursor, ReactionTable.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(MentionTable.TABLE_NAME)) {
@@ -579,25 +579,34 @@ public class FullBackupExporter extends FullBackupBase {
return count;
}
private static boolean isNonExpiringMessage(@NonNull Cursor cursor) {
long expiresIn = CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN);
boolean viewOnce = CursorUtil.requireBoolean(cursor, MessageTable.VIEW_ONCE);
private static boolean isNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull Cursor cursor) {
long id = CursorUtil.requireLong(cursor, MessageTable.ID);
long expireStarted = CursorUtil.requireLong(cursor, MessageTable.EXPIRE_STARTED);
long expiresIn = CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN);
long latestRevisionId = CursorUtil.requireLong(cursor, MessageTable.LATEST_REVISION_ID);
if (expiresIn == 0 && !viewOnce) {
return true;
long expiresAt = expireStarted + expiresIn;
long timeRemaining = expiresAt - System.currentTimeMillis();
if (latestRevisionId > 0 && latestRevisionId != id ) {
return isForNonExpiringMessage(db, latestRevisionId);
}
return expiresIn > EXPIRATION_BACKUP_THRESHOLD;
if (expireStarted > 0 && timeRemaining <= EXPIRATION_BACKUP_THRESHOLD) {
return false;
}
return true;
}
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long messageId) {
String[] columns = new String[] { MessageTable.EXPIRES_IN, MessageTable.VIEW_ONCE };
String[] columns = new String[] { MessageTable.ID, MessageTable.EXPIRE_STARTED, MessageTable.EXPIRES_IN, MessageTable.LATEST_REVISION_ID };
String where = MessageTable.ID + " = ?";
String[] args = SqlUtil.buildArgs(messageId);
try (Cursor mmsCursor = db.query(MessageTable.TABLE_NAME, columns, where, args, null, null, null)) {
if (mmsCursor != null && mmsCursor.moveToFirst()) {
return isNonExpiringMessage(mmsCursor);
return isNonExpiringMessage(db, mmsCursor);
}
}
@@ -194,7 +194,7 @@ public class FullBackupImporter extends FullBackupBase {
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
throws IOException
{
File dataFile = AttachmentTable.newFile(context);
File dataFile = AttachmentTable.newDataFile(context);
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
boolean isLegacyTable = SqlUtil.tableExists(db, "part");
@@ -5,10 +5,16 @@
package org.thoughtcrime.securesms.backup.v2
import org.signal.core.util.Base64
import org.signal.core.util.EventTimer
import org.signal.core.util.logging.Log
import org.signal.core.util.withinTransaction
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
import org.signal.libsignal.messagebackup.MessageBackupKey
import org.signal.libsignal.protocol.ServiceId.Aci
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
import org.thoughtcrime.securesms.backup.v2.processor.AccountDataProcessor
@@ -16,6 +22,7 @@ import org.thoughtcrime.securesms.backup.v2.processor.CallLogBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.ChatItemBackupProcessor
import org.thoughtcrime.securesms.backup.v2.processor.RecipientBackupProcessor
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
@@ -23,13 +30,22 @@ import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.archive.ArchiveGetBackupInfoResponse
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
import org.whispersystems.signalservice.api.archive.ArchiveMediaRequest
import org.whispersystems.signalservice.api.archive.ArchiveMediaResponse
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import java.io.ByteArrayOutputStream
import java.io.InputStream
import kotlin.time.Duration.Companion.milliseconds
@@ -37,6 +53,7 @@ import kotlin.time.Duration.Companion.milliseconds
object BackupRepository {
private val TAG = Log.tag(BackupRepository::class.java)
private const val VERSION = 1L
fun export(plaintext: Boolean = false): ByteArray {
val eventTimer = EventTimer()
@@ -53,7 +70,15 @@ object BackupRepository {
)
}
val exportState = ExportState(System.currentTimeMillis())
writer.use {
writer.write(
BackupInfo(
version = VERSION,
backupTimeMs = exportState.backupTime
)
)
// Note: Without a transaction, we may export inconsistent state. But because we have a transaction,
// writes from other threads are blocked. This is something to think more about.
SignalDatabase.rawDatabase.withinTransaction {
@@ -62,12 +87,12 @@ object BackupRepository {
eventTimer.emit("account")
}
RecipientBackupProcessor.export {
RecipientBackupProcessor.export(exportState) {
writer.write(it)
eventTimer.emit("recipient")
}
ChatBackupProcessor.export { frame ->
ChatBackupProcessor.export(exportState) { frame ->
writer.write(frame)
eventTimer.emit("thread")
}
@@ -77,7 +102,7 @@ object BackupRepository {
eventTimer.emit("call")
}
ChatItemBackupProcessor.export { frame ->
ChatItemBackupProcessor.export(exportState) { frame ->
writer.write(frame)
eventTimer.emit("message")
}
@@ -89,6 +114,13 @@ object BackupRepository {
return outputStream.toByteArray()
}
fun validate(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData): ValidationResult {
val masterKey = SignalStore.svr().getOrCreateMasterKey()
val key = MessageBackupKey(masterKey.serialize(), Aci.parseFromBinary(selfData.aci.toByteArray()))
return MessageBackup.validate(key, MessageBackup.Purpose.REMOTE_BACKUP, inputStreamFactory, length)
}
fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false) {
val eventTimer = EventTimer()
@@ -103,6 +135,15 @@ object BackupRepository {
)
}
val header = frameReader.getHeader()
if (header == null) {
Log.e(TAG, "Backup is missing header!")
return
} else if (header.version > VERSION) {
Log.e(TAG, "Backup version is newer than we understand: ${header.version}")
return
}
// Note: Without a transaction, bad imports could lead to lost data. But because we have a transaction,
// writes from other threads are blocked. This is something to think more about.
SignalDatabase.rawDatabase.withinTransaction {
@@ -118,6 +159,7 @@ object BackupRepository {
SignalDatabase.recipients.setProfileKey(selfId, selfData.profileKey)
SignalDatabase.recipients.setProfileSharing(selfId, true)
eventTimer.emit("setup")
val backupState = BackupState()
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
@@ -162,13 +204,21 @@ object BackupRepository {
}
}
val groups = SignalDatabase.groups.getGroups()
while (groups.hasNext()) {
val group = groups.next()
if (group.id.isV2) {
ApplicationDependencies.getJobManager().add(RequestGroupV2InfoJob(group.id as GroupId.V2))
}
}
Log.d(TAG, "import() ${eventTimer.stop().summary}")
}
/**
* Returns an object with details about the remote backup state.
*/
fun getRemoteBackupState(): NetworkResult<ArchiveGetBackupInfoResponse> {
fun getRemoteBackupState(): NetworkResult<BackupMetadata> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
@@ -182,6 +232,18 @@ object BackupRepository {
}
.then { credential ->
api.getBackupInfo(backupKey, credential)
.map { it to credential }
}
.then { pair ->
val (info, credential) = pair
api.debugGetUploadedMediaItemMetadata(backupKey, credential)
.also { Log.i(TAG, "MediaItemMetadataResult: $it") }
.map { mediaObjects ->
BackupMetadata(
usedSpace = info.usedSpace ?: 0,
mediaCount = mediaObjects.size.toLong()
)
}
}
}
@@ -219,6 +281,77 @@ object BackupRepository {
.also { Log.i(TAG, "OverallResult: $it") } is NetworkResult.Success
}
/**
* Returns an object with details about the remote backup state.
*/
fun debugGetArchivedMediaState(): NetworkResult<List<ArchiveGetMediaItemsResponse.StoredMediaObject>> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.debugGetUploadedMediaItemMetadata(backupKey, credential)
}
}
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
item = attachment.toArchiveMediaRequest(backupKey)
)
}
.also { Log.i(TAG, "backupMediaResult: $it") }
}
fun archiveMedia(attachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResponse> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
items = attachments.map { it.toArchiveMediaRequest(backupKey) }
)
}
.also { Log.i(TAG, "backupMediaResult: $it") }
}
fun deleteArchivedMedia(attachments: List<DatabaseAttachment>): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val mediaToDelete = attachments.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = 3, // TODO [cody] store and reuse backup cdn returned from copy/move call
mediaId = backupKey.deriveMediaId(Base64.decode(it.dataHash!!)).toString()
)
}
return getAuthCredential()
.then { credential ->
api.deleteArchivedMedia(
backupKey = backupKey,
serviceCredential = credential,
mediaToDelete = mediaToDelete
)
}
.also { Log.i(TAG, "deleteBackupMediaResult: $it") }
}
/**
* Retrieves an auth credential, preferring a cached value if available.
*/
@@ -246,6 +379,26 @@ object BackupRepository {
val e164: String,
val profileKey: ProfileKey
)
private fun DatabaseAttachment.toArchiveMediaRequest(backupKey: BackupKey): ArchiveMediaRequest {
val mediaSecrets = backupKey.deriveMediaSecrets(Base64.decode(dataHash!!))
return ArchiveMediaRequest(
sourceAttachment = ArchiveMediaRequest.SourceAttachment(
cdn = cdnNumber,
key = remoteLocation!!
),
objectLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(size)).toInt(),
mediaId = mediaSecrets.id.toString(),
hmacKey = Base64.encodeWithPadding(mediaSecrets.macKey),
encryptionKey = Base64.encodeWithPadding(mediaSecrets.cipherKey),
iv = Base64.encodeWithPadding(mediaSecrets.iv)
)
}
}
class ExportState(val backupTime: Long) {
val recipientIds = HashSet<Long>()
val threadIds = HashSet<Long>()
}
class BackupState {
@@ -255,3 +408,8 @@ class BackupState {
val chatIdToBackupRecipientId = HashMap<Long, Long>()
val callIdToType = HashMap<Long, Long>()
}
class BackupMetadata(
val usedSpace: Long,
val mediaCount: Long
)
@@ -5,9 +5,9 @@
package org.thoughtcrime.securesms.backup.v2.database
import org.signal.core.util.delete
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.database.AttachmentTable
fun AttachmentTable.clearAllDataForBackupRestore() {
writableDatabase.delete(AttachmentTable.TABLE_NAME).run()
writableDatabase.deleteAll(AttachmentTable.TABLE_NAME)
}
@@ -39,17 +39,12 @@ fun CallTable.restoreCallLogFromBackup(call: BackupCall, backupState: BackupStat
Call.Type.UNKNOWN_TYPE -> return
}
val event = when (call.event) {
Call.Event.DELETE -> CallTable.Event.DELETE
Call.Event.JOINED -> CallTable.Event.JOINED
Call.Event.GENERIC_GROUP_CALL -> CallTable.Event.GENERIC_GROUP_CALL
Call.Event.DECLINED -> CallTable.Event.DECLINED
Call.Event.ACCEPTED -> CallTable.Event.ACCEPTED
Call.Event.MISSED -> CallTable.Event.MISSED
Call.Event.OUTGOING_RING -> CallTable.Event.OUTGOING_RING
Call.Event.OUTGOING -> CallTable.Event.ONGOING
Call.Event.NOT_ACCEPTED -> CallTable.Event.NOT_ACCEPTED
Call.Event.UNKNOWN_EVENT -> return
val event = when (call.state) {
Call.State.MISSED -> CallTable.Event.MISSED
Call.State.COMPLETED -> CallTable.Event.ACCEPTED
Call.State.DECLINED_BY_USER -> CallTable.Event.DECLINED
Call.State.DECLINED_BY_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE
Call.State.UNKNOWN_EVENT -> return
}
val direction = if (call.outgoing) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING
@@ -62,7 +57,8 @@ fun CallTable.restoreCallLogFromBackup(call: BackupCall, backupState: BackupStat
CallTable.TYPE to CallTable.Type.serialize(type),
CallTable.DIRECTION to CallTable.Direction.serialize(direction),
CallTable.EVENT to CallTable.Event.serialize(event),
CallTable.TIMESTAMP to call.timestamp
CallTable.TIMESTAMP to call.timestamp,
CallTable.RINGER to if (call.ringerRecipientId != null) backupState.backupToLocalRecipientId[call.ringerRecipientId]?.toLong() else null
)
writableDatabase.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
@@ -102,18 +98,18 @@ class CallLogIterator(private val cursor: Cursor) : Iterator<BackupCall?>, Close
},
timestamp = cursor.requireLong(CallTable.TIMESTAMP),
ringerRecipientId = if (cursor.isNull(CallTable.RINGER)) null else cursor.requireLong(CallTable.RINGER),
event = when (event) {
CallTable.Event.ONGOING -> Call.Event.OUTGOING
CallTable.Event.OUTGOING_RING -> Call.Event.OUTGOING_RING
CallTable.Event.ACCEPTED -> Call.Event.ACCEPTED
CallTable.Event.DECLINED -> Call.Event.DECLINED
CallTable.Event.GENERIC_GROUP_CALL -> Call.Event.GENERIC_GROUP_CALL
CallTable.Event.JOINED -> Call.Event.JOINED
CallTable.Event.MISSED,
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> Call.Event.MISSED
CallTable.Event.DELETE -> Call.Event.DELETE
CallTable.Event.RINGING -> Call.Event.UNKNOWN_EVENT
CallTable.Event.NOT_ACCEPTED -> Call.Event.NOT_ACCEPTED
state = when (event) {
CallTable.Event.ONGOING -> Call.State.COMPLETED
CallTable.Event.OUTGOING_RING -> Call.State.COMPLETED
CallTable.Event.ACCEPTED -> Call.State.COMPLETED
CallTable.Event.DECLINED -> Call.State.DECLINED_BY_USER
CallTable.Event.GENERIC_GROUP_CALL -> Call.State.COMPLETED
CallTable.Event.JOINED -> Call.State.COMPLETED
CallTable.Event.MISSED -> Call.State.MISSED
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> Call.State.DECLINED_BY_NOTIFICATION_PROFILE
CallTable.Event.DELETE -> Call.State.COMPLETED
CallTable.Event.RINGING -> Call.State.MISSED
CallTable.Event.NOT_ACCEPTED -> Call.State.MISSED
}
)
}
@@ -9,6 +9,7 @@ import android.database.Cursor
import com.annimon.stream.Stream
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.Base64.decode
import org.signal.core.util.Base64.decodeOrThrow
import org.signal.core.util.logging.Log
import org.signal.core.util.requireBlob
@@ -16,12 +17,15 @@ import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Quote
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
@@ -40,11 +44,16 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.calls
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
import org.thoughtcrime.securesms.database.model.GroupsV2UpdateMessageConverter
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.push.ServiceId.ACI
@@ -99,6 +108,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
val reactionsById: Map<Long, List<ReactionRecord>> = SignalDatabase.reactions.getReactionsForMessages(records.keys)
val mentionsById: Map<Long, List<Mention>> = SignalDatabase.mentions.getMentionsForMessages(records.keys)
val attachmentsById: Map<Long, List<DatabaseAttachment>> = SignalDatabase.attachments.getAttachmentsForMessages(records.keys)
val groupReceiptsById: Map<Long, List<GroupReceiptTable.GroupReceiptInfo>> = SignalDatabase.groupReceipts.getGroupReceiptInfoForMessages(records.keys)
for ((id, record) in records) {
@@ -110,14 +121,23 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
MessageTypes.isIdentityUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_UPDATE))
MessageTypes.isIdentityVerified(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_VERIFIED))
MessageTypes.isIdentityDefault(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.IDENTITY_DEFAULT))
MessageTypes.isChangeNumber(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHANGE_NUMBER))
MessageTypes.isBoostRequest(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BOOST_REQUEST))
MessageTypes.isChangeNumber(record.type) -> {
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHANGE_NUMBER))
builder.sms = false
}
MessageTypes.isBoostRequest(record.type) -> {
builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BOOST_REQUEST))
builder.sms = false
}
MessageTypes.isEndSessionType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.END_SESSION))
MessageTypes.isChatSessionRefresh(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.CHAT_SESSION_REFRESH))
MessageTypes.isBadDecryptType(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.BAD_DECRYPT))
MessageTypes.isPaymentsActivated(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENTS_ACTIVATED))
MessageTypes.isPaymentsRequestToActivate(record.type) -> builder.updateMessage = ChatUpdateMessage(simpleUpdate = SimpleChatUpdate(type = SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST))
MessageTypes.isExpirationTimerUpdate(record.type) -> builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate((record.expiresIn / 1000).toInt()))
MessageTypes.isExpirationTimerUpdate(record.type) -> {
builder.updateMessage = ChatUpdateMessage(expirationTimerChange = ExpirationTimerChatUpdate(record.expiresIn.toInt()))
builder.expiresInMs = null
}
MessageTypes.isProfileChange(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
profileChange = try {
@@ -133,6 +153,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
ProfileChangeChatUpdate()
}
)
builder.sms = false
}
MessageTypes.isSessionSwitchoverType(record.type) -> {
builder.updateMessage = ChatUpdateMessage(
@@ -154,6 +175,26 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
)
}
MessageTypes.isGroupV2(record.type) && MessageTypes.isGroupUpdate(record.type) -> {
val groupChange = record.messageExtras?.gv2UpdateDescription?.groupChangeUpdate
if (groupChange != null) {
builder.updateMessage = ChatUpdateMessage(
groupChange = groupChange
)
} else if (record.body != null) {
try {
val decoded: ByteArray = decode(record.body)
val context = DecryptedGroupV2Context.ADAPTER.decode(decoded)
builder.updateMessage = ChatUpdateMessage(
groupChange = GroupsV2UpdateMessageConverter.translateDecryptedChange(selfIds = SignalStore.account().getServiceIds(), context)
)
} catch (e: IOException) {
continue
}
} else {
continue
}
}
MessageTypes.isCallLog(record.type) -> {
val call = calls.getCallByMessageId(record.id)
if (call != null) {
@@ -161,10 +202,10 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
} else {
when {
MessageTypes.isMissedAudioCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_AUDIO_CALL)))
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL)))
}
MessageTypes.isMissedVideoCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_VIDEO_CALL)))
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL)))
}
MessageTypes.isIncomingAudioCall(record.type) -> {
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callMessage = IndividualCallChatUpdate(type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL)))
@@ -203,11 +244,11 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
}
record.body == null -> {
Log.w(TAG, "Record missing a body, skipping")
record.body == null && !attachmentsById.containsKey(record.id) -> {
Log.w(TAG, "Record missing a body and doesnt have attachments, skipping")
continue
}
else -> builder.standardMessage = record.toTextMessage(reactionsById[id])
else -> builder.standardMessage = record.toStandardMessage(reactionsById[id], mentions = mentionsById[id], attachments = attachmentsById[record.id])
}
buffer += builder.build()
@@ -241,7 +282,6 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
chatId = record.threadId
authorId = record.fromRecipientId
dateSent = record.dateSent
sealedSender = record.sealedSender
expireStartDate = if (record.expireStarted > 0) record.expireStarted else null
expiresInMs = if (record.expiresIn > 0) record.expiresIn else null
revisions = emptyList()
@@ -255,19 +295,28 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
incoming = ChatItem.IncomingMessageDetails(
dateServerSent = record.dateServer,
dateReceived = record.dateReceived,
read = record.read
read = record.read,
sealedSender = record.sealedSender
)
}
}
}
private fun BackupMessageRecord.toTextMessage(reactionRecords: List<ReactionRecord>?): StandardMessage {
private fun BackupMessageRecord.toStandardMessage(reactionRecords: List<ReactionRecord>?, mentions: List<Mention>?, attachments: List<DatabaseAttachment>?): StandardMessage {
val text = if (body == null) {
null
} else {
Text(
body = this.body,
bodyRanges = (this.bodyRanges?.toBackupBodyRanges() ?: emptyList()) + (mentions?.toBackupBodyRanges() ?: emptyList())
)
}
val quotedAttachments = attachments?.filter { it.quote } ?: emptyList()
val messageAttachments = attachments?.filter { !it.quote } ?: emptyList()
return StandardMessage(
quote = this.toQuote(),
text = Text(
body = this.body!!,
bodyRanges = this.bodyRanges?.toBackupBodyRanges() ?: emptyList()
),
quote = this.toQuote(quotedAttachments),
text = text,
attachments = messageAttachments.toBackupAttachments(),
// TODO Link previews!
linkPreview = emptyList(),
longText = null,
@@ -275,14 +324,14 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
)
}
private fun BackupMessageRecord.toQuote(): Quote? {
private fun BackupMessageRecord.toQuote(attachments: List<DatabaseAttachment>? = null): Quote? {
return if (this.quoteTargetSentTimestamp != MessageTable.QUOTE_NOT_PRESENT_ID && this.quoteAuthor > 0) {
// TODO Attachments!
val type = QuoteModel.Type.fromCode(this.quoteType)
Quote(
targetSentTimestamp = this.quoteTargetSentTimestamp.takeIf { !this.quoteMissing && it != MessageTable.QUOTE_TARGET_MISSING_ID },
authorId = this.quoteAuthor,
text = this.quoteBody,
attachments = attachments?.toBackupQuoteAttachments() ?: emptyList(),
bodyRanges = this.quoteBodyRanges?.toBackupBodyRanges() ?: emptyList(),
type = when (type) {
QuoteModel.Type.NORMAL -> Quote.Type.NORMAL
@@ -294,6 +343,54 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
private fun List<DatabaseAttachment>.toBackupQuoteAttachments(): List<Quote.QuotedAttachment> {
return this.map { attachment ->
Quote.QuotedAttachment(
contentType = attachment.contentType,
fileName = attachment.fileName,
thumbnail = attachment.toBackupAttachment()
)
}
}
private fun DatabaseAttachment.toBackupAttachment(): MessageAttachment {
return MessageAttachment(
pointer = FilePointer(
attachmentLocator = FilePointer.AttachmentLocator(
cdnKey = this.remoteLocation ?: "",
cdnNumber = this.cdnNumber,
uploadTimestamp = this.uploadTimestamp
),
key = if (remoteKey != null) decode(remoteKey).toByteString() else null,
contentType = this.contentType,
size = this.size.toInt(),
incrementalMac = this.incrementalDigest?.toByteString(),
incrementalMacChunkSize = this.incrementalMacChunkSize,
fileName = this.fileName,
width = this.width,
height = this.height,
caption = this.caption,
blurHash = this.blurHash?.hash
)
)
}
private fun List<DatabaseAttachment>.toBackupAttachments(): List<MessageAttachment> {
return this.map { attachment ->
attachment.toBackupAttachment()
}
}
private fun List<Mention>.toBackupBodyRanges(): List<BackupBodyRange> {
return this.map {
BackupBodyRange(
start = it.start,
length = it.length,
mentionAci = SignalDatabase.recipients.getRecord(it.recipientId).aci?.toByteString()
)
}
}
private fun ByteArray.toBackupBodyRanges(): List<BackupBodyRange> {
val decoded: BodyRangeList = try {
BodyRangeList.ADAPTER.decode(this)
@@ -306,7 +403,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
BackupBodyRange(
start = it.start,
length = it.length,
mentionAci = it.mentionUuid?.let { UuidUtil.parseOrThrow(it) }?.toByteArray()?.toByteString(),
mentionAci = it.mentionUuid?.let { uuid -> UuidUtil.parseOrThrow(uuid) }?.toByteArray()?.toByteString(),
style = it.style?.toBackupBodyRangeStyle()
)
}
@@ -412,6 +509,17 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
private fun ByteArray?.parseMessageExtras(): MessageExtras? {
if (this == null) {
return null
}
return try {
MessageExtras.ADAPTER.decode(this)
} catch (e: java.lang.Exception) {
null
}
}
private fun Cursor.toBackupMessageRecord(): BackupMessageRecord {
return BackupMessageRecord(
id = this.requireLong(MessageTable.ID),
@@ -443,7 +551,8 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
receiptTimestamp = this.requireLong(MessageTable.RECEIPT_TIMESTAMP),
networkFailureRecipientIds = this.requireString(MessageTable.NETWORK_FAILURES).parseNetworkFailures(),
identityMismatchRecipientIds = this.requireString(MessageTable.MISMATCHED_IDENTITIES).parseIdentityMismatches(),
baseType = this.requireLong(COLUMN_BASE_TYPE)
baseType = this.requireLong(COLUMN_BASE_TYPE),
messageExtras = this.requireBlob(MessageTable.MESSAGE_EXTRAS).parseMessageExtras()
)
}
@@ -477,6 +586,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
val read: Boolean,
val networkFailureRecipientIds: Set<Long>,
val identityMismatchRecipientIds: Set<Long>,
val baseType: Long
val baseType: Long,
val messageExtras: MessageExtras?
)
}
@@ -10,13 +10,17 @@ import androidx.core.content.contentValuesOf
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.core.util.requireLong
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.Quote
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
@@ -28,11 +32,15 @@ import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.ReactionTable
import org.thoughtcrime.securesms.database.SQLiteDatabase
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
import org.thoughtcrime.securesms.database.documents.NetworkFailure
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
@@ -40,7 +48,12 @@ import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.Optional
/**
* An object that will ingest all fo the [ChatItem]s you want to write, buffer them until hitting a specified batch size, and then batch insert them
@@ -152,7 +165,6 @@ class ChatItemImportInserter(
if (buffer.size == 0) {
return false
}
buildBulkInsert(MessageTable.TABLE_NAME, MESSAGE_COLUMNS, buffer.messages).forEach {
db.rawQuery("${it.query.where} RETURNING ${MessageTable.ID}", it.query.whereArgs).use { cursor ->
var index = 0
@@ -177,6 +189,8 @@ class ChatItemImportInserter(
messageId = SqlUtil.getNextAutoIncrementId(db, MessageTable.TABLE_NAME)
buffer.reset()
return true
}
@@ -202,6 +216,38 @@ class ChatItemImportInserter(
}
}
}
if (this.standardMessage != null) {
val bodyRanges = this.standardMessage.text?.bodyRanges
if (!bodyRanges.isNullOrEmpty()) {
val mentions = bodyRanges.filter { it.mentionAci != null && it.start != null && it.length != null }
.mapNotNull {
val aci = ServiceId.ACI.parseOrNull(it.mentionAci!!)
if (aci != null && !aci.isUnknown) {
val id = RecipientId.from(aci)
Mention(id, it.start!!, it.length!!)
} else {
null
}
}
if (mentions.isNotEmpty()) {
followUp = { messageId ->
SignalDatabase.mentions.insert(threadId, messageId, mentions)
}
}
}
val attachments = this.standardMessage.attachments.mapNotNull { attachment ->
attachment.toLocalAttachment()
}
val quoteAttachments = this.standardMessage.quote?.attachments?.mapNotNull {
it.toLocalAttachment()
} ?: emptyList()
if (attachments.isNotEmpty()) {
followUp = { messageRowId ->
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, attachments, quoteAttachments)
}
}
}
return MessageInsert(contentValues, followUp)
}
@@ -217,7 +263,7 @@ class ChatItemImportInserter(
contentValues.put(MessageTable.TO_RECIPIENT_ID, (if (this.outgoing != null) chatRecipientId else selfId).serialize())
contentValues.put(MessageTable.THREAD_ID, threadId)
contentValues.put(MessageTable.DATE_RECEIVED, this.incoming?.dateReceived ?: this.dateSent)
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOf { it.lastStatusUpdateTimestamp } ?: 0)
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOfOrNull { it.lastStatusUpdateTimestamp } ?: 0)
contentValues.putNull(MessageTable.LATEST_REVISION_ID)
contentValues.putNull(MessageTable.ORIGINAL_MESSAGE_ID)
contentValues.put(MessageTable.REVISION_NUMBER, 0)
@@ -241,8 +287,9 @@ class ChatItemImportInserter(
contentValues.put(MessageTable.VIEWED_COLUMN, 0)
contentValues.put(MessageTable.HAS_READ_RECEIPT, 0)
contentValues.put(MessageTable.HAS_DELIVERY_RECEIPT, 0)
contentValues.put(MessageTable.UNIDENTIFIED, this.sealedSender?.toInt())
contentValues.put(MessageTable.UNIDENTIFIED, this.incoming?.sealedSender?.toInt() ?: 0)
contentValues.put(MessageTable.READ, this.incoming?.read?.toInt() ?: 0)
contentValues.put(MessageTable.NOTIFIED, 1)
}
contentValues.put(MessageTable.QUOTE_ID, 0)
@@ -265,7 +312,6 @@ class ChatItemImportInserter(
val reactions: List<Reaction> = when {
this.standardMessage != null -> this.standardMessage.reactions
this.contactMessage != null -> this.contactMessage.reactions
this.voiceMessage != null -> this.voiceMessage.reactions
this.stickerMessage != null -> this.stickerMessage.reactions
else -> emptyList()
}
@@ -342,7 +388,7 @@ class ChatItemImportInserter(
this.put(MessageTable.BODY, standardMessage.text.body)
if (standardMessage.text.bodyRanges.isNotEmpty()) {
this.put(MessageTable.MESSAGE_RANGES, standardMessage.text.bodyRanges.toLocalBodyRanges()?.encode() as ByteArray?)
this.put(MessageTable.MESSAGE_RANGES, standardMessage.text.bodyRanges.toLocalBodyRanges()?.encode())
}
}
@@ -355,23 +401,24 @@ class ChatItemImportInserter(
var typeFlags: Long = 0
when {
updateMessage.simpleUpdate != null -> {
val typeWithoutBase = (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
typeFlags = when (updateMessage.simpleUpdate.type) {
SimpleChatUpdate.Type.UNKNOWN -> 0
SimpleChatUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE
SimpleChatUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT
SimpleChatUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT
SimpleChatUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT
SimpleChatUpdate.Type.UNKNOWN -> typeWithoutBase
SimpleChatUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE or typeWithoutBase
SimpleChatUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT or typeWithoutBase
SimpleChatUpdate.Type.IDENTITY_VERIFIED -> MessageTypes.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT or typeWithoutBase
SimpleChatUpdate.Type.IDENTITY_DEFAULT -> MessageTypes.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT or typeWithoutBase
SimpleChatUpdate.Type.CHANGE_NUMBER -> MessageTypes.CHANGE_NUMBER_TYPE
SimpleChatUpdate.Type.BOOST_REQUEST -> MessageTypes.BOOST_REQUEST_TYPE
SimpleChatUpdate.Type.END_SESSION -> MessageTypes.END_SESSION_BIT
SimpleChatUpdate.Type.CHAT_SESSION_REFRESH -> MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT
SimpleChatUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE
SimpleChatUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED
SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST
SimpleChatUpdate.Type.END_SESSION -> MessageTypes.END_SESSION_BIT or typeWithoutBase
SimpleChatUpdate.Type.CHAT_SESSION_REFRESH -> MessageTypes.ENCRYPTION_REMOTE_FAILED_BIT or typeWithoutBase
SimpleChatUpdate.Type.BAD_DECRYPT -> MessageTypes.BAD_DECRYPT_TYPE or typeWithoutBase
SimpleChatUpdate.Type.PAYMENTS_ACTIVATED -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED or typeWithoutBase
SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST -> MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST or typeWithoutBase
}
}
updateMessage.expirationTimerChange != null -> {
typeFlags = MessageTypes.EXPIRATION_TIMER_UPDATE_BIT
typeFlags = getAsLong(MessageTable.TYPE) or MessageTypes.EXPIRATION_TIMER_UPDATE_BIT
put(MessageTable.EXPIRES_IN, updateMessage.expirationTimerChange.expiresInMs.toLong())
}
updateMessage.profileChange != null -> {
@@ -381,12 +428,12 @@ class ChatItemImportInserter(
put(MessageTable.BODY, Base64.encodeWithPadding(profileChangeDetails))
}
updateMessage.sessionSwitchover != null -> {
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
val sessionSwitchoverDetails = SessionSwitchoverEvent(e164 = updateMessage.sessionSwitchover.e164.toString()).encode()
put(MessageTable.BODY, Base64.encodeWithPadding(sessionSwitchoverDetails))
}
updateMessage.threadMerge != null -> {
typeFlags = MessageTypes.THREAD_MERGE_TYPE
typeFlags = MessageTypes.THREAD_MERGE_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
val threadMergeDetails = ThreadMergeEvent(previousE164 = updateMessage.threadMerge.previousE164.toString()).encode()
put(MessageTable.BODY, Base64.encodeWithPadding(threadMergeDetails))
}
@@ -401,8 +448,10 @@ class ChatItemImportInserter(
IndividualCallChatUpdate.Type.INCOMING_VIDEO_CALL -> MessageTypes.INCOMING_VIDEO_CALL_TYPE
IndividualCallChatUpdate.Type.OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE
IndividualCallChatUpdate.Type.OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE
IndividualCallChatUpdate.Type.MISSED_AUDIO_CALL -> MessageTypes.MISSED_AUDIO_CALL_TYPE
IndividualCallChatUpdate.Type.MISSED_VIDEO_CALL -> MessageTypes.MISSED_VIDEO_CALL_TYPE
IndividualCallChatUpdate.Type.MISSED_INCOMING_AUDIO_CALL -> MessageTypes.MISSED_AUDIO_CALL_TYPE
IndividualCallChatUpdate.Type.MISSED_INCOMING_VIDEO_CALL -> MessageTypes.MISSED_VIDEO_CALL_TYPE
IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_AUDIO_CALL -> MessageTypes.OUTGOING_AUDIO_CALL_TYPE
IndividualCallChatUpdate.Type.UNANSWERED_OUTGOING_VIDEO_CALL -> MessageTypes.OUTGOING_VIDEO_CALL_TYPE
IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags
}
}
@@ -410,8 +459,19 @@ class ChatItemImportInserter(
// Calls don't use the incoming/outgoing flags, so we overwrite the flags here
this.put(MessageTable.TYPE, typeFlags)
}
updateMessage.groupChange != null -> {
put(MessageTable.BODY, "")
put(
MessageTable.MESSAGE_EXTRAS,
MessageExtras(
gv2UpdateDescription =
GV2UpdateDescription(groupChangeUpdate = updateMessage.groupChange)
).encode()
)
typeFlags = getAsLong(MessageTable.TYPE) or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT
}
}
this.put(MessageTable.TYPE, getAsLong(MessageTable.TYPE) or typeFlags)
this.put(MessageTable.TYPE, typeFlags)
}
private fun ContentValues.addQuote(quote: Quote) {
@@ -470,7 +530,7 @@ class ChatItemImportInserter(
}
return BodyRangeList(
ranges = this.map { bodyRange ->
ranges = this.filter { it.mentionAci == null }.map { bodyRange ->
BodyRangeList.BodyRange(
mentionUuid = bodyRange.mentionAci?.let { UuidUtil.fromByteString(it) }?.toString(),
style = bodyRange.style?.let {
@@ -503,6 +563,39 @@ class ChatItemImportInserter(
}
}
private fun MessageAttachment.toLocalAttachment(contentType: String? = pointer?.contentType, fileName: String? = pointer?.fileName): Attachment? {
if (pointer == null) return null
if (pointer.attachmentLocator != null) {
val signalAttachmentPointer = SignalServiceAttachmentPointer(
pointer.attachmentLocator.cdnNumber,
SignalServiceAttachmentRemoteId.from(pointer.attachmentLocator.cdnKey),
contentType,
pointer.key?.toByteArray(),
Optional.ofNullable(pointer.size),
Optional.empty(),
pointer.width ?: 0,
pointer.height ?: 0,
Optional.empty(),
Optional.ofNullable(pointer.incrementalMac?.toByteArray()),
pointer.incrementalMacChunkSize ?: 0,
Optional.ofNullable(fileName),
flag == MessageAttachment.Flag.VOICE_MESSAGE,
flag == MessageAttachment.Flag.BORDERLESS,
flag == MessageAttachment.Flag.GIF,
Optional.ofNullable(pointer.caption),
Optional.ofNullable(pointer.blurHash),
pointer.attachmentLocator.uploadTimestamp
)
return PointerAttachment.forPointer(Optional.of(signalAttachmentPointer)).orNull()
}
return null
}
private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? {
return thumbnail?.toLocalAttachment(this.contentType, this.fileName)
?: if (this.contentType == null) null else PointerAttachment.forPointer(SignalServiceDataMessage.Quote.QuotedAttachment(contentType = this.contentType!!, fileName = this.fileName, thumbnail = null)).orNull()
}
private class MessageInsert(val contentValues: ContentValues, val followUp: ((Long) -> Unit)?)
private class Buffer(
@@ -512,5 +605,11 @@ class ChatItemImportInserter(
) {
val size: Int
get() = listOf(messages.size, reactions.size, groupReceipts.size).max()
fun reset() {
messages.clear()
reactions.clear()
groupReceipts.clear()
}
}
}
@@ -7,7 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.database
import okio.ByteString.Companion.toByteString
import org.signal.core.util.CursorUtil
import org.signal.core.util.delete
import org.signal.core.util.deleteAll
import org.signal.core.util.logging.Log
import org.signal.core.util.readToList
import org.signal.core.util.requireLong
@@ -28,38 +28,45 @@ import org.thoughtcrime.securesms.backup.v2.proto.DistributionList as BackupDist
private val TAG = Log.tag(DistributionListTables::class.java)
data class DistributionRecipient(val id: RecipientId, val record: DistributionListRecord)
fun DistributionListTables.getAllForBackup(): List<BackupRecipient> {
val records = readableDatabase
.select()
.from(DistributionListTables.ListTable.TABLE_NAME)
.where(DistributionListTables.ListTable.IS_NOT_DELETED)
.run()
.readToList { cursor ->
val id: DistributionListId = DistributionListId.from(cursor.requireLong(DistributionListTables.ListTable.ID))
val privacyMode: DistributionListPrivacyMode = cursor.requireObject(DistributionListTables.ListTable.PRIVACY_MODE, DistributionListPrivacyMode.Serializer)
DistributionListRecord(
id = id,
name = cursor.requireNonNullString(DistributionListTables.ListTable.NAME),
distributionId = DistributionId.from(cursor.requireNonNullString(DistributionListTables.ListTable.DISTRIBUTION_ID)),
allowsReplies = CursorUtil.requireBoolean(cursor, DistributionListTables.ListTable.ALLOWS_REPLIES),
rawMembers = getRawMembers(id, privacyMode),
members = getMembers(id),
deletedAtTimestamp = 0L,
isUnknown = CursorUtil.requireBoolean(cursor, DistributionListTables.ListTable.IS_UNKNOWN),
privacyMode = privacyMode
val recipientId: RecipientId = RecipientId.from(cursor.requireLong(DistributionListTables.ListTable.RECIPIENT_ID))
DistributionRecipient(
id = recipientId,
record = DistributionListRecord(
id = id,
name = cursor.requireNonNullString(DistributionListTables.ListTable.NAME),
distributionId = DistributionId.from(cursor.requireNonNullString(DistributionListTables.ListTable.DISTRIBUTION_ID)),
allowsReplies = CursorUtil.requireBoolean(cursor, DistributionListTables.ListTable.ALLOWS_REPLIES),
rawMembers = getRawMembers(id, privacyMode),
members = getMembers(id),
deletedAtTimestamp = 0L,
isUnknown = CursorUtil.requireBoolean(cursor, DistributionListTables.ListTable.IS_UNKNOWN),
privacyMode = privacyMode
)
)
}
return records
.map { record ->
.map { recipient ->
BackupRecipient(
id = recipient.id.toLong(),
distributionList = BackupDistributionList(
name = record.name,
distributionId = record.distributionId.asUuid().toByteArray().toByteString(),
allowReplies = record.allowsReplies,
deletionTimestamp = record.deletedAtTimestamp,
privacyMode = record.privacyMode.toBackupPrivacyMode(),
memberRecipientIds = record.members.map { it.toLong() }
name = recipient.record.name,
distributionId = recipient.record.distributionId.asUuid().toByteArray().toByteString(),
allowReplies = recipient.record.allowsReplies,
deletionTimestamp = recipient.record.deletedAtTimestamp,
privacyMode = recipient.record.privacyMode.toBackupPrivacyMode(),
memberRecipientIds = recipient.record.members.map { it.toLong() }
)
)
}
@@ -88,12 +95,10 @@ fun DistributionListTables.restoreFromBackup(dlist: BackupDistributionList, back
fun DistributionListTables.clearAllDataForBackupRestore() {
writableDatabase
.delete(DistributionListTables.ListTable.TABLE_NAME)
.run()
.deleteAll(DistributionListTables.ListTable.TABLE_NAME)
writableDatabase
.delete(DistributionListTables.MembershipTable.TABLE_NAME)
.run()
.deleteAll(DistributionListTables.MembershipTable.TABLE_NAME)
}
private fun DistributionListPrivacyMode.toBackupPrivacyMode(): BackupDistributionList.PrivacyMode {
@@ -11,11 +11,12 @@ import org.signal.core.util.select
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
import java.util.concurrent.TimeUnit
private val TAG = Log.tag(MessageTable::class.java)
private const val BASE_TYPE = "base_type"
fun MessageTable.getMessagesForBackup(): ChatItemExportIterator {
fun MessageTable.getMessagesForBackup(backupTime: Long): ChatItemExportIterator {
val cursor = readableDatabase
.select(
MessageTable.ID,
@@ -47,18 +48,17 @@ fun MessageTable.getMessagesForBackup(): ChatItemExportIterator {
MessageTable.READ,
MessageTable.NETWORK_FAILURES,
MessageTable.MISMATCHED_IDENTITIES,
"${MessageTable.TYPE} & ${MessageTypes.BASE_TYPE_MASK} AS ${ChatItemExportIterator.COLUMN_BASE_TYPE}"
"${MessageTable.TYPE} & ${MessageTypes.BASE_TYPE_MASK} AS ${ChatItemExportIterator.COLUMN_BASE_TYPE}",
MessageTable.MESSAGE_EXTRAS
)
.from(MessageTable.TABLE_NAME)
.where(
"""
$BASE_TYPE IN (
${MessageTypes.BASE_INBOX_TYPE},
${MessageTypes.BASE_OUTBOX_TYPE},
${MessageTypes.BASE_SENT_TYPE},
${MessageTypes.BASE_SENDING_TYPE},
${MessageTypes.BASE_SENT_FAILED_TYPE}
) OR ${MessageTable.IS_CALL_TYPE_CLAUSE}
(
${MessageTable.EXPIRE_STARTED} = 0
OR
(${MessageTable.EXPIRES_IN} > 0 AND (${MessageTable.EXPIRE_STARTED} + ${MessageTable.EXPIRES_IN}) > $backupTime + ${TimeUnit.DAYS.toMillis(1)})
)
"""
)
.orderBy("${MessageTable.DATE_RECEIVED} ASC")
@@ -10,7 +10,7 @@ import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.delete
import org.signal.core.util.deleteAll
import org.signal.core.util.logging.Log
import org.signal.core.util.nullIfBlank
import org.signal.core.util.requireBoolean
@@ -22,23 +22,31 @@ import org.signal.core.util.select
import org.signal.core.util.toInt
import org.signal.core.util.update
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.Contact
import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.backup.v2.proto.Self
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTableCursorUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.api.util.toByteArray
import java.io.Closeable
typealias BackupRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
@@ -94,7 +102,8 @@ fun RecipientTable.getGroupsForBackup(): BackupGroupIterator {
"${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL}",
"${RecipientTable.TABLE_NAME}.${RecipientTable.EXTRAS}",
"${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY}",
"${GroupTable.TABLE_NAME}.${GroupTable.SHOW_AS_STORY_STATE}"
"${GroupTable.TABLE_NAME}.${GroupTable.SHOW_AS_STORY_STATE}",
"${GroupTable.TABLE_NAME}.${GroupTable.TITLE}"
)
.from(
"""
@@ -102,6 +111,7 @@ fun RecipientTable.getGroupsForBackup(): BackupGroupIterator {
INNER JOIN ${GroupTable.TABLE_NAME} ON ${RecipientTable.TABLE_NAME}.${RecipientTable.ID} = ${GroupTable.TABLE_NAME}.${GroupTable.RECIPIENT_ID}
"""
)
.where("${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY} IS NOT NULL")
.run()
return BackupGroupIterator(cursor)
@@ -115,8 +125,10 @@ fun RecipientTable.restoreRecipientFromBackup(recipient: BackupRecipient, backup
// TODO Also, should we move this when statement up to mimic the export? Kinda weird that this calls distributionListTable functions
return when {
recipient.contact != null -> restoreContactFromBackup(recipient.contact)
recipient.group != null -> restoreGroupFromBackup(recipient.group)
recipient.distributionList != null -> SignalDatabase.distributionLists.restoreFromBackup(recipient.distributionList, backupState)
recipient.self != null -> Recipient.self().id
recipient.releaseNotes != null -> restoreReleaseNotes()
else -> {
Log.w(TAG, "Unrecognized recipient type!")
null
@@ -155,7 +167,7 @@ fun RecipientTable.restoreSelfFromBackup(accountData: AccountData, selfId: Recip
}
fun RecipientTable.clearAllDataForBackupRestore() {
writableDatabase.delete(RecipientTable.TABLE_NAME).run()
writableDatabase.deleteAll(RecipientTable.TABLE_NAME)
SqlUtil.resetAutoIncrementValue(writableDatabase, RecipientTable.TABLE_NAME)
RecipientId.clearCache()
@@ -177,6 +189,7 @@ private fun RecipientTable.restoreContactFromBackup(contact: Contact): Recipient
.values(
RecipientTable.BLOCKED to contact.blocked,
RecipientTable.HIDDEN to contact.hidden,
RecipientTable.TYPE to RecipientTable.RecipientType.INDIVIDUAL.id,
RecipientTable.PROFILE_FAMILY_NAME to contact.profileFamilyName.nullIfBlank(),
RecipientTable.PROFILE_GIVEN_NAME to contact.profileGivenName.nullIfBlank(),
RecipientTable.PROFILE_JOINED_NAME to ProfileName.fromParts(contact.profileGivenName.nullIfBlank(), contact.profileFamilyName.nullIfBlank()).toString().nullIfBlank(),
@@ -193,6 +206,50 @@ private fun RecipientTable.restoreContactFromBackup(contact: Contact): Recipient
return id
}
private fun RecipientTable.restoreReleaseNotes(): RecipientId {
val releaseChannelId: RecipientId = insertReleaseChannelRecipient()
SignalStore.releaseChannelValues().setReleaseChannelRecipientId(releaseChannelId)
setProfileName(releaseChannelId, ProfileName.asGiven("Signal"))
setMuted(releaseChannelId, Long.MAX_VALUE)
return releaseChannelId
}
private fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId {
val masterKey = GroupMasterKey(group.masterKey.toByteArray())
val groupId = GroupId.v2(masterKey)
val placeholderState = DecryptedGroup.Builder()
.revision(GroupsV2StateProcessor.PLACEHOLDER_REVISION)
.build()
val values = ContentValues().apply {
put(RecipientTable.GROUP_ID, groupId.toString())
put(RecipientTable.AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize())
put(RecipientTable.PROFILE_SHARING, group.whitelisted)
put(RecipientTable.TYPE, RecipientTable.RecipientType.GV2.id)
put(RecipientTable.STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
if (group.hideStory) {
val extras = RecipientExtras.Builder().hideStory(true).build()
put(RecipientTable.EXTRAS, extras.encode())
}
}
val recipientId = writableDatabase.insert(RecipientTable.TABLE_NAME, null, values)
val groupValues = ContentValues().apply {
put(GroupTable.RECIPIENT_ID, recipientId)
put(GroupTable.GROUP_ID, groupId.toString())
put(GroupTable.TITLE, group.name)
put(GroupTable.V2_MASTER_KEY, masterKey.serialize())
put(GroupTable.V2_DECRYPTED_GROUP, placeholderState.encode())
put(GroupTable.V2_REVISION, placeholderState.revision)
put(GroupTable.SHOW_AS_STORY_STATE, group.storySendMode.toGroupShowAsStoryState().code)
}
writableDatabase.insert(GroupTable.TABLE_NAME, null, groupValues)
return RecipientId.from(recipientId)
}
private fun Contact.toLocalExtras(): RecipientExtras {
return RecipientExtras(
hideStory = this.hideStory
@@ -235,8 +292,8 @@ class BackupContactIterator(private val cursor: Cursor, private val selfId: Long
return BackupRecipient(
id = id,
contact = Contact(
aci = aci?.toByteArray()?.toByteString(),
pni = pni?.toByteArray()?.toByteString(),
aci = aci?.rawUuid?.toByteArray()?.toByteString(),
pni = pni?.rawUuid?.toByteArray()?.toByteString(),
username = cursor.requireString(RecipientTable.USERNAME),
e164 = cursor.requireString(RecipientTable.E164)?.e164ToLong(),
blocked = cursor.requireBoolean(RecipientTable.BLOCKED),
@@ -280,7 +337,8 @@ class BackupGroupIterator(private val cursor: Cursor) : Iterator<BackupRecipient
masterKey = cursor.requireNonNullBlob(GroupTable.V2_MASTER_KEY).toByteString(),
whitelisted = cursor.requireBoolean(RecipientTable.PROFILE_SHARING),
hideStory = extras?.hideStory() ?: false,
storySendMode = showAsStoryState.toGroupStorySendMode()
storySendMode = showAsStoryState.toGroupStorySendMode(),
name = cursor.requireString(GroupTable.TITLE) ?: ""
)
)
}
@@ -324,6 +382,14 @@ private fun GroupTable.ShowAsStoryState.toGroupStorySendMode(): Group.StorySendM
}
}
private fun Group.StorySendMode.toGroupShowAsStoryState(): GroupTable.ShowAsStoryState {
return when (this) {
Group.StorySendMode.ENABLED -> GroupTable.ShowAsStoryState.ALWAYS
Group.StorySendMode.DISABLED -> GroupTable.ShowAsStoryState.NEVER
Group.StorySendMode.DEFAULT -> GroupTable.ShowAsStoryState.IF_ACTIVE
}
}
private val Contact.formattedE164: String?
get() {
return e164?.let {
@@ -6,15 +6,16 @@
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import androidx.core.content.contentValuesOf
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.select
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.recipients.RecipientId
import java.io.Closeable
@@ -22,16 +23,21 @@ import java.io.Closeable
private val TAG = Log.tag(ThreadTable::class.java)
fun ThreadTable.getThreadsForBackup(): ChatIterator {
val cursor = readableDatabase
.select(
ThreadTable.ID,
ThreadTable.RECIPIENT_ID,
ThreadTable.ARCHIVED,
ThreadTable.PINNED,
ThreadTable.EXPIRES_IN
)
.from(ThreadTable.TABLE_NAME)
.run()
//language=sql
val query = """
SELECT
${ThreadTable.TABLE_NAME}.${ThreadTable.ID},
${ThreadTable.RECIPIENT_ID},
${ThreadTable.PINNED},
${ThreadTable.READ},
${ThreadTable.ARCHIVED},
${RecipientTable.TABLE_NAME}.${RecipientTable.MESSAGE_EXPIRATION_TIME},
${RecipientTable.TABLE_NAME}.${RecipientTable.MUTE_UNTIL},
${RecipientTable.TABLE_NAME}.${RecipientTable.MENTION_SETTING}
FROM ${ThreadTable.TABLE_NAME}
LEFT OUTER JOIN ${RecipientTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = ${RecipientTable.TABLE_NAME}.${RecipientTable.ID}
"""
val cursor = readableDatabase.query(query)
return ChatIterator(cursor)
}
@@ -43,14 +49,29 @@ fun ThreadTable.clearAllDataForBackupRestore() {
}
fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId): Long? {
return writableDatabase
val threadId = writableDatabase
.insertInto(ThreadTable.TABLE_NAME)
.values(
ThreadTable.RECIPIENT_ID to recipientId.serialize(),
ThreadTable.PINNED to chat.pinnedOrder,
ThreadTable.ARCHIVED to chat.archived.toInt()
ThreadTable.ARCHIVED to chat.archived.toInt(),
ThreadTable.READ to if (chat.markedUnread) ThreadTable.ReadStatus.FORCED_UNREAD.serialize() else ThreadTable.ReadStatus.READ.serialize(),
ThreadTable.ACTIVE to 1
)
.run()
writableDatabase
.update(
RecipientTable.TABLE_NAME,
contentValuesOf(
RecipientTable.MENTION_SETTING to (if (chat.dontNotifyForMentionsIfMuted) RecipientTable.MentionSetting.DO_NOT_NOTIFY.id else RecipientTable.MentionSetting.ALWAYS_NOTIFY.id),
RecipientTable.MUTE_UNTIL to chat.muteUntilMs,
RecipientTable.MESSAGE_EXPIRATION_TIME to chat.expirationTimerMs
),
"${RecipientTable.ID} = ?",
SqlUtil.buildArgs(recipientId.toLong())
)
return threadId
}
class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
@@ -68,7 +89,10 @@ class ChatIterator(private val cursor: Cursor) : Iterator<Chat>, Closeable {
recipientId = cursor.requireLong(ThreadTable.RECIPIENT_ID),
archived = cursor.requireBoolean(ThreadTable.ARCHIVED),
pinnedOrder = cursor.requireInt(ThreadTable.PINNED),
expirationTimerMs = cursor.requireLong(ThreadTable.EXPIRES_IN)
expirationTimerMs = cursor.requireLong(RecipientTable.MESSAGE_EXPIRATION_TIME),
muteUntilMs = cursor.requireLong(RecipientTable.MUTE_UNTIL),
markedUnread = ThreadTable.ReadStatus.deserialize(cursor.requireInt(ThreadTable.READ)) == ThreadTable.ReadStatus.FORCED_UNREAD,
dontNotifyForMentionsIfMuted = RecipientTable.MentionSetting.DO_NOT_NOTIFY.id == cursor.requireInt(RecipientTable.MENTION_SETTING)
)
}
@@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -27,6 +28,7 @@ import org.whispersystems.signalservice.api.push.UsernameLinkComponents
import org.whispersystems.signalservice.api.storage.StorageRecordProtoUtil.defaultAccountRecord
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import org.whispersystems.signalservice.api.util.UuidUtil
import kotlin.jvm.optionals.getOrNull
object AccountDataProcessor {
@@ -46,27 +48,27 @@ object AccountDataProcessor {
familyName = self.profileName.familyName,
avatarUrlPath = self.profileAvatar ?: "",
subscriptionManuallyCancelled = SignalStore.donationsValues().isUserManuallyCancelled(),
username = SignalStore.account().username,
username = self.username.getOrNull(),
subscriberId = subscriber?.subscriberId?.bytes?.toByteString() ?: defaultAccountRecord.subscriberId,
subscriberCurrencyCode = subscriber?.currencyCode ?: defaultAccountRecord.subscriberCurrencyCode,
accountSettings = AccountData.AccountSettings(
storyViewReceiptsEnabled = SignalStore.storyValues().viewedReceiptsEnabled,
noteToSelfMarkedUnread = record != null && record.syncExtras.isForcedUnread,
typingIndicators = TextSecurePreferences.isTypingIndicatorsEnabled(context),
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
linkPreviews = SignalStore.settings().isLinkPreviewsEnabled,
notDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isUnlisted,
notDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode == PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE,
phoneNumberSharingMode = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
preferContactAvatars = SignalStore.settings().isPreferSystemContactPhotos,
universalExpireTimer = SignalStore.settings().universalExpireTimer,
preferredReactionEmoji = SignalStore.emojiValues().reactions,
preferredReactionEmoji = SignalStore.emojiValues().rawReactions,
storiesDisabled = SignalStore.storyValues().isFeatureDisabled,
hasViewedOnboardingStory = SignalStore.storyValues().userHasViewedOnboardingStory,
hasSetMyStoriesPrivacy = SignalStore.storyValues().userHasBeenNotifiedAboutStories,
keepMutedChatsArchived = SignalStore.settings().shouldKeepMutedChatsArchived(),
displayBadgesOnProfile = SignalStore.donationsValues().getDisplayBadgesOnProfile(),
hasSeenGroupStoryEducationSheet = SignalStore.storyValues().userHasSeenGroupStoryEducationSheet
hasSeenGroupStoryEducationSheet = SignalStore.storyValues().userHasSeenGroupStoryEducationSheet,
hasCompletedUsernameOnboarding = SignalStore.uiHints().hasCompletedUsernameOnboarding()
)
)
)
@@ -86,7 +88,7 @@ object AccountDataProcessor {
TextSecurePreferences.setTypingIndicatorsEnabled(context, settings.typingIndicators)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, settings.sealedSenderIndicators)
SignalStore.settings().isLinkPreviewsEnabled = settings.linkPreviews
SignalStore.phoneNumberPrivacy().phoneNumberListingMode = if (settings.notDiscoverableByPhoneNumber) PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED else PhoneNumberPrivacyValues.PhoneNumberListingMode.LISTED
SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = if (settings.notDiscoverableByPhoneNumber) PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberDiscoverabilityMode.DISCOVERABLE
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = settings.phoneNumberSharingMode.toLocalPhoneNumberMode()
SignalStore.settings().isPreferSystemContactPhotos = settings.preferContactAvatars
SignalStore.settings().universalExpireTimer = settings.universalExpireTimer
@@ -121,6 +123,14 @@ object AccountDataProcessor {
)
SignalStore.misc().usernameQrCodeColorScheme = accountData.usernameLink.color.toLocalUsernameColor()
}
if (settings.preferredReactionEmoji.isNotEmpty()) {
SignalStore.emojiValues().reactions = settings.preferredReactionEmoji
}
if (settings.hasCompletedUsernameOnboarding) {
SignalStore.uiHints().setHasCompletedUsernameOnboarding(true)
}
}
SignalDatabase.runPostSuccessfulTransaction { ProfileUtil.handleSelfProfileKeyChange() }
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.database.getThreadsForBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreFromBackup
import org.thoughtcrime.securesms.backup.v2.proto.Chat
@@ -18,10 +19,15 @@ import org.thoughtcrime.securesms.recipients.RecipientId
object ChatBackupProcessor {
val TAG = Log.tag(ChatBackupProcessor::class.java)
fun export(emitter: BackupFrameEmitter) {
fun export(exportState: ExportState, emitter: BackupFrameEmitter) {
SignalDatabase.threads.getThreadsForBackup().use { reader ->
for (chat in reader) {
emitter.emit(Frame(chat = chat))
if (exportState.recipientIds.contains(chat.recipientId)) {
exportState.threadIds.add(chat.id)
emitter.emit(Frame(chat = chat))
} else {
Log.w(TAG, "dropping thread for deleted recipient ${chat.recipientId}")
}
}
}
}
@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
import org.thoughtcrime.securesms.backup.v2.database.createChatItemInserter
import org.thoughtcrime.securesms.backup.v2.database.getMessagesForBackup
@@ -17,10 +18,12 @@ import org.thoughtcrime.securesms.database.SignalDatabase
object ChatItemBackupProcessor {
val TAG = Log.tag(ChatItemBackupProcessor::class.java)
fun export(emitter: BackupFrameEmitter) {
SignalDatabase.messages.getMessagesForBackup().use { chatItems ->
fun export(exportState: ExportState, emitter: BackupFrameEmitter) {
SignalDatabase.messages.getMessagesForBackup(exportState.backupTime).use { chatItems ->
for (chatItem in chatItems) {
emitter.emit(Frame(chatItem = chatItem))
if (exportState.threadIds.contains(chatItem.chatId)) {
emitter.emit(Frame(chatItem = chatItem))
}
}
}
}
@@ -7,13 +7,17 @@ package org.thoughtcrime.securesms.backup.v2.processor
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.database.BackupRecipient
import org.thoughtcrime.securesms.backup.v2.database.getAllForBackup
import org.thoughtcrime.securesms.backup.v2.database.getContactsForBackup
import org.thoughtcrime.securesms.backup.v2.database.getGroupsForBackup
import org.thoughtcrime.securesms.backup.v2.database.restoreRecipientFromBackup
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.proto.ReleaseNotes
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
typealias BackupRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
@@ -22,12 +26,24 @@ object RecipientBackupProcessor {
val TAG = Log.tag(RecipientBackupProcessor::class.java)
fun export(emitter: BackupFrameEmitter) {
fun export(state: ExportState, emitter: BackupFrameEmitter) {
val selfId = Recipient.self().id.toLong()
val releaseChannelId = SignalStore.releaseChannelValues().releaseChannelRecipientId
if (releaseChannelId != null) {
emitter.emit(
Frame(
recipient = BackupRecipient(
id = releaseChannelId.toLong(),
releaseNotes = ReleaseNotes()
)
)
)
}
SignalDatabase.recipients.getContactsForBackup(selfId).use { reader ->
for (backupRecipient in reader) {
if (backupRecipient != null) {
state.recipientIds.add(backupRecipient.id)
emitter.emit(Frame(recipient = backupRecipient))
}
}
@@ -35,11 +51,13 @@ object RecipientBackupProcessor {
SignalDatabase.recipients.getGroupsForBackup().use { reader ->
for (backupRecipient in reader) {
state.recipientIds.add(backupRecipient.id)
emitter.emit(Frame(recipient = backupRecipient))
}
}
SignalDatabase.distributionLists.getAllForBackup().forEach {
state.recipientIds.add(it.id)
emitter.emit(Frame(recipient = it))
}
}
@@ -5,8 +5,10 @@
package org.thoughtcrime.securesms.backup.v2.stream
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.Frame
interface BackupExportWriter : AutoCloseable {
fun write(header: BackupInfo)
fun write(frame: Frame)
}

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