Compare commits

..

221 Commits

Author SHA1 Message Date
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
Alex Hart c725a2fabb Bump version to 6.44.1 2024-01-12 16:40:49 -04:00
Alex Hart 1ddececa16 Updated baseline profile. 2024-01-12 16:36:13 -04:00
Alex Hart 4e2e6cd83e Update translations and other static files. 2024-01-12 16:33:05 -04:00
Alex Hart efcfe2dafc Fix call disposition update issue. 2024-01-12 15:59:45 -04:00
Alex Hart b8ddb9e673 Fix several story issues around bad manifest interactions. 2024-01-12 10:11:06 -04:00
Alex Hart a1f19e9d8a Bump version to 6.44.0 2024-01-11 17:13:48 -04:00
Alex Hart 5464edf639 Updated baseline profile. 2024-01-11 17:13:27 -04:00
Alex Hart 179c3790e6 Update translations and other static files. 2024-01-11 17:10:48 -04:00
Greyson Parrelli cfae9753a3 Cleanup 2024-01-11 15:56:52 -05:00
Greyson Parrelli 61a4a3b322 Add support for restoring usernames post-registration. 2024-01-11 15:56:51 -05:00
Nicholas Tinsley c16bf65a80 Change audio tone for raise hand. 2024-01-11 14:09:08 -05:00
Greyson Parrelli 16ea1912b4 fixup! Combine username confirmation and link creation into a single operation. 2024-01-11 12:29:40 -05:00
Greyson Parrelli 54012cb33a Combine username confirmation and link creation into a single operation. 2024-01-11 12:00:44 -05:00
Alex Hart 459c5c0a55 Username UX polish. 2024-01-11 11:32:38 -04:00
Greyson Parrelli 4216b56443 Update types of messages we backup. 2024-01-11 10:26:05 -05:00
Alex Hart d7b79314d9 Fix crash if donation error dialog is dismissed after fragment disappears. 2024-01-11 11:09:37 -04:00
Cody Henthorne a340b13f65 Fix group call continuing to ring after accepted on another device. 2024-01-11 10:01:27 -05:00
Alex Hart 72f6b15dba Hide header decorations when no subtitle or description is set. 2024-01-11 09:48:27 -04:00
Alex Hart 64dbb77e63 Fix clipping and padding on about sheet. 2024-01-10 17:01:56 -04:00
Alex Hart af10b0e4f6 Clip avatar click indication to avatar circle. 2024-01-10 16:49:54 -04:00
Cody Henthorne 6f15c16a42 Add notification profile specific events for missed calls. 2024-01-10 15:30:45 -05:00
Alex Hart 86158027d7 Fix conversation header margins on thinner screens. 2024-01-10 16:25:26 -04:00
Greyson Parrelli 50369890f7 Refactor username state to use Username models. 2024-01-10 14:57:31 -05:00
Cody Henthorne b8dea25aef Fix loss of formatted text on copy. 2024-01-10 14:08:02 -05:00
Cody Henthorne 64e9324aa0 Default new notification profiles to allow calls. 2024-01-10 11:53:08 -05:00
Cody Henthorne 20f8c69b07 Add donate_friend remote megaphone action. 2024-01-10 11:52:55 -05:00
Nicholas Tinsley dd1a15c249 fixup! Refactor video testapp. 2024-01-10 11:52:20 -05:00
Nicholas Tinsley 8b24498fa7 Refactor video testapp. 2024-01-10 11:50:54 -05:00
Nicholas Tinsley 3673fa4908 Null safety for TransformProperties during attachment compression. 2024-01-09 16:59:20 -05:00
Nicholas Tinsley 960c1df5e7 Apply faststart to videos transcoded using Streams. 2024-01-09 16:59:20 -05:00
Greyson Parrelli 8c3c7c18ad Fix backup restore issue with new attachment table name. 2024-01-09 16:17:14 -05:00
Greyson Parrelli b96a5af133 Fix issue when opening view-once messages. 2024-01-09 15:24:47 -05:00
Alex Hart d0d4008100 Add cleanup job for group ringing. 2024-01-09 13:40:50 -04:00
Alex Hart 17a6fcafa1 Add ability to set custom username discriminators. 2024-01-09 11:37:39 -04:00
Bernie Dolan fb75440769 Update payments to 6.0.1 2024-01-09 11:12:48 -04:00
Greyson Parrelli fe39b5e4e2 Clean up AttachmentTable schema. 2024-01-09 11:12:48 -04:00
Alex Hart 62b142cdeb Add new state transitions for group call disposition. 2024-01-09 11:12:48 -04:00
Nicholas Tinsley ffce7213b4 fixup! Fix width of attachment download status text. 2024-01-09 11:12:48 -04:00
Nicholas Tinsley 4205934806 fixup! Fix width of attachment download status text. 2024-01-09 11:12:48 -04:00
Nicholas Tinsley 7aab86643a Fix width of attachment download status text. 2024-01-09 11:12:48 -04:00
Alex Hart 1bb0c55d88 Cleanup unused imports in AttachmentTable. 2024-01-09 11:12:48 -04:00
Jim Gustafson d22ac9ee00 Update to RingRTC v2.36.0 2024-01-09 11:12:48 -04:00
Greyson Parrelli 80a7db2511 Fix crash when sending trimmed videos. 2024-01-09 11:12:48 -04:00
Nicholas Tinsley e0fb102572 Prevent back gesture during video trimming. 2024-01-09 11:12:48 -04:00
Cody Henthorne 8d1a16dcd6 Remove story references from multi-recipient saftey number change sheet. 2024-01-09 11:12:48 -04:00
Cody Henthorne 0b4bbd5db2 Fix unread decorator position when read follow unread. 2024-01-09 11:12:48 -04:00
Jim Gustafson 78b714e019 Remove legacy call message fields 2024-01-09 11:12:48 -04:00
Nicholas Tinsley 5022d81d9a Allow canceling media attachment send. 2024-01-09 11:12:48 -04:00
Nicholas Tinsley deacf28d77 Make entire video preview file size bubble clickable. 2024-01-09 11:12:48 -04:00
Cody Henthorne 5e8d324860 Fix large balance issues. 2024-01-09 11:12:48 -04:00
Greyson Parrelli 3554f82ea3 Convert AttachmentTable and models to kotlin. 2024-01-09 11:12:48 -04:00
1196 changed files with 35371 additions and 30038 deletions
+20 -27
View File
@@ -21,8 +21,8 @@ plugins {
apply(from = "static-ips.gradle.kts") apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1373 val canonicalVersionCode = 1389
val canonicalVersionName = "6.43.2" val canonicalVersionName = "6.47.0"
val postFixSize = 100 val postFixSize = 100
val abiPostFix: Map<String, Int> = mapOf( val abiPostFix: Map<String, Int> = mapOf(
@@ -98,7 +98,6 @@ android {
kotlinOptions { kotlinOptions {
jvmTarget = signalKotlinJvmTarget jvmTarget = signalKotlinJvmTarget
freeCompilerArgs = listOf("-Xallow-result-return-type")
} }
keystores["debug"]?.let { properties -> keystores["debug"]?.let { properties ->
@@ -164,8 +163,8 @@ android {
versionCode = canonicalVersionCode * postFixSize versionCode = canonicalVersionCode * postFixSize
versionName = canonicalVersionName versionName = canonicalVersionName
minSdkVersion(signalMinSdkVersion) minSdk = signalMinSdkVersion
targetSdkVersion(signalTargetSdkVersion) targetSdk = signalTargetSdkVersion
multiDexEnabled = true multiDexEnabled = true
@@ -415,9 +414,7 @@ android {
} }
applicationVariants.all { applicationVariants.all {
val variant = this outputs
variant.outputs
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl } .map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
.forEach { output -> .forEach { output ->
if (output.baseName.contains("nightly")) { if (output.baseName.contains("nightly")) {
@@ -430,10 +427,10 @@ android {
output.versionNameOverride = tag output.versionNameOverride = tag
output.outputFileName = output.outputFileName.replace(".apk", "-${output.versionNameOverride}.apk") output.outputFileName = output.outputFileName.replace(".apk", "-${output.versionNameOverride}.apk")
} else { } else {
output.outputFileName = output.outputFileName.replace(".apk", "-${variant.versionName}.apk") output.outputFileName = output.outputFileName.replace(".apk", "-$versionName.apk")
} }
} else { } 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 abiName: String = output.getFilter("ABI") ?: "universal"
val postFix: Int = abiPostFix[abiName]!! val postFix: Int = abiPostFix[abiName]!!
@@ -447,25 +444,20 @@ android {
} }
} }
android.variantFilter { androidComponents {
val distribution: String = flavors[0].name beforeVariants { variant ->
val environment: String = flavors[1].name variant.enable = variant.name in selectableVariants
val buildType: String = buildType.name
val fullName: String = distribution + environment.capitalize() + buildType.capitalize()
if (!selectableVariants.contains(fullName)) {
ignore = true
} }
} }
android.buildTypes.forEach { val releaseDir = "$projectDir/src/release/java"
val path: String = if (it.name == "release") { val debugDir = "$projectDir/src/debug/java"
"$projectDir/src/release/java"
} else {
"$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 +476,8 @@ dependencies {
implementation(project(":donations")) implementation(project(":donations"))
implementation(project(":contacts")) implementation(project(":contacts"))
implementation(project(":qr")) implementation(project(":qr"))
implementation(project(":sms-exporter"))
implementation(project(":sticky-header-grid")) implementation(project(":sticky-header-grid"))
implementation(project(":photoview")) implementation(project(":photoview"))
implementation(project(":glide-webp"))
implementation(project(":core-ui")) implementation(project(":core-ui"))
implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.fragment.ktx)
@@ -507,16 +497,19 @@ dependencies {
implementation(libs.androidx.exifinterface) implementation(libs.androidx.exifinterface)
implementation(libs.androidx.compose.rxjava3) implementation(libs.androidx.compose.rxjava3)
implementation(libs.androidx.compose.runtime.livedata) implementation(libs.androidx.compose.runtime.livedata)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.constraintlayout) implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.multidex) implementation(libs.androidx.multidex)
implementation(libs.androidx.navigation.fragment.ktx) implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx) implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.livedata.ktx) implementation(libs.androidx.lifecycle.livedata.ktx)
implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.lifecycle.process)
implementation(libs.androidx.lifecycle.viewmodel.savedstate) implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.lifecycle.common.java8) implementation(libs.androidx.lifecycle.common.java8)
implementation(libs.androidx.lifecycle.reactivestreams.ktx) implementation(libs.androidx.lifecycle.reactivestreams.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.camera.core) implementation(libs.androidx.camera.core)
implementation(libs.androidx.camera.camera2) implementation(libs.androidx.camera.camera2)
implementation(libs.androidx.camera.lifecycle) implementation(libs.androidx.camera.lifecycle)
@@ -255,7 +255,7 @@ class BackupTest {
SignalStore.donationsValues().setSubscriber(Subscriber(SubscriberId.generate(), "USD")) SignalStore.donationsValues().setSubscriber(Subscriber(SubscriberId.generate(), "USD"))
SignalStore.donationsValues().setDisplayBadgesOnProfile(false) SignalStore.donationsValues().setDisplayBadgesOnProfile(false)
SignalStore.phoneNumberPrivacy().phoneNumberListingMode = PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY
SignalStore.settings().isLinkPreviewsEnabled = false SignalStore.settings().isLinkPreviewsEnabled = false
@@ -8,6 +8,7 @@ package org.thoughtcrime.securesms.conversation.v2.items
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.bumptech.glide.RequestManager
import io.mockk.mockk import io.mockk.mockk
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Rule import org.junit.Rule
@@ -29,7 +30,6 @@ import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator import org.thoughtcrime.securesms.stickers.StickerLocator
@@ -209,8 +209,9 @@ class V2ConversationItemShapeTest {
override val selectedItems: Set<MultiselectPart> = emptySet() override val selectedItems: Set<MultiselectPart> = emptySet()
override val isMessageRequestAccepted: Boolean = true override val isMessageRequestAccepted: Boolean = true
override val searchQuery: String? = null override val searchQuery: String? = null
override val glideRequests: GlideRequests = mockk() override val requestManager: RequestManager = mockk()
override val isParentInScroll: Boolean = false override val isParentInScroll: Boolean = false
override fun getChatColorsData(): ChatColorsDrawable.ChatColorsData = ChatColorsDrawable.ChatColorsData(null, null)
override fun onStartExpirationTimeout(messageRecord: MessageRecord) = Unit override fun onStartExpirationTimeout(messageRecord: MessageRecord) = Unit
@@ -321,5 +322,11 @@ class V2ConversationItemShapeTest {
override fun onItemClick(item: MultiselectPart?) = Unit override fun onItemClick(item: MultiselectPart?) = Unit
override fun onItemLongClick(itemView: View?, 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
} }
} }
@@ -61,8 +61,8 @@ class AttachmentTableTest {
false false
) )
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA) val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA) val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
assertNotEquals(attachment1Info, attachment2Info) assertNotEquals(attachment1Info, attachment2Info)
} }
@@ -89,8 +89,8 @@ class AttachmentTableTest {
true true
) )
val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA) val attachment1Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment.attachmentId, AttachmentTable.DATA_FILE)
val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA) val attachment2Info = SignalDatabase.attachments.getAttachmentDataFileInfo(attachment2.attachmentId, AttachmentTable.DATA_FILE)
assertNotEquals(attachment1Info, attachment2Info) assertNotEquals(attachment1Info, attachment2Info)
} }
@@ -124,9 +124,9 @@ class AttachmentTableTest {
SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData), false) SignalDatabase.attachments.updateAttachmentData(standardDatabaseAttachment, createMediaStream(compressedData), false)
// THEN // THEN
val previousInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(previousDatabaseAttachmentId, AttachmentTable.DATA)!! val previousInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(previousDatabaseAttachmentId, AttachmentTable.DATA_FILE)!!
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA)!! val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA)!! val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
assertNotEquals(standardInfo, highInfo) assertNotEquals(standardInfo, highInfo)
standardInfo.file assertIs previousInfo.file standardInfo.file assertIs previousInfo.file
@@ -158,9 +158,9 @@ class AttachmentTableTest {
val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload) val secondHighDatabaseAttachment = SignalDatabase.attachments.insertAttachmentForPreUpload(secondHighQualityPreUpload)
// THEN // THEN
val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA)!! val standardInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(standardDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA)!! val highInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(highDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
val secondHighInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(secondHighDatabaseAttachment.attachmentId, AttachmentTable.DATA)!! val secondHighInfo = SignalDatabase.attachments.getAttachmentDataFileInfo(secondHighDatabaseAttachment.attachmentId, AttachmentTable.DATA_FILE)!!
highInfo.file assertIsNot standardInfo.file highInfo.file assertIsNot standardInfo.file
secondHighInfo.file assertIs highInfo.file secondHighInfo.file assertIs highInfo.file
@@ -214,6 +214,175 @@ class CallTableTest {
assertEquals(CallTable.Event.JOINED, acceptedCall?.event) assertEquals(CallTable.Event.JOINED, acceptedCall?.event)
} }
@Test
fun givenAnOutgoingRingCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() {
val callId = 1L
SignalDatabase.calls.insertAcceptedGroupCall(
callId = callId,
recipientId = groupRecipientId,
direction = CallTable.Direction.OUTGOING,
timestamp = 1
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertNotNull(call)
assertEquals(CallTable.Event.OUTGOING_RING, call?.event)
SignalDatabase.calls.acceptOutgoingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event)
}
@Test
fun givenARingingCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = groupRecipientId,
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertNotNull(call)
assertEquals(CallTable.Event.RINGING, call?.event)
SignalDatabase.calls.acceptOutgoingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenAMissedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = groupRecipientId,
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.EXPIRED_REQUEST
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertNotNull(call)
assertEquals(CallTable.Event.MISSED, call?.event)
SignalDatabase.calls.acceptOutgoingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenADeclinedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = groupRecipientId,
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.DECLINED_ON_ANOTHER_DEVICE
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertNotNull(call)
assertEquals(CallTable.Event.DECLINED, call?.event)
SignalDatabase.calls.acceptOutgoingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenAnAcceptedCall_whenIAcceptedOutgoingGroupCall_thenIExpectAccepted() {
val callId = 1L
SignalDatabase.calls.insertOrUpdateGroupCallFromRingState(
ringId = callId,
groupRecipientId = groupRecipientId,
ringerRecipient = harness.others[1],
dateReceived = System.currentTimeMillis(),
ringState = CallManager.RingUpdate.REQUESTED
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertNotNull(call)
assertEquals(CallTable.Event.RINGING, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
SignalDatabase.calls.acceptOutgoingGroupCall(
SignalDatabase.calls.getCallById(callId, groupRecipientId)!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertEquals(CallTable.Event.ACCEPTED, acceptedCall?.event)
}
@Test
fun givenAGenericGroupCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = groupRecipientId,
sender = harness.others[1],
timestamp = System.currentTimeMillis(),
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertNotNull(call)
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
SignalDatabase.calls.acceptOutgoingGroupCall(
call!!
)
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event)
}
@Test
fun givenAJoinedGroupCall_whenIAcceptedOutgoingGroupCall_thenIExpectOutgoingRing() {
val era = "aaa"
val callId = CallId.fromEra(era).longValue()
SignalDatabase.calls.insertOrUpdateGroupCallFromLocalEvent(
groupRecipientId = groupRecipientId,
sender = harness.others[1],
timestamp = System.currentTimeMillis(),
peekGroupCallEraId = "aaa",
peekJoinedUuids = emptyList(),
isCallFull = false
)
val call = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertNotNull(call)
assertEquals(CallTable.Event.GENERIC_GROUP_CALL, call?.event)
SignalDatabase.calls.acceptIncomingGroupCall(
call!!
)
SignalDatabase.calls.acceptOutgoingGroupCall(SignalDatabase.calls.getCallById(callId, groupRecipientId)!!)
val acceptedCall = SignalDatabase.calls.getCallById(callId, groupRecipientId)
assertEquals(CallTable.Event.OUTGOING_RING, acceptedCall?.event)
}
@Test @Test
fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() { fun givenNoPriorCallEvent_whenIReceiveAGroupCallUpdateMessage_thenIExpectAGenericGroupCall() {
val era = "aaa" val era = "aaa"
@@ -7,7 +7,7 @@ import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test 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.readToList
import org.signal.core.util.requireLong import org.signal.core.util.requireLong
import org.signal.core.util.withinTransaction import org.signal.core.util.withinTransaction
@@ -33,8 +33,8 @@ class GroupTableTest {
fun setUp() { fun setUp() {
groupTable = SignalDatabase.groups groupTable = SignalDatabase.groups
groupTable.writableDatabase.delete(GroupTable.TABLE_NAME).run() groupTable.writableDatabase.deleteAll(GroupTable.TABLE_NAME)
groupTable.writableDatabase.delete(GroupTable.MembershipTable.TABLE_NAME).run() groupTable.writableDatabase.deleteAll(GroupTable.MembershipTable.TABLE_NAME)
} }
@Test @Test
@@ -10,7 +10,7 @@ import org.signal.core.util.forEach
import org.signal.core.util.requireLong import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString import org.signal.core.util.requireNonNullString
import org.signal.core.util.select 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.crash.CrashConfig
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.testing.assertIs import org.thoughtcrime.securesms.testing.assertIs
@@ -220,7 +220,7 @@ class LogDatabaseTest {
) )
db.writableDatabase db.writableDatabase
.update(LogDatabase.CrashTable.TABLE_NAME) .updateAll(LogDatabase.CrashTable.TABLE_NAME)
.values(LogDatabase.CrashTable.LAST_PROMPTED_AT to currentTime) .values(LogDatabase.CrashTable.LAST_PROMPTED_AT to currentTime)
.run() .run()
@@ -190,7 +190,7 @@ class RecipientTableTest {
val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A) val mainId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
val mainRecord = SignalDatabase.recipients.getRecord(mainId) 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() 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.SqlUtil
import org.signal.core.util.exists import org.signal.core.util.exists
import org.signal.core.util.orNull import org.signal.core.util.orNull
import org.signal.core.util.readToSingleBoolean
import org.signal.core.util.requireLong import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString import org.signal.core.util.requireNonNullString
import org.signal.core.util.select import org.signal.core.util.select
@@ -109,6 +110,18 @@ class RecipientTableTest_getAndPossiblyMerge {
val record = SignalDatabase.recipients.getRecord(id) val record = SignalDatabase.recipients.getRecord(id)
assertEquals(RecipientTable.RegisteredState.REGISTERED, record.registered) 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 @Test
@@ -164,6 +177,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, ACI_A) expect(E164_A, PNI_A, ACI_A)
expectNoSessionSwitchoverEvent() expectNoSessionSwitchoverEvent()
expectPniVerified()
} }
test("no match, all fields") { test("no match, all fields") {
@@ -225,6 +239,8 @@ class RecipientTableTest_getAndPossiblyMerge {
given(E164_A, PNI_A, null, pniSession = true) given(E164_A, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A, pniVerified = true) process(E164_A, PNI_A, ACI_A, pniVerified = true)
expect(E164_A, PNI_A, ACI_A) expect(E164_A, PNI_A, ACI_A)
expectPniVerified()
} }
test("e164 and aci matches, all provided, new pni") { test("e164 and aci matches, all provided, new pni") {
@@ -694,6 +710,8 @@ class RecipientTableTest_getAndPossiblyMerge {
expectDeleted() expectDeleted()
expect(E164_A, PNI_A, ACI_A) expect(E164_A, PNI_A, ACI_A)
expectPniVerified()
} }
test("merge, e164+pni & aci, pni session, pni verified") { test("merge, e164+pni & aci, pni session, pni verified") {
@@ -706,6 +724,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, ACI_A) expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A) expectThreadMergeEvent(E164_A)
expectPniVerified()
} }
test("merge, e164+pni & e164+pni+aci, change number") { test("merge, e164+pni & e164+pni+aci, change number") {
@@ -1037,6 +1056,10 @@ class RecipientTableTest_getAndPossiblyMerge {
if (!test.sessionSwitchoverExpected) { if (!test.sessionSwitchoverExpected) {
test.expectNoSessionSwitchoverEvent() test.expectNoSessionSwitchoverEvent()
} }
if (!test.pniVerifiedExpected) {
test.expectPniNotVerified()
}
} catch (e: Throwable) { } catch (e: Throwable) {
if (e.javaClass != exception) { if (e.javaClass != exception) {
val error = java.lang.AssertionError("[$name] ${e.message}") val error = java.lang.AssertionError("[$name] ${e.message}")
@@ -1056,6 +1079,7 @@ class RecipientTableTest_getAndPossiblyMerge {
var changeNumberExpected = false var changeNumberExpected = false
var threadMergeExpected = false var threadMergeExpected = false
var sessionSwitchoverExpected = false var sessionSwitchoverExpected = false
var pniVerifiedExpected = false
init { init {
// Need to delete these first to prevent foreign key crash // Need to delete these first to prevent foreign key crash
@@ -1207,6 +1231,24 @@ class RecipientTableTest_getAndPossiblyMerge {
assertNull("Unexpected thread merge event!", getLatestThreadMergeEvent(outputRecipientId)) 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 { private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert( val id: Long = SignalDatabase.rawDatabase.insert(
RecipientTable.TABLE_NAME, RecipientTable.TABLE_NAME,
@@ -290,7 +290,8 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
from = sender, from = sender,
timestamp = wallClock, timestamp = wallClock,
groupId = groupId, groupId = groupId,
groupContext = groupContext groupContext = groupContext,
serverGuid = null
) )
} }
@@ -39,7 +39,6 @@ class EditMessageSyncProcessorTest {
) )
private val IGNORE_ATTACHMENT_COLUMNS = listOf( private val IGNORE_ATTACHMENT_COLUMNS = listOf(
AttachmentTable.UNIQUE_ID,
AttachmentTable.TRANSFER_FILE AttachmentTable.TRANSFER_FILE
) )
} }
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.assertIsNotNull import org.thoughtcrime.securesms.testing.assertIsNotNull
import org.thoughtcrime.securesms.testing.assertIsNull import org.thoughtcrime.securesms.testing.assertIsNull
import org.thoughtcrime.securesms.testing.success import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.util.Usernames
import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse import org.whispersystems.signalservice.internal.push.ReserveUsernameResponse
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -58,7 +59,7 @@ class UsernameEditFragmentTest {
@Test @Test
fun testUsernameCreationInRegistration() { fun testUsernameCreationInRegistration() {
val scenario = createScenario(true) val scenario = createScenario(UsernameEditMode.REGISTRATION)
scenario.moveToState(Lifecycle.State.RESUMED) scenario.moveToState(Lifecycle.State.RESUMED)
@@ -76,7 +77,7 @@ class UsernameEditFragmentTest {
@Ignore("Flakey espresso test.") @Ignore("Flakey espresso test.")
@Test @Test
fun testUsernameCreationOutsideOfRegistration() { fun testUsernameCreationOutsideOfRegistration() {
val scenario = createScenario() val scenario = createScenario(UsernameEditMode.NORMAL)
scenario.moveToState(Lifecycle.State.RESUMED) scenario.moveToState(Lifecycle.State.RESUMED)
@@ -96,7 +97,7 @@ class UsernameEditFragmentTest {
fun testNicknameUpdateHappyPath() { fun testNicknameUpdateHappyPath() {
val nickname = "Spiderman" val nickname = "Spiderman"
val discriminator = "4578" val discriminator = "4578"
val username = "$nickname${UsernameState.DELIMITER}$discriminator" val username = "$nickname${Usernames.DELIMITER}$discriminator"
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers( InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/accounts/username/reserved") { Put("/v1/accounts/username/reserved") {
@@ -107,7 +108,7 @@ class UsernameEditFragmentTest {
} }
) )
val scenario = createScenario(isInRegistration = true) val scenario = createScenario(UsernameEditMode.REGISTRATION)
scenario.moveToState(Lifecycle.State.RESUMED) scenario.moveToState(Lifecycle.State.RESUMED)
onView(withId(R.id.username_text)).perform(typeText(nickname)) onView(withId(R.id.username_text)).perform(typeText(nickname))
@@ -131,8 +132,8 @@ class UsernameEditFragmentTest {
onView(withId(R.id.username_done_button)).check(matches(isNotEnabled())) onView(withId(R.id.username_done_button)).check(matches(isNotEnabled()))
} }
private fun createScenario(isInRegistration: Boolean = false): FragmentScenario<UsernameEditFragment> { private fun createScenario(mode: UsernameEditMode = UsernameEditMode.NORMAL): FragmentScenario<UsernameEditFragment> {
val fragmentArgs = UsernameEditFragmentArgs.Builder().setIsInRegistration(isInRegistration).build().toBundle() val fragmentArgs = UsernameEditFragmentArgs.Builder().setMode(mode).build().toBundle()
return launchFragmentInContainer( return launchFragmentInContainer(
fragmentArgs = fragmentArgs, fragmentArgs = fragmentArgs,
themeResId = R.style.Signal_DayNight_NoActionBar themeResId = R.style.Signal_DayNight_NoActionBar
@@ -33,7 +33,7 @@ class ContactRecordProcessorTest {
} }
@Test @Test
fun process_splitContact_normalSplit() { fun process_splitContact_normalSplit_twoRecords() {
// GIVEN // GIVEN
val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A) val originalId = SignalDatabase.recipients.getAndPossiblyMerge(ACI_A, PNI_A, E164_A)
setStorageId(originalId, STORAGE_ID_A) setStorageId(originalId, STORAGE_ID_A)
@@ -69,6 +69,35 @@ class ContactRecordProcessorTest {
assertNotEquals(byAci, byE164) 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 @Test
fun process_splitContact_doNotSplitIfAciRecordIsRegistered() { fun process_splitContact_doNotSplitIfAciRecordIsRegistered() {
// GIVEN // GIVEN
@@ -2,7 +2,9 @@ package org.thoughtcrime.securesms.testing
import org.junit.rules.TestWatcher import org.junit.rules.TestWatcher
import org.junit.runner.Description import org.junit.runner.Description
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.ThreadTable
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.push.ServiceId.PNI
@@ -34,7 +36,8 @@ class SignalDatabaseRule(
private fun deleteAllThreads() { private fun deleteAllThreads() {
if (deleteAllThreadsOnEachRun) { if (deleteAllThreadsOnEachRun) {
SignalDatabase.threads.clearForTests() SignalDatabase.threads.deleteAllConversations()
SignalDatabase.rawDatabase.deleteAll(ThreadTable.TABLE_NAME)
} }
} }
} }
@@ -117,7 +117,9 @@ class ConversationElementGenerator {
-1, -1,
null, null,
null, null,
0 0,
false,
null
) )
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData( val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(
@@ -13,6 +13,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.navigation.navGraphViewModels import androidx.navigation.navGraphViewModels
import com.bumptech.glide.Glide
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.kotlin.subscribeBy import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.concurrent.LifecycleDisposable 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.colors.RecyclerViewColorizer
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
import org.thoughtcrime.securesms.conversation.v2.ConversationAdapterV2 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.InMemoryMessageRecord
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord 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.groups.GroupMigrationMembershipChange
import org.thoughtcrime.securesms.linkpreview.LinkPreview import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.stickers.StickerLocator import org.thoughtcrime.securesms.stickers.StickerLocator
@@ -61,11 +62,12 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val adapter = ConversationAdapterV2( val adapter = ConversationAdapterV2(
lifecycleOwner = viewLifecycleOwner, lifecycleOwner = viewLifecycleOwner,
glideRequests = GlideApp.with(this), requestManager = Glide.with(this),
clickListener = ClickListener(), clickListener = ClickListener(),
hasWallpaper = springboardViewModel.hasWallpaper.value, hasWallpaper = springboardViewModel.hasWallpaper.value,
colorizer = Colorizer(), colorizer = Colorizer(),
startExpirationTimeout = {} startExpirationTimeout = {},
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) }
) )
if (springboardViewModel.hasWallpaper.value) { if (springboardViewModel.hasWallpaper.value) {
@@ -296,5 +298,17 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
override fun onItemLongClick(itemView: View?, item: MultiselectPart?) { override fun onItemLongClick(itemView: View?, item: MultiselectPart?) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() 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()
}
} }
} }
+23 -32
View File
@@ -28,11 +28,6 @@
<uses-permission android:name="android.permission.WRITE_CONTACTS"/> <uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" /> <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_STATE" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" /> <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
@@ -160,12 +155,6 @@
android:value=".MainActivity" /> android:value=".MainActivity" />
</activity> </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" <activity android:name=".DeviceProvisioningActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="true"> android:exported="true">
@@ -693,7 +682,12 @@
<activity android:name=".NewConversationActivity" <activity android:name=".NewConversationActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" 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:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/> android:exported="false"/>
@@ -768,6 +762,13 @@
android:windowSoftInputMode="stateAlwaysHidden" android:windowSoftInputMode="stateAlwaysHidden"
android:exported="false"/> 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 <activity
android:name=".stories.settings.StorySettingsActivity" android:name=".stories.settings.StorySettingsActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -1072,13 +1073,6 @@
android:launchMode="singleTask" android:launchMode="singleTask"
android:exported="false"/> 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" <activity android:name=".ratelimit.RecaptchaProofActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
@@ -1100,24 +1094,11 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/> android:exported="false"/>
<activity android:name=".exporter.flow.SmsExportActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity" <activity android:name=".components.settings.app.subscription.donate.DonateToSignalActivity"
android:theme="@style/Theme.Signal.DayNight.NoActionBar" android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/> android:exported="false"/>
<service
android:enabled="true"
android:name=".exporter.SignalSmsExportService"
android:foregroundServiceType="dataSync"
android:exported="false"/>
<service <service
android:enabled="true" android:enabled="true"
android:name=".service.webrtc.WebRtcCallService" android:name=".service.webrtc.WebRtcCallService"
@@ -1396,6 +1377,16 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<service android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService" android:exported="false" />
<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"/> <uses-library android:name="org.apache.http.legacy" android:required="false"/>
</application> </application>
File diff suppressed because it is too large Load Diff
@@ -24,6 +24,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread; import androidx.annotation.WorkerThread;
import androidx.multidex.MultiDexApplication; import androidx.multidex.MultiDexApplication;
import com.bumptech.glide.Glide;
import com.google.android.gms.security.ProviderInstaller; import com.google.android.gms.security.ProviderInstaller;
import org.conscrypt.ConscryptSignal; import org.conscrypt.ConscryptSignal;
@@ -57,6 +58,7 @@ import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
import org.thoughtcrime.securesms.jobs.ExternalLaunchDonationJob; import org.thoughtcrime.securesms.jobs.ExternalLaunchDonationJob;
import org.thoughtcrime.securesms.jobs.FcmRefreshJob; import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
import org.thoughtcrime.securesms.jobs.FontDownloaderJob; import org.thoughtcrime.securesms.jobs.FontDownloaderJob;
import org.thoughtcrime.securesms.jobs.GroupRingCleanupJob;
import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob; import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob; import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob; import org.thoughtcrime.securesms.jobs.PnpInitializeDevicesJob;
@@ -73,7 +75,6 @@ import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver; import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations; import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.SignalGlideComponents; import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule; import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.providers.BlobProvider; import org.thoughtcrime.securesms.providers.BlobProvider;
@@ -177,7 +178,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addBlocking("ring-rtc", this::initializeRingRtc) .addBlocking("ring-rtc", this::initializeRingRtc)
.addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents())) .addBlocking("glide", () -> SignalGlideModule.setRegisterGlideComponents(new SignalGlideComponents()))
.addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete()) .addNonBlocking(() -> RegistrationUtil.maybeMarkRegistrationComplete())
.addNonBlocking(() -> GlideApp.get(this)) .addNonBlocking(() -> Glide.get(this))
.addNonBlocking(this::cleanAvatarStorage) .addNonBlocking(this::cleanAvatarStorage)
.addNonBlocking(this::initializeRevealableMessageManager) .addNonBlocking(this::initializeRevealableMessageManager)
.addNonBlocking(this::initializePendingRetryReceiptManager) .addNonBlocking(this::initializePendingRetryReceiptManager)
@@ -214,6 +215,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved()) .addPostRender(() -> ApplicationDependencies.getExoPlayerPool().getPoolStats().getMaxUnreserved())
.addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp()) .addPostRender(() -> ApplicationDependencies.getRecipientCache().warmUp())
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary) .addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
.addPostRender(GroupRingCleanupJob::enqueue)
.execute(); .execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms"); Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -18,6 +18,7 @@ import androidx.appcompat.widget.Toolbar;
import androidx.core.app.ActivityOptionsCompat; import androidx.core.app.ActivityOptionsCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.engine.GlideException; 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.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FullscreenHelper; import org.thoughtcrime.securesms.util.FullscreenHelper;
@@ -96,7 +96,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
Resources resources = this.getResources(); Resources resources = this.getResources();
GlideApp.with(this) Glide.with(this)
.asBitmap() .asBitmap()
.load(contactPhoto) .load(contactPhoto)
.fallback(fallbackPhoto.asCallCard(this)) .fallback(fallbackPhoto.asCallCard(this))
@@ -8,6 +8,8 @@ import androidx.annotation.Nullable;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import com.bumptech.glide.RequestManager;
import org.signal.ringrtc.CallLinkRootKey; import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState; import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
import org.thoughtcrime.securesms.contactshare.Contact; 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.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory; import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.stickers.StickerLocator; import org.thoughtcrime.securesms.stickers.StickerLocator;
@@ -41,7 +42,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
@NonNull ConversationMessage messageRecord, @NonNull ConversationMessage messageRecord,
@NonNull Optional<MessageRecord> previousMessageRecord, @NonNull Optional<MessageRecord> previousMessageRecord,
@NonNull Optional<MessageRecord> nextMessageRecord, @NonNull Optional<MessageRecord> nextMessageRecord,
@NonNull GlideRequests glideRequests, @NonNull RequestManager requestManager,
@NonNull Locale locale, @NonNull Locale locale,
@NonNull Set<MultiselectPart> batchSelected, @NonNull Set<MultiselectPart> batchSelected,
@NonNull Recipient recipients, @NonNull Recipient recipients,
@@ -122,5 +123,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord); void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks); void onShowGroupDescriptionClicked(@NonNull String groupName, @NonNull String description, boolean shouldLinkifyWebLinks);
void onJoinCallLink(@NonNull CallLinkRootKey callLinkRootKey); 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.annotation.NonNull;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet; import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
@@ -14,7 +15,7 @@ public interface BindableConversationListItem extends Unbindable {
void bind(@NonNull LifecycleOwner lifecycleOwner, void bind(@NonNull LifecycleOwner lifecycleOwner,
@NonNull ThreadRecord thread, @NonNull ThreadRecord thread,
@NonNull GlideRequests glideRequests, @NonNull Locale locale, @NonNull RequestManager requestManager, @NonNull Locale locale,
@NonNull Set<Long> typingThreads, @NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations); @NonNull ConversationSet selectedConversations);
@@ -11,16 +11,36 @@ import androidx.lifecycle.Lifecycle;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; 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.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. * This should be used whenever we want to prompt the user to block/unblock a recipient.
*/ */
public final class BlockUnblockDialog { 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, public static void showBlockFor(@NonNull Context context,
@NonNull Lifecycle lifecycle, @NonNull Lifecycle lifecycle,
@@ -137,4 +157,37 @@ public final class BlockUnblockDialog {
return builder; 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.appcompat.widget.Toolbar;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.signal.core.util.DimensionUnit;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.ContactFilterView; import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode; import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery; import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DisplayMetricsUtil;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme; import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ServiceUtil; import org.thoughtcrime.securesms.util.ServiceUtil;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@@ -99,6 +102,10 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
private void initializeContactFilterView() { private void initializeContactFilterView() {
this.contactFilterView = findViewById(R.id.contact_filter_edit_text); this.contactFilterView = findViewById(R.id.contact_filter_edit_text);
if (getResources().getDisplayMetrics().heightPixels >= DimensionUnit.DP.toPixels(600) || !FeatureFlags.usernames()) {
this.contactFilterView.focusAndShowKeyboard();
}
} }
private void initializeToolbar() { 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(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(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(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> { class NewGroupModel : MappingModel<NewGroupModel> {
@@ -44,6 +46,16 @@ class ContactSelectionListAdapter(
override fun areContentsTheSame(newItem: RefreshContactsModel): Boolean = true 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> { class MoreHeaderModel : MappingModel<MoreHeaderModel> {
override fun areItemsTheSame(newItem: MoreHeaderModel): Boolean = true 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 { class ArbitraryRepository : org.thoughtcrime.securesms.contacts.paged.ArbitraryRepository {
enum class ArbitraryRow(val code: String) { enum class ArbitraryRow(val code: String) {
NEW_GROUP("new-group"), NEW_GROUP("new-group"),
INVITE_TO_SIGNAL("invite-to-signal"), INVITE_TO_SIGNAL("invite-to-signal"),
MORE_HEADING("more-heading"), 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 { companion object {
fun fromCode(code: String) = values().first { it.code == code } fun fromCode(code: String) = values().first { it.code == code }
@@ -120,6 +152,8 @@ class ContactSelectionListAdapter(
ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel() ArbitraryRow.INVITE_TO_SIGNAL -> InviteToSignalModel()
ArbitraryRow.MORE_HEADING -> MoreHeaderModel() ArbitraryRow.MORE_HEADING -> MoreHeaderModel()
ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel() ArbitraryRow.REFRESH_CONTACTS -> RefreshContactsModel()
ArbitraryRow.FIND_BY_PHONE_NUMBER -> FindByPhoneNumberModel()
ArbitraryRow.FIND_BY_USERNAME -> FindByUsernameModel()
} }
} }
} }
@@ -128,5 +162,7 @@ class ContactSelectionListAdapter(
fun onNewGroupClicked() fun onNewGroupClicked()
fun onInviteToSignalClicked() fun onInviteToSignalClicked()
fun onRefreshContactsClicked() 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.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CommunicationActions; import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.UsernameUtil; import org.thoughtcrime.securesms.util.UsernameUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
@@ -141,6 +142,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
private ContactSearchMediator contactSearchMediator; private ContactSearchMediator contactSearchMediator;
@Nullable private NewConversationCallback newConversationCallback; @Nullable private NewConversationCallback newConversationCallback;
@Nullable private FindByCallback findByCallback;
@Nullable private NewCallCallback newCallCallback; @Nullable private NewCallCallback newCallCallback;
@Nullable private ScrollCallback scrollCallback; @Nullable private ScrollCallback scrollCallback;
@Nullable private OnItemLongClickListener onItemLongClickListener; @Nullable private OnItemLongClickListener onItemLongClickListener;
@@ -161,6 +163,10 @@ public final class ContactSelectionListFragment extends LoggingFragment {
newConversationCallback = (NewConversationCallback) context; newConversationCallback = (NewConversationCallback) context;
} }
if (context instanceof FindByCallback) {
findByCallback = (FindByCallback) context;
}
if (context instanceof NewCallCallback) { if (context instanceof NewCallCallback) {
newCallCallback = (NewCallCallback) context; newCallCallback = (NewCallCallback) context;
} }
@@ -379,6 +385,16 @@ public final class ContactSelectionListFragment extends LoggingFragment {
newConversationCallback.onNewGroup(false); newConversationCallback.onNewGroup(false);
} }
@Override
public void onFindByPhoneNumberClicked() {
findByCallback.onFindByPhoneNumber();
}
@Override
public void onFindByUsernameClicked() {
findByCallback.onFindByUsername();
}
@Override @Override
public void onInviteToSignalClicked() { public void onInviteToSignalClicked() {
if (newConversationCallback != null) { 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 { private class ListClickListener {
public void onItemClick(ContactSearchKey contact) { public void onItemClick(ContactSearchKey contact) {
boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey; boolean isUnknown = contact instanceof ContactSearchKey.UnknownRecipientKey;
@@ -874,6 +894,12 @@ public final class ContactSelectionListFragment extends LoggingFragment {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode()); builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.NEW_GROUP.getCode());
} }
if (findByCallback != null && FeatureFlags.usernames()) {
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_USERNAME.getCode());
}
builder.arbitrary(ContactSelectionListAdapter.ArbitraryRepository.ArbitraryRow.FIND_BY_PHONE_NUMBER.getCode());
if (transportType != null) { if (transportType != null) {
if (!hasQuery && includeRecents) { if (!hasQuery && includeRecents) {
builder.addSection(new ContactSearchConfiguration.Section.Recents( builder.addSection(new ContactSearchConfiguration.Section.Recents(
@@ -891,7 +917,7 @@ public final class ContactSelectionListFragment extends LoggingFragment {
builder.addSection(new ContactSearchConfiguration.Section.Individuals( builder.addSection(new ContactSearchConfiguration.Section.Individuals(
includeSelf, includeSelf,
transportType, transportType,
newCallCallback == null, newCallCallback == null && findByCallback == null,
null, null,
!hideLetterHeaders() !hideLetterHeaders()
)); ));
@@ -1011,6 +1037,12 @@ public final class ContactSelectionListFragment extends LoggingFragment {
void onNewGroup(boolean forceV1); void onNewGroup(boolean forceV1);
} }
public interface FindByCallback {
void onFindByUsername();
void onFindByPhoneNumber();
}
public interface NewCallCallback { public interface NewCallCallback {
void onInvite(); void onInvite();
} }
@@ -38,7 +38,6 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
.setOnDismissListener(dialog13 -> finish()) .setOnDismissListener(dialog13 -> finish())
.create(); .create();
dialog.setIcon(getResources().getDrawable(R.drawable.icon_dialog));
dialog.show(); dialog.show();
} }
} }
@@ -17,7 +17,6 @@ import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.LifecycleDisposable; import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.logging.Log;
import org.signal.donations.StripeApi; import org.signal.donations.StripeApi;
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment; import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment;
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment; import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment;
@@ -50,6 +50,8 @@ import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientId;
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.CommunicationActions;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog; import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
@@ -70,14 +72,15 @@ import io.reactivex.rxjava3.disposables.Disposable;
* @author Moxie Marlinspike * @author Moxie Marlinspike
*/ */
public class NewConversationActivity extends ContactSelectionActivity public class NewConversationActivity extends ContactSelectionActivity
implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener implements ContactSelectionListFragment.NewConversationCallback, ContactSelectionListFragment.OnItemLongClickListener, ContactSelectionListFragment.FindByCallback
{ {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = Log.tag(NewConversationActivity.class); private static final String TAG = Log.tag(NewConversationActivity.class);
private ContactsManagementViewModel viewModel; private ContactsManagementViewModel viewModel;
private ActivityResultLauncher<Intent> contactLauncher; private ActivityResultLauncher<Intent> contactLauncher;
private ActivityResultLauncher<FindByMode> findByLauncher;
private final LifecycleDisposable disposables = new LifecycleDisposable(); private final LifecycleDisposable disposables = new LifecycleDisposable();
@@ -99,6 +102,12 @@ public class NewConversationActivity extends ContactSelectionActivity
} }
}); });
findByLauncher = registerForActivityResult(new FindByActivity.Contract(), result -> {
if (result != null) {
launch(result);
}
});
viewModel = new ViewModelProvider(this, factory).get(ContactsManagementViewModel.class); viewModel = new ViewModelProvider(this, factory).get(ContactsManagementViewModel.class);
} }
@@ -163,7 +172,12 @@ public class NewConversationActivity extends ContactSelectionActivity
} }
private void launch(Recipient recipient) { 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 .map(builder -> builder
.withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT)) .withDraftText(getIntent().getStringExtra(Intent.EXTRA_TEXT))
.withDataUri(getIntent().getData()) .withDataUri(getIntent().getData())
@@ -234,6 +248,16 @@ public class NewConversationActivity extends ContactSelectionActivity
finish(); finish();
} }
@Override
public void onFindByUsername() {
findByLauncher.launch(FindByMode.USERNAME);
}
@Override
public void onFindByPhoneNumber() {
findByLauncher.launch(FindByMode.PHONE_NUMBER);
}
@Override @Override
public boolean onLongClick(View anchorView, ContactSearchKey contactSearchKey, RecyclerView recyclerView) { public boolean onLongClick(View anchorView, ContactSearchKey contactSearchKey, RecyclerView recyclerView) {
RecipientId recipientId = contactSearchKey.requireRecipientSearchKey().getRecipientId(); RecipientId recipientId = contactSearchKey.requireRecipientSearchKey().getRecipientId();
@@ -374,7 +374,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
@Override @Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
Log.i(TAG, "onAuthenticationSucceeded"); 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.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() { fingerprintPrompt.animate().setInterpolator(new BounceInterpolator()).scaleX(1.1f).scaleY(1.1f).setDuration(500).setListener(new AnimationCompleteListener() {
@Override @Override
@@ -388,7 +388,7 @@ public class PassphrasePromptActivity extends PassphraseActivity {
public void onAuthenticationFailed() { public void onAuthenticationFailed() {
Log.w(TAG, "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); fingerprintPrompt.getBackground().setColorFilter(getResources().getColor(R.color.red_500), PorterDuff.Mode.SRC_IN);
TranslateAnimation shake = new TranslateAnimation(0, 30, 0, 0); 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());
}
}
@@ -765,7 +765,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 if (isFinishing()) return; // XXX Stuart added this check above, not sure why, so I'm repeating in ignorance. - moxie
new MaterialAlertDialogBuilder(this) new MaterialAlertDialogBuilder(this)
.setTitle(R.string.RedPhone_number_not_registered) .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) .setMessage(R.string.RedPhone_the_number_you_dialed_does_not_support_secure_voice)
.setCancelable(true) .setCancelable(true)
.setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL)) .setPositiveButton(R.string.RedPhone_got_it, (d, w) -> handleTerminate(event.getRecipient(), HangupMessage.Type.NORMAL))
@@ -1,319 +0,0 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.attachments;
import android.net.Uri;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.ParcelCompat;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.ParcelUtil;
import java.util.Objects;
public abstract class Attachment implements Parcelable {
@NonNull
private final String contentType;
private final int transferState;
private final long size;
@Nullable
private final String fileName;
private final int cdnNumber;
@Nullable
private final String location;
@Nullable
private final String key;
@Nullable
private final String relay;
@Nullable
private final byte[] digest;
@Nullable
private final byte[] incrementalDigest;
@Nullable
private final String fastPreflightId;
private final boolean voiceNote;
private final boolean borderless;
private final boolean videoGif;
private final int width;
private final int height;
private final boolean quote;
private final long uploadTimestamp;
private final int incrementalMacChunkSize;
@Nullable
private final String caption;
@Nullable
private final StickerLocator stickerLocator;
@Nullable
private final BlurHash blurHash;
@Nullable
private final AudioHash audioHash;
@NonNull
private final TransformProperties transformProperties;
public Attachment(@NonNull String contentType,
int transferState,
long size,
@Nullable String fileName,
int cdnNumber,
@Nullable String location,
@Nullable String key,
@Nullable String relay,
@Nullable byte[] digest,
@Nullable byte[] incrementalDigest,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean videoGif,
int width,
int height,
int incrementalMacChunkSize,
boolean quote,
long uploadTimestamp,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
this.contentType = contentType;
this.transferState = transferState;
this.size = size;
this.fileName = fileName;
this.cdnNumber = cdnNumber;
this.location = location;
this.key = key;
this.relay = relay;
this.digest = digest;
this.incrementalDigest = incrementalDigest;
this.fastPreflightId = fastPreflightId;
this.voiceNote = voiceNote;
this.borderless = borderless;
this.videoGif = videoGif;
this.width = width;
this.height = height;
this.incrementalMacChunkSize = incrementalMacChunkSize;
this.quote = quote;
this.uploadTimestamp = uploadTimestamp;
this.stickerLocator = stickerLocator;
this.caption = caption;
this.blurHash = blurHash;
this.audioHash = audioHash;
this.transformProperties = transformProperties != null ? transformProperties : TransformProperties.empty();
}
protected Attachment(Parcel in) {
this.contentType = Objects.requireNonNull(in.readString());
this.transferState = in.readInt();
this.size = in.readLong();
this.fileName = in.readString();
this.cdnNumber = in.readInt();
this.location = in.readString();
this.key = in.readString();
this.relay = in.readString();
this.digest = ParcelUtil.readByteArray(in);
this.incrementalDigest = ParcelUtil.readByteArray(in);
this.fastPreflightId = in.readString();
this.voiceNote = ParcelUtil.readBoolean(in);
this.borderless = ParcelUtil.readBoolean(in);
this.videoGif = ParcelUtil.readBoolean(in);
this.width = in.readInt();
this.height = in.readInt();
this.incrementalMacChunkSize = in.readInt();
this.quote = ParcelUtil.readBoolean(in);
this.uploadTimestamp = in.readLong();
this.stickerLocator = ParcelCompat.readParcelable(in, StickerLocator.class.getClassLoader(), StickerLocator.class);
this.caption = in.readString();
this.blurHash = ParcelCompat.readParcelable(in, BlurHash.class.getClassLoader(), BlurHash.class);
this.audioHash = ParcelCompat.readParcelable(in, AudioHash.class.getClassLoader(), AudioHash.class);
this.transformProperties = Objects.requireNonNull(ParcelCompat.readParcelable(in, TransformProperties.class.getClassLoader(), TransformProperties.class));
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
AttachmentCreator.writeSubclass(dest, this);
dest.writeString(contentType);
dest.writeInt(transferState);
dest.writeLong(size);
dest.writeString(fileName);
dest.writeInt(cdnNumber);
dest.writeString(location);
dest.writeString(key);
dest.writeString(relay);
ParcelUtil.writeByteArray(dest, digest);
ParcelUtil.writeByteArray(dest, incrementalDigest);
dest.writeString(fastPreflightId);
ParcelUtil.writeBoolean(dest, voiceNote);
ParcelUtil.writeBoolean(dest, borderless);
ParcelUtil.writeBoolean(dest, videoGif);
dest.writeInt(width);
dest.writeInt(height);
dest.writeInt(incrementalMacChunkSize);
ParcelUtil.writeBoolean(dest, quote);
dest.writeLong(uploadTimestamp);
dest.writeParcelable(stickerLocator, 0);
dest.writeString(caption);
dest.writeParcelable(blurHash, 0);
dest.writeParcelable(audioHash, 0);
dest.writeParcelable(transformProperties, 0);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<Attachment> CREATOR = AttachmentCreator.INSTANCE;
@Nullable
public abstract Uri getUri();
public abstract @Nullable Uri getPublicUri();
public int getTransferState() {
return transferState;
}
public boolean isInProgress() {
return transferState != AttachmentTable.TRANSFER_PROGRESS_DONE &&
transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED &&
transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE;
}
public boolean isPermanentlyFailed() {
return transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE;
}
public long getSize() {
return size;
}
@Nullable
public String getFileName() {
return fileName;
}
@NonNull
public String getContentType() {
return contentType;
}
public int getCdnNumber() {
return cdnNumber;
}
@Nullable
public String getLocation() {
return location;
}
@Nullable
public String getKey() {
return key;
}
@Nullable
public String getRelay() {
return relay;
}
@Nullable
public byte[] getDigest() {
return digest;
}
@Nullable
public byte[] getIncrementalDigest() {
if (incrementalDigest != null && incrementalDigest.length > 0) {
return incrementalDigest;
} else {
return null;
}
}
@Nullable
public String getFastPreflightId() {
return fastPreflightId;
}
public boolean isVoiceNote() {
return voiceNote;
}
public boolean isBorderless() {
return borderless;
}
public boolean isVideoGif() {
return videoGif;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public int getIncrementalMacChunkSize() {
return incrementalMacChunkSize;
}
public boolean isQuote() {
return quote;
}
public long getUploadTimestamp() {
return uploadTimestamp;
}
public boolean isSticker() {
return stickerLocator != null;
}
public @Nullable StickerLocator getSticker() {
return stickerLocator;
}
public @Nullable BlurHash getBlurHash() {
return blurHash;
}
public @Nullable AudioHash getAudioHash() {
return audioHash;
}
public @Nullable String getCaption() {
return caption;
}
public @NonNull TransformProperties getTransformProperties() {
return transformProperties;
}
}
@@ -0,0 +1,152 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import android.os.Parcelable
import androidx.core.os.ParcelCompat
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.ParcelUtil
/**
* Note: We have to use our own Parcelable implementation because we need to do custom stuff to preserve
* subclass information.
*/
abstract class Attachment(
@JvmField
val contentType: String,
@JvmField
val transferState: Int,
@JvmField
val size: Long,
@JvmField
val fileName: String?,
@JvmField
val cdnNumber: Int,
@JvmField
val remoteLocation: String?,
@JvmField
val remoteKey: String?,
@JvmField
val remoteDigest: ByteArray?,
@JvmField
val incrementalDigest: ByteArray?,
@JvmField
val fastPreflightId: String?,
@JvmField
val voiceNote: Boolean,
@JvmField
val borderless: Boolean,
@JvmField
val videoGif: Boolean,
@JvmField
val width: Int,
@JvmField
val height: Int,
@JvmField
val incrementalMacChunkSize: Int,
@JvmField
val quote: Boolean,
@JvmField
val uploadTimestamp: Long,
@JvmField
val caption: String?,
@JvmField
val stickerLocator: StickerLocator?,
@JvmField
val blurHash: BlurHash?,
@JvmField
val audioHash: AudioHash?,
@JvmField
val transformProperties: TransformProperties?
) : Parcelable {
abstract val uri: Uri?
abstract val publicUri: Uri?
protected constructor(parcel: Parcel) : this(
contentType = parcel.readString()!!,
transferState = parcel.readInt(),
size = parcel.readLong(),
fileName = parcel.readString(),
cdnNumber = parcel.readInt(),
remoteLocation = parcel.readString(),
remoteKey = parcel.readString(),
remoteDigest = ParcelUtil.readByteArray(parcel),
incrementalDigest = ParcelUtil.readByteArray(parcel),
fastPreflightId = parcel.readString(),
voiceNote = ParcelUtil.readBoolean(parcel),
borderless = ParcelUtil.readBoolean(parcel),
videoGif = ParcelUtil.readBoolean(parcel),
width = parcel.readInt(),
height = parcel.readInt(),
incrementalMacChunkSize = parcel.readInt(),
quote = ParcelUtil.readBoolean(parcel),
uploadTimestamp = parcel.readLong(),
caption = parcel.readString(),
stickerLocator = ParcelCompat.readParcelable(parcel, StickerLocator::class.java.classLoader, StickerLocator::class.java),
blurHash = ParcelCompat.readParcelable(parcel, BlurHash::class.java.classLoader, BlurHash::class.java),
audioHash = ParcelCompat.readParcelable(parcel, AudioHash::class.java.classLoader, AudioHash::class.java),
transformProperties = ParcelCompat.readParcelable(parcel, TransformProperties::class.java.classLoader, TransformProperties::class.java)
)
override fun writeToParcel(dest: Parcel, flags: Int) {
AttachmentCreator.writeSubclass(dest, this)
dest.writeString(contentType)
dest.writeInt(transferState)
dest.writeLong(size)
dest.writeString(fileName)
dest.writeInt(cdnNumber)
dest.writeString(remoteLocation)
dest.writeString(remoteKey)
ParcelUtil.writeByteArray(dest, remoteDigest)
ParcelUtil.writeByteArray(dest, incrementalDigest)
dest.writeString(fastPreflightId)
ParcelUtil.writeBoolean(dest, voiceNote)
ParcelUtil.writeBoolean(dest, borderless)
ParcelUtil.writeBoolean(dest, videoGif)
dest.writeInt(width)
dest.writeInt(height)
dest.writeInt(incrementalMacChunkSize)
ParcelUtil.writeBoolean(dest, quote)
dest.writeLong(uploadTimestamp)
dest.writeString(caption)
dest.writeParcelable(stickerLocator, 0)
dest.writeParcelable(blurHash, 0)
dest.writeParcelable(audioHash, 0)
dest.writeParcelable(transformProperties, 0)
}
override fun describeContents(): Int {
return 0
}
val isInProgress: Boolean
get() = transferState != AttachmentTable.TRANSFER_PROGRESS_DONE && transferState != AttachmentTable.TRANSFER_PROGRESS_FAILED && transferState != AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
val isPermanentlyFailed: Boolean
get() = transferState == AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE
val isSticker: Boolean
get() = stickerLocator != null
fun getIncrementalDigest(): ByteArray? {
return if (incrementalDigest != null && incrementalDigest.size > 0) {
incrementalDigest
} else {
null
}
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<Attachment> = AttachmentCreator
}
}
@@ -15,7 +15,6 @@ import android.os.Parcelable
object AttachmentCreator : Parcelable.Creator<Attachment> { object AttachmentCreator : Parcelable.Creator<Attachment> {
enum class Subclass(val clazz: Class<out Attachment>, val code: String) { enum class Subclass(val clazz: Class<out Attachment>, val code: String) {
DATABASE(DatabaseAttachment::class.java, "database"), DATABASE(DatabaseAttachment::class.java, "database"),
MMS_NOTIFICATION(MmsNotificationAttachment::class.java, "mms_notification"),
POINTER(PointerAttachment::class.java, "pointer"), POINTER(PointerAttachment::class.java, "pointer"),
TOMBSTONE(TombstoneAttachment::class.java, "tombstone"), TOMBSTONE(TombstoneAttachment::class.java, "tombstone"),
URI(UriAttachment::class.java, "uri") URI(UriAttachment::class.java, "uri")
@@ -32,7 +31,6 @@ object AttachmentCreator : Parcelable.Creator<Attachment> {
return when (Subclass.values().first { rawCode == it.code }) { return when (Subclass.values().first { rawCode == it.code }) {
Subclass.DATABASE -> DatabaseAttachment(source) Subclass.DATABASE -> DatabaseAttachment(source)
Subclass.MMS_NOTIFICATION -> MmsNotificationAttachment(source)
Subclass.POINTER -> PointerAttachment(source) Subclass.POINTER -> PointerAttachment(source)
Subclass.TOMBSTONE -> TombstoneAttachment(source) Subclass.TOMBSTONE -> TombstoneAttachment(source)
Subclass.URI -> UriAttachment(source) Subclass.URI -> UriAttachment(source)
@@ -1,89 +0,0 @@
package org.thoughtcrime.securesms.attachments;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.util.Util;
public class AttachmentId implements Parcelable {
@JsonProperty
private final long rowId;
@JsonProperty
private final long uniqueId;
public AttachmentId(@JsonProperty("rowId") long rowId, @JsonProperty("uniqueId") long uniqueId) {
this.rowId = rowId;
this.uniqueId = uniqueId;
}
private AttachmentId(Parcel in) {
this.rowId = in.readLong();
this.uniqueId = in.readLong();
}
public long getRowId() {
return rowId;
}
public long getUniqueId() {
return uniqueId;
}
public String[] toStrings() {
return new String[] {String.valueOf(rowId), String.valueOf(uniqueId)};
}
public @NonNull String toString() {
return "AttachmentId::(" + rowId + ", " + uniqueId + ")";
}
public boolean isValid() {
return rowId >= 0 && uniqueId >= 0;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AttachmentId attachmentId = (AttachmentId)o;
if (rowId != attachmentId.rowId) return false;
return uniqueId == attachmentId.uniqueId;
}
@Override
public int hashCode() {
return Util.hashCode(rowId, uniqueId);
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(rowId);
dest.writeLong(uniqueId);
}
public static final Creator<AttachmentId> CREATOR = new Creator<AttachmentId>() {
@Override
public AttachmentId createFromParcel(Parcel in) {
return new AttachmentId(in);
}
@Override
public AttachmentId[] newArray(int size) {
return new AttachmentId[size];
}
};
}
@@ -0,0 +1,20 @@
package org.thoughtcrime.securesms.attachments
import android.os.Parcelable
import com.fasterxml.jackson.annotation.JsonProperty
import kotlinx.parcelize.Parcelize
@Parcelize
data class AttachmentId(
@JsonProperty("rowId")
@JvmField
val id: Long
) : Parcelable {
val isValid: Boolean
get() = id >= 0
override fun toString(): String {
return "AttachmentId::$id"
}
}
@@ -1,142 +0,0 @@
package org.thoughtcrime.securesms.attachments;
import android.net.Uri;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.ParcelCompat;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ParcelUtil;
import java.util.Comparator;
public class DatabaseAttachment extends Attachment {
private final AttachmentId attachmentId;
private final long mmsId;
private final boolean hasData;
private final boolean hasThumbnail;
private final int displayOrder;
public DatabaseAttachment(AttachmentId attachmentId,
long mmsId,
boolean hasData,
boolean hasThumbnail,
String contentType,
int transferProgress,
long size,
String fileName,
int cdnNumber,
String location,
String key,
String relay,
byte[] digest,
byte[] incrementalDigest,
int incrementalMacChunkSize,
String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean videoGif,
int width,
int height,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties,
int displayOrder,
long uploadTimestamp)
{
super(contentType, transferProgress, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, incrementalMacChunkSize, quote, uploadTimestamp, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.attachmentId = attachmentId;
this.hasData = hasData;
this.hasThumbnail = hasThumbnail;
this.mmsId = mmsId;
this.displayOrder = displayOrder;
}
protected DatabaseAttachment(Parcel in) {
super(in);
this.attachmentId = ParcelCompat.readParcelable(in, AttachmentId.class.getClassLoader(), AttachmentId.class);
this.hasData = ParcelUtil.readBoolean(in);
this.hasThumbnail = ParcelUtil.readBoolean(in);
this.mmsId = in.readLong();
this.displayOrder = in.readInt();
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(attachmentId, 0);
ParcelUtil.writeBoolean(dest, hasData);
ParcelUtil.writeBoolean(dest, hasThumbnail);
dest.writeLong(mmsId);
dest.writeInt(displayOrder);
}
@Override
@Nullable
public Uri getUri() {
if (hasData || (FeatureFlags.instantVideoPlayback() && getIncrementalDigest() != null)) {
return PartAuthority.getAttachmentDataUri(attachmentId);
} else {
return null;
}
}
@Override
public @Nullable Uri getPublicUri() {
if (hasData) {
return PartAuthority.getAttachmentPublicUri(getUri());
} else {
return null;
}
}
public AttachmentId getAttachmentId() {
return attachmentId;
}
public int getDisplayOrder() {
return displayOrder;
}
@Override
public boolean equals(Object other) {
return other != null &&
other instanceof DatabaseAttachment &&
((DatabaseAttachment) other).attachmentId.equals(this.attachmentId);
}
@Override
public int hashCode() {
return attachmentId.hashCode();
}
public long getMmsId() {
return mmsId;
}
public boolean hasData() {
return hasData;
}
public boolean hasThumbnail() {
return hasThumbnail;
}
public static class DisplayOrderComparator implements Comparator<DatabaseAttachment> {
@Override
public int compare(DatabaseAttachment lhs, DatabaseAttachment rhs) {
return Integer.compare(lhs.getDisplayOrder(), rhs.getDisplayOrder());
}
}
}
@@ -0,0 +1,133 @@
package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import androidx.core.os.ParcelCompat
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.ParcelUtil
class DatabaseAttachment : Attachment {
@JvmField
val attachmentId: AttachmentId
@JvmField
val mmsId: Long
@JvmField
val hasData: Boolean
private val hasThumbnail: Boolean
val displayOrder: Int
constructor(
attachmentId: AttachmentId,
mmsId: Long,
hasData: Boolean,
hasThumbnail: Boolean,
contentType: String?,
transferProgress: Int,
size: Long,
fileName: String?,
cdnNumber: Int,
location: String?,
key: String?,
digest: ByteArray?,
incrementalDigest: ByteArray?,
incrementalMacChunkSize: Int,
fastPreflightId: String?,
voiceNote: Boolean,
borderless: Boolean,
videoGif: Boolean,
width: Int,
height: Int,
quote: Boolean,
caption: String?,
stickerLocator: StickerLocator?,
blurHash: BlurHash?,
audioHash: AudioHash?,
transformProperties: TransformProperties?,
displayOrder: Int,
uploadTimestamp: Long
) : super(
contentType = contentType!!,
transferState = transferProgress,
size = size,
fileName = fileName,
cdnNumber = cdnNumber,
remoteLocation = location,
remoteKey = key,
remoteDigest = digest,
incrementalDigest = incrementalDigest,
fastPreflightId = fastPreflightId,
voiceNote = voiceNote,
borderless = borderless,
videoGif = videoGif, width = width,
height = height,
incrementalMacChunkSize = incrementalMacChunkSize,
quote = quote,
uploadTimestamp = uploadTimestamp,
caption = caption,
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = audioHash,
transformProperties = transformProperties
) {
this.attachmentId = attachmentId
this.mmsId = mmsId
this.hasData = hasData
this.hasThumbnail = hasThumbnail
this.displayOrder = displayOrder
}
constructor(parcel: Parcel) : super(parcel) {
attachmentId = ParcelCompat.readParcelable(parcel, AttachmentId::class.java.classLoader, AttachmentId::class.java)!!
hasData = ParcelUtil.readBoolean(parcel)
hasThumbnail = ParcelUtil.readBoolean(parcel)
mmsId = parcel.readLong()
displayOrder = parcel.readInt()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeParcelable(attachmentId, 0)
ParcelUtil.writeBoolean(dest, hasData)
ParcelUtil.writeBoolean(dest, hasThumbnail)
dest.writeLong(mmsId)
dest.writeInt(displayOrder)
}
override val uri: Uri?
get() = if (hasData || FeatureFlags.instantVideoPlayback() && getIncrementalDigest() != null) {
PartAuthority.getAttachmentDataUri(attachmentId)
} else {
null
}
override val publicUri: Uri?
get() = if (hasData) {
PartAuthority.getAttachmentPublicUri(uri)
} else {
null
}
override fun equals(other: Any?): Boolean {
return other != null &&
other is DatabaseAttachment && other.attachmentId == attachmentId
}
override fun hashCode(): Int {
return attachmentId.hashCode()
}
class DisplayOrderComparator : Comparator<DatabaseAttachment> {
override fun compare(lhs: DatabaseAttachment, rhs: DatabaseAttachment): Int {
return lhs.displayOrder.compareTo(rhs.displayOrder)
}
}
}
@@ -1,45 +0,0 @@
package org.thoughtcrime.securesms.attachments;
import android.net.Uri;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.MessageTable;
public class MmsNotificationAttachment extends Attachment {
public MmsNotificationAttachment(int status, long size) {
super("application/mms", getTransferStateFromStatus(status), size, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, 0, false, 0, null, null, null, null, null);
}
protected MmsNotificationAttachment(Parcel in) {
super(in);
}
@Nullable
@Override
public Uri getUri() {
return null;
}
@Override
public @Nullable Uri getPublicUri() {
return null;
}
private static int getTransferStateFromStatus(int status) {
if (status == MessageTable.MmsStatus.DOWNLOAD_INITIALIZED ||
status == MessageTable.MmsStatus.DOWNLOAD_NO_CONNECTIVITY)
{
return AttachmentTable.TRANSFER_PROGRESS_PENDING;
} else if (status == MessageTable.MmsStatus.DOWNLOAD_CONNECTING) {
return AttachmentTable.TRANSFER_PROGRESS_STARTED;
} else {
return AttachmentTable.TRANSFER_PROGRESS_FAILED;
}
}
}
@@ -1,194 +0,0 @@
package org.thoughtcrime.securesms.attachments;
import android.net.Uri;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import org.signal.core.util.Base64;
import org.whispersystems.signalservice.api.InvalidMessageStructureException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil;
import org.whispersystems.signalservice.internal.push.DataMessage;
import java.util.LinkedList;
import java.util.List;
import java.util.Optional;
public class PointerAttachment extends Attachment {
private PointerAttachment(@NonNull String contentType,
int transferState,
long size,
@Nullable String fileName,
int cdnNumber,
@NonNull String location,
@Nullable String key,
@Nullable String relay,
@Nullable byte[] digest,
@Nullable byte[] incrementalDigest,
int incrementalMacChunkSize,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean videoGif,
int width,
int height,
long uploadTimestamp,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash)
{
super(contentType, transferState, size, fileName, cdnNumber, location, key, relay, digest, incrementalDigest, fastPreflightId, voiceNote, borderless, videoGif, width, height, incrementalMacChunkSize, false, uploadTimestamp, caption, stickerLocator, blurHash, null, null);
}
protected PointerAttachment(Parcel in) {
super(in);
}
@Nullable
@Override
public Uri getUri() {
return null;
}
@Override
public @Nullable Uri getPublicUri() {
return null;
}
public static List<Attachment> forPointers(Optional<List<SignalServiceAttachment>> pointers) {
List<Attachment> results = new LinkedList<>();
if (pointers.isPresent()) {
for (SignalServiceAttachment pointer : pointers.get()) {
Optional<Attachment> result = forPointer(Optional.of(pointer));
if (result.isPresent()) {
results.add(result.get());
}
}
}
return results;
}
public static List<Attachment> forPointers(@Nullable List<SignalServiceDataMessage.Quote.QuotedAttachment> pointers) {
List<Attachment> results = new LinkedList<>();
if (pointers != null) {
for (SignalServiceDataMessage.Quote.QuotedAttachment pointer : pointers) {
Optional<Attachment> result = forPointer(pointer);
if (result.isPresent()) {
results.add(result.get());
}
}
}
return results;
}
public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> pointer) {
return forPointer(pointer, null, null);
}
public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> pointer, @Nullable StickerLocator stickerLocator) {
return forPointer(pointer, stickerLocator, null);
}
public static Optional<Attachment> forPointer(Optional<SignalServiceAttachment> pointer, @Nullable StickerLocator stickerLocator, @Nullable String fastPreflightId) {
if (!pointer.isPresent() || !pointer.get().isPointer()) return Optional.empty();
String encodedKey = null;
if (pointer.get().asPointer().getKey() != null) {
encodedKey = Base64.encodeWithPadding(pointer.get().asPointer().getKey());
}
return Optional.of(new PointerAttachment(pointer.get().getContentType(),
AttachmentTable.TRANSFER_PROGRESS_PENDING,
pointer.get().asPointer().getSize().orElse(0),
pointer.get().asPointer().getFileName().orElse(null),
pointer.get().asPointer().getCdnNumber(),
pointer.get().asPointer().getRemoteId().toString(),
encodedKey,
null,
pointer.get().asPointer().getDigest().orElse(null),
pointer.get().asPointer().getIncrementalDigest().orElse(null),
pointer.get().asPointer().getIncrementalMacChunkSize(),
fastPreflightId,
pointer.get().asPointer().getVoiceNote(),
pointer.get().asPointer().isBorderless(),
pointer.get().asPointer().isGif(),
pointer.get().asPointer().getWidth(),
pointer.get().asPointer().getHeight(),
pointer.get().asPointer().getUploadTimestamp(),
pointer.get().asPointer().getCaption().orElse(null),
stickerLocator,
BlurHash.parseOrNull(pointer.get().asPointer().getBlurHash().orElse(null))));
}
public static Optional<Attachment> forPointer(SignalServiceDataMessage.Quote.QuotedAttachment pointer) {
SignalServiceAttachment thumbnail = pointer.getThumbnail();
return Optional.of(new PointerAttachment(pointer.getContentType(),
AttachmentTable.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
pointer.getFileName(),
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeWithPadding(thumbnail.asPointer().getKey()) : null,
null,
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
thumbnail != null ? thumbnail.asPointer().getIncrementalDigest().orElse(null) : null,
thumbnail != null ? thumbnail.asPointer().getIncrementalMacChunkSize() : 0,
null,
false,
false,
false,
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
thumbnail != null ? thumbnail.asPointer().getCaption().orElse(null) : null,
null,
null));
}
public static Optional<Attachment> forPointer(DataMessage.Quote.QuotedAttachment quotedAttachment) {
SignalServiceAttachment thumbnail;
try {
thumbnail = quotedAttachment.thumbnail != null ? AttachmentPointerUtil.createSignalAttachmentPointer(quotedAttachment.thumbnail) : null;
} catch (InvalidMessageStructureException e) {
return Optional.empty();
}
return Optional.of(new PointerAttachment(quotedAttachment.contentType,
AttachmentTable.TRANSFER_PROGRESS_PENDING,
thumbnail != null ? thumbnail.asPointer().getSize().orElse(0) : 0,
quotedAttachment.fileName,
thumbnail != null ? thumbnail.asPointer().getCdnNumber() : 0,
thumbnail != null ? thumbnail.asPointer().getRemoteId().toString() : "0",
thumbnail != null && thumbnail.asPointer().getKey() != null ? Base64.encodeWithPadding(thumbnail.asPointer().getKey()) : null,
null,
thumbnail != null ? thumbnail.asPointer().getDigest().orElse(null) : null,
thumbnail != null ? thumbnail.asPointer().getIncrementalDigest().orElse(null) : null,
thumbnail != null ? thumbnail.asPointer().getIncrementalMacChunkSize() : 0,
null,
false,
false,
false,
thumbnail != null ? thumbnail.asPointer().getWidth() : 0,
thumbnail != null ? thumbnail.asPointer().getHeight() : 0,
thumbnail != null ? thumbnail.asPointer().getUploadTimestamp() : 0,
thumbnail != null ? thumbnail.asPointer().getCaption().orElse(null) : null,
null,
null));
}
}
@@ -0,0 +1,187 @@
package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import org.signal.core.util.Base64.encodeWithPadding
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.whispersystems.signalservice.api.InvalidMessageStructureException
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil
import org.whispersystems.signalservice.internal.push.DataMessage
import java.util.Optional
class PointerAttachment : Attachment {
private constructor(
contentType: String,
transferState: Int,
size: Long,
fileName: String?,
cdnNumber: Int,
location: String,
key: String?,
digest: ByteArray?,
incrementalDigest: ByteArray?,
incrementalMacChunkSize: Int,
fastPreflightId: String?,
voiceNote: Boolean,
borderless: Boolean,
videoGif: Boolean,
width: Int,
height: Int,
uploadTimestamp: Long,
caption: String?,
stickerLocator: StickerLocator?,
blurHash: BlurHash?
) : super(
contentType = contentType,
transferState = transferState,
size = size,
fileName = fileName,
cdnNumber = cdnNumber,
remoteLocation = location,
remoteKey = key,
remoteDigest = digest,
incrementalDigest = incrementalDigest,
fastPreflightId = fastPreflightId,
voiceNote = voiceNote,
borderless = borderless,
videoGif = videoGif,
width = width,
height = height,
incrementalMacChunkSize = incrementalMacChunkSize,
quote = false,
uploadTimestamp = uploadTimestamp,
caption = caption,
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = null,
transformProperties = null
)
constructor(parcel: Parcel) : super(parcel)
override val uri: Uri? = null
override val publicUri: Uri? = null
companion object {
@JvmStatic
fun forPointers(pointers: Optional<List<SignalServiceAttachment>>): List<Attachment> {
if (!pointers.isPresent) {
return emptyList()
}
return pointers.get()
.map { forPointer(Optional.ofNullable(it)) }
.filter { it.isPresent }
.map { it.get() }
}
@JvmStatic
@JvmOverloads
fun forPointer(pointer: Optional<SignalServiceAttachment>, stickerLocator: StickerLocator? = null, fastPreflightId: String? = null): Optional<Attachment> {
if (!pointer.isPresent || !pointer.get().isPointer) {
return Optional.empty()
}
val encodedKey: String? = if (pointer.get().asPointer().key != null) {
encodeWithPadding(pointer.get().asPointer().key)
} else {
null
}
return Optional.of(
PointerAttachment(
contentType = pointer.get().contentType,
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
size = pointer.get().asPointer().size.orElse(0).toLong(),
fileName = pointer.get().asPointer().fileName.orElse(null),
cdnNumber = pointer.get().asPointer().cdnNumber,
location = pointer.get().asPointer().remoteId.toString(),
key = encodedKey,
digest = pointer.get().asPointer().digest.orElse(null),
incrementalDigest = pointer.get().asPointer().incrementalDigest.orElse(null),
incrementalMacChunkSize = pointer.get().asPointer().incrementalMacChunkSize,
fastPreflightId = fastPreflightId,
voiceNote = pointer.get().asPointer().voiceNote,
borderless = pointer.get().asPointer().isBorderless,
videoGif = pointer.get().asPointer().isGif,
width = pointer.get().asPointer().width,
height = pointer.get().asPointer().height,
uploadTimestamp = pointer.get().asPointer().uploadTimestamp,
caption = pointer.get().asPointer().caption.orElse(null),
stickerLocator = stickerLocator,
blurHash = BlurHash.parseOrNull(pointer.get().asPointer().blurHash.orElse(null))
)
)
}
fun forPointer(pointer: SignalServiceDataMessage.Quote.QuotedAttachment): Optional<Attachment> {
val thumbnail = pointer.thumbnail
return Optional.of(
PointerAttachment(
contentType = pointer.contentType,
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(),
fileName = pointer.fileName,
cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0,
location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0",
key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null,
digest = thumbnail?.asPointer()?.digest?.orElse(null),
incrementalDigest = thumbnail?.asPointer()?.incrementalDigest?.orElse(null),
incrementalMacChunkSize = thumbnail?.asPointer()?.incrementalMacChunkSize ?: 0,
fastPreflightId = null,
voiceNote = false,
borderless = false,
videoGif = false,
width = thumbnail?.asPointer()?.width ?: 0,
height = thumbnail?.asPointer()?.height ?: 0,
uploadTimestamp = thumbnail?.asPointer()?.uploadTimestamp ?: 0,
caption = thumbnail?.asPointer()?.caption?.orElse(null),
stickerLocator = null,
blurHash = null
)
)
}
fun forPointer(quotedAttachment: DataMessage.Quote.QuotedAttachment): Optional<Attachment> {
val thumbnail: SignalServiceAttachment? = try {
if (quotedAttachment.thumbnail != null) {
AttachmentPointerUtil.createSignalAttachmentPointer(quotedAttachment.thumbnail)
} else {
null
}
} catch (e: InvalidMessageStructureException) {
return Optional.empty()
}
return Optional.of(
PointerAttachment(
contentType = quotedAttachment.contentType!!,
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(),
fileName = quotedAttachment.fileName,
cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0,
location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0",
key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null,
digest = thumbnail?.asPointer()?.digest?.orElse(null),
incrementalDigest = thumbnail?.asPointer()?.incrementalDigest?.orElse(null),
incrementalMacChunkSize = thumbnail?.asPointer()?.incrementalMacChunkSize ?: 0,
fastPreflightId = null,
voiceNote = false,
borderless = false,
videoGif = false,
width = thumbnail?.asPointer()?.width ?: 0,
height = thumbnail?.asPointer()?.height ?: 0,
uploadTimestamp = thumbnail?.asPointer()?.uploadTimestamp ?: 0,
caption = thumbnail?.asPointer()?.caption?.orElse(null),
stickerLocator = null,
blurHash = null
)
)
}
}
}
@@ -1,36 +0,0 @@
package org.thoughtcrime.securesms.attachments;
import android.net.Uri;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.database.AttachmentTable;
/**
* An attachment that represents where an attachment used to be. Useful when you need to know that
* a message had an attachment and some metadata about it (like the contentType), even though the
* underlying media no longer exists. An example usecase would be view-once messages, so that we can
* quote them and know their contentType even though the media has been deleted.
*/
public class TombstoneAttachment extends Attachment {
public TombstoneAttachment(@NonNull String contentType, boolean quote) {
super(contentType, AttachmentTable.TRANSFER_PROGRESS_DONE, 0, null, 0, null, null, null, null, null, null, false, false, false, 0, 0, 0, quote, 0, null, null, null, null, null);
}
protected TombstoneAttachment(Parcel in) {
super(in);
}
@Override
public @Nullable Uri getUri() {
return null;
}
@Override
public @Nullable Uri getPublicUri() {
return null;
}
}
@@ -0,0 +1,44 @@
package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import org.thoughtcrime.securesms.database.AttachmentTable
/**
* An attachment that represents where an attachment used to be. Useful when you need to know that
* a message had an attachment and some metadata about it (like the contentType), even though the
* underlying media no longer exists. An example usecase would be view-once messages, so that we can
* quote them and know their contentType even though the media has been deleted.
*/
class TombstoneAttachment : Attachment {
constructor(contentType: String, quote: Boolean) : super(
contentType = contentType,
quote = quote,
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
size = 0,
fileName = null,
cdnNumber = 0,
remoteLocation = null,
remoteKey = null,
remoteDigest = null,
incrementalDigest = null,
fastPreflightId = null,
voiceNote = false,
borderless = false,
videoGif = false,
width = 0,
height = 0,
incrementalMacChunkSize = 0,
uploadTimestamp = 0,
caption = null,
stickerLocator = null,
blurHash = null,
audioHash = null,
transformProperties = null
)
constructor(parcel: Parcel) : super(parcel)
override val uri: Uri? = null
override val publicUri: Uri? = null
}
@@ -1,92 +0,0 @@
package org.thoughtcrime.securesms.attachments;
import android.net.Uri;
import android.os.Parcel;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.ParcelCompat;
import org.thoughtcrime.securesms.audio.AudioHash;
import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties;
import org.thoughtcrime.securesms.stickers.StickerLocator;
import java.util.Objects;
public class UriAttachment extends Attachment {
private final @NonNull Uri dataUri;
public UriAttachment(@NonNull Uri uri,
@NonNull String contentType,
int transferState,
long size,
@Nullable String fileName,
boolean voiceNote,
boolean borderless,
boolean videoGif,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
this(uri, contentType, transferState, size, 0, 0, fileName, null, voiceNote, borderless, videoGif, quote, caption, stickerLocator, blurHash, audioHash, transformProperties);
}
public UriAttachment(@NonNull Uri dataUri,
@NonNull String contentType,
int transferState,
long size,
int width,
int height,
@Nullable String fileName,
@Nullable String fastPreflightId,
boolean voiceNote,
boolean borderless,
boolean videoGif,
boolean quote,
@Nullable String caption,
@Nullable StickerLocator stickerLocator,
@Nullable BlurHash blurHash,
@Nullable AudioHash audioHash,
@Nullable TransformProperties transformProperties)
{
super(contentType, transferState, size, fileName, 0, null, null, null, null, null, fastPreflightId, voiceNote, borderless, videoGif, width, height, 0, quote, 0, caption, stickerLocator, blurHash, audioHash, transformProperties);
this.dataUri = Objects.requireNonNull(dataUri);
}
protected UriAttachment(Parcel in) {
super(in);
this.dataUri = Objects.requireNonNull(ParcelCompat.readParcelable(in, Uri.class.getClassLoader(), Uri.class));
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
dest.writeParcelable(dataUri, 0);
}
@Override
@NonNull
public Uri getUri() {
return dataUri;
}
@Override
public @Nullable Uri getPublicUri() {
return null;
}
@Override
public boolean equals(Object other) {
return other != null && other instanceof UriAttachment && ((UriAttachment) other).dataUri.equals(this.dataUri);
}
@Override
public int hashCode() {
return dataUri.hashCode();
}
}
@@ -0,0 +1,114 @@
package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import androidx.core.os.ParcelCompat
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.stickers.StickerLocator
import java.util.Objects
class UriAttachment : Attachment {
constructor(
uri: Uri,
contentType: String,
transferState: Int,
size: Long,
fileName: String?,
voiceNote: Boolean,
borderless: Boolean,
videoGif: Boolean,
quote: Boolean,
caption: String?,
stickerLocator: StickerLocator?,
blurHash: BlurHash?,
audioHash: AudioHash?,
transformProperties: TransformProperties?
) : this(
dataUri = uri,
contentType = contentType,
transferState = transferState,
size = size,
width = 0,
height = 0,
fileName = fileName,
fastPreflightId = null,
voiceNote = voiceNote,
borderless = borderless,
videoGif = videoGif,
quote = quote,
caption = caption,
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = audioHash,
transformProperties = transformProperties
)
constructor(
dataUri: Uri,
contentType: String,
transferState: Int,
size: Long,
width: Int,
height: Int,
fileName: String?,
fastPreflightId: String?,
voiceNote: Boolean,
borderless: Boolean,
videoGif: Boolean,
quote: Boolean,
caption: String?,
stickerLocator: StickerLocator?,
blurHash: BlurHash?,
audioHash: AudioHash?,
transformProperties: TransformProperties?
) : super(
contentType = contentType,
transferState = transferState,
size = size,
fileName = fileName,
cdnNumber = 0,
remoteLocation = null,
remoteKey = null,
remoteDigest = null,
incrementalDigest = null,
fastPreflightId = fastPreflightId,
voiceNote = voiceNote,
borderless = borderless,
videoGif = videoGif,
width = width,
height = height,
incrementalMacChunkSize = 0,
quote = quote,
uploadTimestamp = 0,
caption = caption,
stickerLocator = stickerLocator,
blurHash = blurHash,
audioHash = audioHash,
transformProperties = transformProperties
) {
uri = Objects.requireNonNull(dataUri)
}
constructor(parcel: Parcel) : super(parcel) {
uri = ParcelCompat.readParcelable(parcel, Uri::class.java.classLoader, Uri::class.java)!!
}
override val uri: Uri
override val publicUri: Uri? = null
override fun writeToParcel(dest: Parcel, flags: Int) {
super.writeToParcel(dest, flags)
dest.writeParcelable(uri, 0)
}
override fun equals(other: Any?): Boolean {
return other != null && other is UriAttachment && other.uri == uri
}
override fun hashCode(): Int {
return uri.hashCode()
}
}
@@ -12,7 +12,7 @@ import androidx.annotation.WorkerThread;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.media.DecryptableUriMediaInput; 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.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
@@ -8,12 +8,12 @@ import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.setPadding import androidx.core.view.setPadding
import com.airbnb.lottie.SimpleColorFilter import com.airbnb.lottie.SimpleColorFilter
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.Avatar import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarRenderer import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader 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.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
@@ -132,12 +132,12 @@ object AvatarPickerItem {
} }
is Avatar.Photo -> { is Avatar.Photo -> {
textView.visible = false 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 -> { is Avatar.Resource -> {
imageView.setPadding((imageView.width * 0.2).toInt()) imageView.setPadding((imageView.width * 0.2).toInt())
textView.visible = false textView.visible = false
GlideApp.with(imageView).clear(imageView) Glide.with(imageView).clear(imageView)
imageView.setImageResource(model.avatar.resourceId) imageView.setImageResource(model.avatar.resourceId)
imageView.colorFilter = SimpleColorFilter(model.avatar.color.foregroundColor) imageView.colorFilter = SimpleColorFilter(model.avatar.color.foregroundColor)
imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor) imageView.background.colorFilter = SimpleColorFilter(model.avatar.color.backgroundColor)
@@ -5,10 +5,10 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.content.res.use import androidx.core.content.res.use
import com.bumptech.glide.RequestManager
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.AvatarImageView import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.database.model.StoryViewState import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.util.visible import org.thoughtcrime.securesms.util.visible
@@ -76,7 +76,7 @@ class AvatarView @JvmOverloads constructor(
/** /**
* Displays Note-to-Self * 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) avatar.setAvatar(requestManager, recipient, isQuickContactEnabled)
} }
@@ -21,7 +21,7 @@ object BackupCountQueries {
@get:JvmStatic @get:JvmStatic
val attachmentCount: String = """ val attachmentCount: String = """
SELECT COUNT(*) FROM ${AttachmentTable.TABLE_NAME} SELECT COUNT(*) FROM ${AttachmentTable.TABLE_NAME}
INNER JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MMS_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID} INNER JOIN ${MessageTable.TABLE_NAME} ON ${AttachmentTable.TABLE_NAME}.${AttachmentTable.MESSAGE_ID} = ${MessageTable.TABLE_NAME}.${MessageTable.ID}
WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0 WHERE ${MessageTable.TABLE_NAME}.${MessageTable.EXPIRES_IN} <= 0 AND ${MessageTable.TABLE_NAME}.${MessageTable.VIEW_ONCE} <= 0
""" """
} }
@@ -119,8 +119,7 @@ class BackupFrameOutputStream extends FullBackupBase.BackupStream {
try { try {
write(outputStream, new BackupFrame.Builder() write(outputStream, new BackupFrame.Builder()
.attachment(new Attachment.Builder() .attachment(new Attachment.Builder()
.rowId(attachmentId.getRowId()) .rowId(attachmentId.id)
.attachmentId(attachmentId.getUniqueId())
.length(Util.toIntExact(size)) .length(Util.toIntExact(size))
.build()) .build())
.build()); .build());
@@ -44,7 +44,6 @@ import org.thoughtcrime.securesms.database.SessionTable;
import org.thoughtcrime.securesms.database.SignedPreKeyTable; import org.thoughtcrime.securesms.database.SignedPreKeyTable;
import org.thoughtcrime.securesms.database.StickerTable; import org.thoughtcrime.securesms.database.StickerTable;
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase; import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet; import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet;
import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -65,6 +64,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import okio.ByteString; import okio.ByteString;
@@ -77,6 +77,7 @@ public class FullBackupExporter extends FullBackupBase {
private static final long TABLE_RECORD_COUNT_MULTIPLIER = 3L; private static final long TABLE_RECORD_COUNT_MULTIPLIER = 3L;
private static final long IDENTITY_KEY_BACKUP_RECORD_COUNT = 2L; private static final long IDENTITY_KEY_BACKUP_RECORD_COUNT = 2L;
private static final long FINAL_MESSAGE_COUNT = 1L; private static final long FINAL_MESSAGE_COUNT = 1L;
private static final long EXPIRATION_BACKUP_THRESHOLD = TimeUnit.DAYS.toMillis(1);
/** /**
* Tables in list will still have their *schema* exported (so the tables will be created), * Tables in list will still have their *schema* exported (so the tables will be created),
@@ -159,15 +160,15 @@ public class FullBackupExporter extends FullBackupBase {
for (String table : tables) { for (String table : tables) {
throwIfCanceled(cancellationSignal); throwIfCanceled(cancellationSignal);
if (table.equals(MessageTable.TABLE_NAME)) { if (table.equals(MessageTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, FullBackupExporter::isNonExpiringMmsMessage, null, count, estimatedCount, cancellationSignal); count = exportTable(table, input, outputStream, cursor -> isNonExpiringMessage(input, cursor), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(ReactionTable.TABLE_NAME)) { } else if (table.equals(ReactionTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, new MessageId(CursorUtil.requireLong(cursor, ReactionTable.MESSAGE_ID))), null, count, estimatedCount, cancellationSignal); 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)) { } else if (table.equals(MentionTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, CursorUtil.requireLong(cursor, MentionTable.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal); count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, CursorUtil.requireLong(cursor, MentionTable.MESSAGE_ID)), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(GroupReceiptTable.TABLE_NAME)) { } else if (table.equals(GroupReceiptTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptTable.MMS_ID))), null, count, estimatedCount, cancellationSignal); count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(GroupReceiptTable.MMS_ID))), null, count, estimatedCount, cancellationSignal);
} else if (table.equals(AttachmentTable.TABLE_NAME)) { } else if (table.equals(AttachmentTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMmsMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.MMS_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal); count = exportTable(table, input, outputStream, cursor -> isForNonExpiringMessage(input, cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.MESSAGE_ID))), (cursor, innerCount) -> exportAttachment(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (table.equals(StickerTable.TABLE_NAME)) { } else if (table.equals(StickerTable.TABLE_NAME)) {
count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal); count = exportTable(table, input, outputStream, cursor -> true, (cursor, innerCount) -> exportSticker(attachmentSecret, cursor, outputStream, innerCount, estimatedCount), count, estimatedCount, cancellationSignal);
} else if (!TABLE_CONTENT_BLOCKLIST.contains(table)) { } else if (!TABLE_CONTENT_BLOCKLIST.contains(table)) {
@@ -444,11 +445,10 @@ public class FullBackupExporter extends FullBackupBase {
long estimatedCount) long estimatedCount)
throws IOException throws IOException
{ {
long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.ROW_ID)); long rowId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.ID));
long uniqueId = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.UNIQUE_ID)); long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.DATA_SIZE));
long size = cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentTable.SIZE));
String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentTable.DATA)); String data = cursor.getString(cursor.getColumnIndexOrThrow(AttachmentTable.DATA_FILE));
byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentTable.DATA_RANDOM)); byte[] random = cursor.getBlob(cursor.getColumnIndexOrThrow(AttachmentTable.DATA_RANDOM));
if (!TextUtils.isEmpty(data)) { if (!TextUtils.isEmpty(data)) {
@@ -457,14 +457,14 @@ public class FullBackupExporter extends FullBackupBase {
if (size <= 0 || fileLength != dbLength) { if (size <= 0 || fileLength != dbLength) {
size = calculateVeryOldStreamLength(attachmentSecret, random, data); size = calculateVeryOldStreamLength(attachmentSecret, random, data);
Log.w(TAG, "Needed size calculation! Manual: " + size + " File: " + fileLength + " DB: " + dbLength + " ID: " + new AttachmentId(rowId, uniqueId)); Log.w(TAG, "Needed size calculation! Manual: " + size + " File: " + fileLength + " DB: " + dbLength + " ID: " + new AttachmentId(rowId));
} }
} }
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount)); EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, ++count, estimatedCount));
if (!TextUtils.isEmpty(data) && size > 0) { if (!TextUtils.isEmpty(data) && size > 0) {
try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) { try (InputStream inputStream = openAttachmentStream(attachmentSecret, random, data)) {
outputStream.write(new AttachmentId(rowId, uniqueId), inputStream, size); outputStream.write(new AttachmentId(rowId), inputStream, size);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
Log.w(TAG, "Missing attachment", e); Log.w(TAG, "Missing attachment", e);
} }
@@ -579,27 +579,34 @@ public class FullBackupExporter extends FullBackupBase {
return count; return count;
} }
private static boolean isNonExpiringMmsMessage(@NonNull Cursor cursor) { private static boolean isNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull Cursor cursor) {
return cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.EXPIRES_IN)) <= 0 && long id = CursorUtil.requireLong(cursor, MessageTable.ID);
cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.VIEW_ONCE)) <= 0; 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);
long expiresAt = expireStarted + expiresIn;
long timeRemaining = expiresAt - System.currentTimeMillis();
if (latestRevisionId > 0 && latestRevisionId != id ) {
return isForNonExpiringMessage(db, latestRevisionId);
}
if (expireStarted > 0 && timeRemaining <= EXPIRATION_BACKUP_THRESHOLD) {
return false;
}
return true;
} }
private static boolean isNonExpiringSmsMessage(@NonNull Cursor cursor) { private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long messageId) {
return cursor.getLong(cursor.getColumnIndexOrThrow(MessageTable.EXPIRES_IN)) <= 0; String[] columns = new String[] { MessageTable.ID, MessageTable.EXPIRE_STARTED, MessageTable.EXPIRES_IN, MessageTable.LATEST_REVISION_ID };
}
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull MessageId messageId) {
return isForNonExpiringMmsMessage(db, messageId.getId());
}
private static boolean isForNonExpiringMmsMessage(@NonNull SQLiteDatabase db, long mmsId) {
String[] columns = new String[] { MessageTable.EXPIRES_IN, MessageTable.VIEW_ONCE };
String where = MessageTable.ID + " = ?"; String where = MessageTable.ID + " = ?";
String[] args = new String[] { String.valueOf(mmsId) }; String[] args = SqlUtil.buildArgs(messageId);
try (Cursor mmsCursor = db.query(MessageTable.TABLE_NAME, columns, where, args, null, null, null)) { try (Cursor mmsCursor = db.query(MessageTable.TABLE_NAME, columns, where, args, null, null, null)) {
if (mmsCursor != null && mmsCursor.moveToFirst()) { if (mmsCursor != null && mmsCursor.moveToFirst()) {
return isNonExpiringMmsMessage(mmsCursor); return isNonExpiringMessage(db, mmsCursor);
} }
} }
@@ -194,26 +194,33 @@ public class FullBackupImporter extends FullBackupBase {
private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream) private static void processAttachment(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Attachment attachment, BackupRecordInputStream inputStream)
throws IOException throws IOException
{ {
File dataFile = AttachmentTable.newFile(context); File dataFile = AttachmentTable.newFile(context);
Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false); Pair<byte[], OutputStream> output = ModernEncryptingPartOutputStream.createFor(attachmentSecret, dataFile, false);
boolean isLegacyTable = SqlUtil.tableExists(db, "part");
String dataFileColumnName = isLegacyTable ? "_data" : AttachmentTable.DATA_FILE;
String dataRandomColumnName = isLegacyTable ? "data_random" : AttachmentTable.DATA_RANDOM;
String idColumnName = isLegacyTable ? "_id" : AttachmentTable.ID;
String tableName = isLegacyTable ? "part" : AttachmentTable.TABLE_NAME;
ContentValues contentValues = new ContentValues(); ContentValues contentValues = new ContentValues();
try { try {
inputStream.readAttachmentTo(output.second, attachment.length); inputStream.readAttachmentTo(output.second, attachment.length);
contentValues.put(AttachmentTable.DATA, dataFile.getAbsolutePath()); contentValues.put(dataFileColumnName, dataFile.getAbsolutePath());
contentValues.put(AttachmentTable.DATA_RANDOM, output.first); contentValues.put(dataRandomColumnName, output.first);
} catch (BackupRecordInputStream.BadMacException e) { } catch (BackupRecordInputStream.BadMacException e) {
Log.w(TAG, "Bad MAC for attachment " + attachment.attachmentId + "! Can't restore it.", e); Log.w(TAG, "Bad MAC for attachment " + attachment.attachmentId + "! Can't restore it.", e);
dataFile.delete(); dataFile.delete();
contentValues.put(AttachmentTable.DATA, (String) null); contentValues.put(dataFileColumnName, (String) null);
contentValues.put(AttachmentTable.DATA_RANDOM, (String) null); contentValues.put(dataRandomColumnName, (String) null);
} }
db.update(AttachmentTable.TABLE_NAME, contentValues, db.update(tableName,
AttachmentTable.ROW_ID + " = ? AND " + AttachmentTable.UNIQUE_ID + " = ?", contentValues,
new String[] {String.valueOf(attachment.rowId), String.valueOf(attachment.attachmentId)}); idColumnName + " = ?",
SqlUtil.buildArgs(attachment.rowId));
} }
private static void processSticker(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Sticker sticker, BackupRecordInputStream inputStream) private static void processSticker(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Sticker sticker, BackupRecordInputStream inputStream)
@@ -26,7 +26,6 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.NetworkResult import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.archive.ArchiveGetBackupInfoResponse
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
import org.whispersystems.signalservice.api.push.ServiceId.ACI import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI import org.whispersystems.signalservice.api.push.ServiceId.PNI
@@ -168,7 +167,7 @@ object BackupRepository {
/** /**
* Returns an object with details about the remote backup state. * Returns an object with details about the remote backup state.
*/ */
fun getRemoteBackupState(): NetworkResult<ArchiveGetBackupInfoResponse> { fun getRemoteBackupState(): NetworkResult<BackupMetadata> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey() val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
@@ -182,6 +181,18 @@ object BackupRepository {
} }
.then { credential -> .then { credential ->
api.getBackupInfo(backupKey, 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()
)
}
} }
} }
@@ -255,3 +266,8 @@ class BackupState {
val chatIdToBackupRecipientId = HashMap<Long, Long>() val chatIdToBackupRecipientId = HashMap<Long, Long>()
val callIdToType = 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 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 import org.thoughtcrime.securesms.database.AttachmentTable
fun AttachmentTable.clearAllDataForBackupRestore() { fun AttachmentTable.clearAllDataForBackupRestore() {
writableDatabase.delete(AttachmentTable.TABLE_NAME).run() writableDatabase.deleteAll(AttachmentTable.TABLE_NAME)
} }
@@ -109,7 +109,8 @@ class CallLogIterator(private val cursor: Cursor) : Iterator<BackupCall?>, Close
CallTable.Event.DECLINED -> Call.Event.DECLINED CallTable.Event.DECLINED -> Call.Event.DECLINED
CallTable.Event.GENERIC_GROUP_CALL -> Call.Event.GENERIC_GROUP_CALL CallTable.Event.GENERIC_GROUP_CALL -> Call.Event.GENERIC_GROUP_CALL
CallTable.Event.JOINED -> Call.Event.JOINED CallTable.Event.JOINED -> Call.Event.JOINED
CallTable.Event.MISSED -> Call.Event.MISSED CallTable.Event.MISSED,
CallTable.Event.MISSED_NOTIFICATION_PROFILE -> Call.Event.MISSED
CallTable.Event.DELETE -> Call.Event.DELETE CallTable.Event.DELETE -> Call.Event.DELETE
CallTable.Event.RINGING -> Call.Event.UNKNOWN_EVENT CallTable.Event.RINGING -> Call.Event.UNKNOWN_EVENT
CallTable.Event.NOT_ACCEPTED -> Call.Event.NOT_ACCEPTED CallTable.Event.NOT_ACCEPTED -> Call.Event.NOT_ACCEPTED
@@ -7,7 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.database
import okio.ByteString.Companion.toByteString import okio.ByteString.Companion.toByteString
import org.signal.core.util.CursorUtil 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.logging.Log
import org.signal.core.util.readToList import org.signal.core.util.readToList
import org.signal.core.util.requireLong import org.signal.core.util.requireLong
@@ -88,12 +88,10 @@ fun DistributionListTables.restoreFromBackup(dlist: BackupDistributionList, back
fun DistributionListTables.clearAllDataForBackupRestore() { fun DistributionListTables.clearAllDataForBackupRestore() {
writableDatabase writableDatabase
.delete(DistributionListTables.ListTable.TABLE_NAME) .deleteAll(DistributionListTables.ListTable.TABLE_NAME)
.run()
writableDatabase writableDatabase
.delete(DistributionListTables.MembershipTable.TABLE_NAME) .deleteAll(DistributionListTables.MembershipTable.TABLE_NAME)
.run()
} }
private fun DistributionListPrivacyMode.toBackupPrivacyMode(): BackupDistributionList.PrivacyMode { private fun DistributionListPrivacyMode.toBackupPrivacyMode(): BackupDistributionList.PrivacyMode {
@@ -10,7 +10,7 @@ import android.database.Cursor
import okio.ByteString.Companion.toByteString import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64 import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil 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.logging.Log
import org.signal.core.util.nullIfBlank import org.signal.core.util.nullIfBlank
import org.signal.core.util.requireBoolean import org.signal.core.util.requireBoolean
@@ -155,7 +155,7 @@ fun RecipientTable.restoreSelfFromBackup(accountData: AccountData, selfId: Recip
} }
fun RecipientTable.clearAllDataForBackupRestore() { fun RecipientTable.clearAllDataForBackupRestore() {
writableDatabase.delete(RecipientTable.TABLE_NAME).run() writableDatabase.deleteAll(RecipientTable.TABLE_NAME)
SqlUtil.resetAutoIncrementValue(writableDatabase, RecipientTable.TABLE_NAME) SqlUtil.resetAutoIncrementValue(writableDatabase, RecipientTable.TABLE_NAME)
RecipientId.clearCache() RecipientId.clearCache()
@@ -56,7 +56,7 @@ object AccountDataProcessor {
readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context), readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context),
sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context), sealedSenderIndicators = TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context),
linkPreviews = SignalStore.settings().isLinkPreviewsEnabled, linkPreviews = SignalStore.settings().isLinkPreviewsEnabled,
notDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberListingMode.isUnlisted, notDiscoverableByPhoneNumber = SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode.isUndiscoverable,
phoneNumberSharingMode = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode.toBackupPhoneNumberSharingMode(), phoneNumberSharingMode = SignalStore.phoneNumberPrivacy().phoneNumberSharingMode.toBackupPhoneNumberSharingMode(),
preferContactAvatars = SignalStore.settings().isPreferSystemContactPhotos, preferContactAvatars = SignalStore.settings().isPreferSystemContactPhotos,
universalExpireTimer = SignalStore.settings().universalExpireTimer, universalExpireTimer = SignalStore.settings().universalExpireTimer,
@@ -86,7 +86,7 @@ object AccountDataProcessor {
TextSecurePreferences.setTypingIndicatorsEnabled(context, settings.typingIndicators) TextSecurePreferences.setTypingIndicatorsEnabled(context, settings.typingIndicators)
TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, settings.sealedSenderIndicators) TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, settings.sealedSenderIndicators)
SignalStore.settings().isLinkPreviewsEnabled = settings.linkPreviews SignalStore.settings().isLinkPreviewsEnabled = settings.linkPreviews
SignalStore.phoneNumberPrivacy().phoneNumberListingMode = if (settings.notDiscoverableByPhoneNumber) PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED else PhoneNumberPrivacyValues.PhoneNumberListingMode.LISTED SignalStore.phoneNumberPrivacy().phoneNumberDiscoverabilityMode = if (settings.notDiscoverableByPhoneNumber) PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.NOT_DISCOVERABLE else PhoneNumberPrivacyValues.PhoneNumberDiscoverabilityMode.DISCOVERABLE
SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = settings.phoneNumberSharingMode.toLocalPhoneNumberMode() SignalStore.phoneNumberPrivacy().phoneNumberSharingMode = settings.phoneNumberSharingMode.toLocalPhoneNumberMode()
SignalStore.settings().isPreferSystemContactPhotos = settings.preferContactAvatars SignalStore.settings().isPreferSystemContactPhotos = settings.preferContactAvatars
SignalStore.settings().universalExpireTimer = settings.universalExpireTimer SignalStore.settings().universalExpireTimer = settings.universalExpireTimer
@@ -0,0 +1,261 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.updateLayoutParams
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.components.settings.app.subscription.models.GooglePayButton
import org.thoughtcrime.securesms.databinding.PaypalButtonBinding
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
import java.util.Currency
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MessageBackupsCheckoutSheet(
messageBackupsType: MessageBackupsType,
availablePaymentGateways: List<GatewayResponse.Gateway>,
onDismissRequest: () -> Unit,
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
) {
ModalBottomSheet(
onDismissRequest = onDismissRequest,
dragHandle = { BottomSheets.Handle() },
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
messageBackupsType = messageBackupsType,
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = onPaymentGatewaySelected
)
}
}
@Composable
private fun SheetContent(
messageBackupsType: MessageBackupsType,
availablePaymentGateways: List<GatewayResponse.Gateway>,
onPaymentGatewaySelected: (GatewayResponse.Gateway) -> Unit
) {
val resources = LocalContext.current.resources
val formattedPrice = remember(messageBackupsType.pricePerMonth) {
FiatMoneyUtil.format(resources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
text = "Pay $formattedPrice/month to Signal", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 48.dp)
)
Text(
text = "You'll get:", // TODO [message-backups] Finalized copy
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 5.dp)
)
MessageBackupsTypeBlock(
messageBackupsType = messageBackupsType,
isSelected = false,
onSelected = {},
enabled = false,
modifier = Modifier.padding(top = 24.dp)
)
Column(
verticalArrangement = spacedBy(12.dp),
modifier = Modifier.padding(top = 48.dp, bottom = 24.dp)
) {
availablePaymentGateways.forEach {
when (it) {
GatewayResponse.Gateway.GOOGLE_PAY -> GooglePayButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.GOOGLE_PAY)
}
GatewayResponse.Gateway.PAYPAL -> PayPalButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.PAYPAL)
}
GatewayResponse.Gateway.CREDIT_CARD -> CreditOrDebitCardButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.CREDIT_CARD)
}
GatewayResponse.Gateway.SEPA_DEBIT -> SepaButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.SEPA_DEBIT)
}
GatewayResponse.Gateway.IDEAL -> IdealButton {
onPaymentGatewaySelected(GatewayResponse.Gateway.IDEAL)
}
}
}
}
}
@Composable
private fun PayPalButton(
onClick: () -> Unit
) {
AndroidView(factory = {
val view = LayoutInflater.from(it).inflate(R.layout.paypal_button, null)
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
view
}) {
val binding = PaypalButtonBinding.bind(it)
binding.paypalButton.updateLayoutParams<ViewGroup.MarginLayoutParams> {
marginStart = 0
marginEnd = 0
}
binding.paypalButton.setOnClickListener {
onClick()
}
}
}
@Composable
private fun GooglePayButton(
onClick: () -> Unit
) {
val model = GooglePayButton.Model(onClick, true)
AndroidView(factory = {
LayoutInflater.from(it).inflate(R.layout.google_pay_button_pref, null)
}) {
val holder = GooglePayButton.ViewHolder(it)
holder.bind(model)
}
}
@Composable
private fun SepaButton(
onClick: () -> Unit
) {
Buttons.LargeTonal(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(id = R.drawable.bank_transfer),
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(text = stringResource(id = R.string.GatewaySelectorBottomSheet__bank_transfer))
}
}
@Composable
private fun IdealButton(
onClick: () -> Unit
) {
Buttons.LargeTonal(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
) {
Image(
painter = painterResource(id = R.drawable.logo_ideal),
contentDescription = null,
modifier = Modifier
.size(32.dp)
.padding(end = 8.dp)
)
Text(text = stringResource(id = R.string.GatewaySelectorBottomSheet__ideal))
}
}
@Composable
private fun CreditOrDebitCardButton(
onClick: () -> Unit
) {
Buttons.LargePrimary(
onClick = onClick,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(id = R.drawable.credit_card),
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = stringResource(id = R.string.GatewaySelectorBottomSheet__credit_or_debit_card)
)
}
}
@Preview
@Composable
private fun MessageBackupsCheckoutSheetPreview() {
val paidTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
title = "Text + All your media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!"
)
)
)
val availablePaymentGateways = GatewayResponse.Gateway.values().toList()
Previews.Preview {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
SheetContent(
messageBackupsType = paidTier,
availablePaymentGateways = availablePaymentGateways,
onPaymentGatewaySelected = {}
)
}
}
}
@@ -0,0 +1,187 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
/**
* Educational content which allows user to proceed to set up automatic backups
* or navigate to a support page to learn more.
*/
@Composable
fun MessageBackupsEducationScreen(
onNavigationClick: () -> Unit,
onEnableBackups: () -> Unit,
onLearnMore: () -> Unit
) {
Scaffolds.Settings(
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_x_24),
title = "Chat backups" // TODO [message-backups] Finalized copy
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
item {
Image(
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Final image asset
contentDescription = null,
modifier = Modifier
.padding(top = 48.dp)
.size(88.dp)
)
}
item {
Text(
text = "Chat Backups", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 15.dp)
)
}
item {
Text(
text = "Back up your messages and media and using Signals secure, end-to-end encrypted storage service. Never lose a message when you get a new phone or reinstall Signal.", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp)
)
}
item {
Column(
modifier = Modifier.padding(top = 32.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
) {
NotableFeatureRow(
painter = painterResource(id = R.drawable.symbol_lock_compact_20),
text = "End-to-end Encrypted" // TODO [message-backups] Finalized copy
)
NotableFeatureRow(
painter = painterResource(id = R.drawable.symbol_check_square_compact_20),
text = "Optional, always" // TODO [message-backups] Finalized copy
)
NotableFeatureRow(
painter = painterResource(id = R.drawable.symbol_trash_compact_20),
text = "Delete your backup anytime" // TODO [message-backups] Finalized copy
)
}
}
}
Buttons.LargePrimary(
onClick = onEnableBackups,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Enable backups" // TODO [message-backups] Finalized copy
)
}
TextButton(
onClick = onLearnMore,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = "Learn more" // TODO [message-backups] Finalized copy
)
}
}
}
}
@Preview
@Composable
private fun MessageBackupsEducationSheetPreview() {
Previews.Preview {
MessageBackupsEducationScreen(
onNavigationClick = {},
onEnableBackups = {},
onLearnMore = {}
)
}
}
@Preview
@Composable
private fun NotableFeatureRowPreview() {
Previews.Preview {
NotableFeatureRow(
painter = painterResource(id = R.drawable.symbol_lock_compact_20),
text = "Notable feature information"
)
}
}
@Composable
private fun NotableFeatureRow(
painter: Painter,
text: String
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = painter,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(end = 8.dp)
.size(32.dp)
.background(color = SignalTheme.colors.colorSurface2, shape = CircleShape)
.padding(6.dp)
)
Text(
text = text,
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@@ -0,0 +1,115 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.dialog
import androidx.navigation.compose.rememberNavController
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.PassphraseRequiredActivity
import org.thoughtcrime.securesms.util.viewModel
class MessageBackupsFlowActivity : PassphraseRequiredActivity() {
private val viewModel: MessageBackupsFlowViewModel by viewModel { MessageBackupsFlowViewModel() }
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
setContent {
SignalTheme {
val state by viewModel.state
val navController = rememberNavController()
fun MessageBackupsScreen.next() {
val nextScreen = viewModel.goToNextScreen(this)
if (nextScreen != this) {
navController.navigate(nextScreen.name)
}
}
fun NavController.popOrFinish() {
if (popBackStack()) {
return
}
finishAfterTransition()
}
LaunchedEffect(Unit) {
navController.setLifecycleOwner(this@MessageBackupsFlowActivity)
navController.setOnBackPressedDispatcher(this@MessageBackupsFlowActivity.onBackPressedDispatcher)
navController.enableOnBackPressed(true)
}
NavHost(
navController = navController,
startDestination = MessageBackupsScreen.EDUCATION.name,
enterTransition = { slideInHorizontally(initialOffsetX = { it }) },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) },
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) }
) {
composable(route = MessageBackupsScreen.EDUCATION.name) {
MessageBackupsEducationScreen(
onNavigationClick = navController::popOrFinish,
onEnableBackups = { MessageBackupsScreen.EDUCATION.next() },
onLearnMore = {}
)
}
composable(route = MessageBackupsScreen.PIN_EDUCATION.name) {
MessageBackupsPinEducationScreen(
onNavigationClick = navController::popOrFinish,
onGeneratePinClick = {},
onUseCurrentPinClick = { MessageBackupsScreen.PIN_EDUCATION.next() },
recommendedPinSize = 16 // TODO [message-backups] This value should come from some kind of config
)
}
composable(route = MessageBackupsScreen.PIN_CONFIRMATION.name) {
MessageBackupsPinConfirmationScreen(
pin = state.pin,
onPinChanged = viewModel::onPinEntryUpdated,
pinKeyboardType = state.pinKeyboardType,
onPinKeyboardTypeSelected = viewModel::onPinKeyboardTypeUpdated,
onNextClick = { MessageBackupsScreen.PIN_CONFIRMATION.next() }
)
}
composable(route = MessageBackupsScreen.TYPE_SELECTION.name) {
MessageBackupsTypeSelectionScreen(
selectedBackupsType = state.selectedMessageBackupsType,
availableBackupsTypes = state.availableBackupsTypes,
onMessageBackupsTypeSelected = viewModel::onMessageBackupsTypeUpdated,
onNavigationClick = navController::popOrFinish,
onReadMoreClicked = {},
onNextClicked = { MessageBackupsScreen.TYPE_SELECTION.next() }
)
}
dialog(route = MessageBackupsScreen.CHECKOUT_SHEET.name) {
MessageBackupsCheckoutSheet(
messageBackupsType = state.selectedMessageBackupsType!!,
availablePaymentGateways = state.availablePaymentGateways,
onDismissRequest = navController::popOrFinish,
onPaymentGatewaySelected = {
viewModel.onPaymentGatewayUpdated(it)
MessageBackupsScreen.CHECKOUT_SHEET.next()
}
)
}
}
}
}
}
}
@@ -0,0 +1,8 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
class MessageBackupsFlowRepository
@@ -0,0 +1,19 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
data class MessageBackupsFlowState(
val selectedMessageBackupsType: MessageBackupsType? = null,
val availableBackupsTypes: List<MessageBackupsType> = emptyList(),
val selectedPaymentGateway: GatewayResponse.Gateway? = null,
val availablePaymentGateways: List<GatewayResponse.Gateway> = emptyList(),
val pin: String = "",
val pinKeyboardType: PinKeyboardType = SignalStore.pinValues().keyboardType
)
@@ -0,0 +1,58 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
class MessageBackupsFlowViewModel : ViewModel() {
private val internalState = mutableStateOf(MessageBackupsFlowState())
val state: State<MessageBackupsFlowState> = internalState
fun goToNextScreen(currentScreen: MessageBackupsScreen): MessageBackupsScreen {
return when (currentScreen) {
MessageBackupsScreen.EDUCATION -> MessageBackupsScreen.PIN_EDUCATION
MessageBackupsScreen.PIN_EDUCATION -> MessageBackupsScreen.PIN_CONFIRMATION
MessageBackupsScreen.PIN_CONFIRMATION -> validatePinAndUpdateState()
MessageBackupsScreen.TYPE_SELECTION -> validateTypeAndUpdateState()
MessageBackupsScreen.CHECKOUT_SHEET -> validateGatewayAndUpdateState()
MessageBackupsScreen.PROCESS_PAYMENT -> MessageBackupsScreen.COMPLETED
MessageBackupsScreen.COMPLETED -> error("Unsupported state transition from terminal state COMPLETED")
}
}
fun onPinEntryUpdated(pin: String) {
internalState.value = state.value.copy(pin = pin)
}
fun onPinKeyboardTypeUpdated(pinKeyboardType: PinKeyboardType) {
internalState.value = state.value.copy(pinKeyboardType = pinKeyboardType)
}
fun onPaymentGatewayUpdated(gateway: GatewayResponse.Gateway) {
internalState.value = state.value.copy(selectedPaymentGateway = gateway)
}
fun onMessageBackupsTypeUpdated(messageBackupsType: MessageBackupsType) {
internalState.value = state.value.copy(selectedMessageBackupsType = messageBackupsType)
}
private fun validatePinAndUpdateState(): MessageBackupsScreen {
return MessageBackupsScreen.TYPE_SELECTION
}
private fun validateTypeAndUpdateState(): MessageBackupsScreen {
return MessageBackupsScreen.CHECKOUT_SHEET
}
private fun validateGatewayAndUpdateState(): MessageBackupsScreen {
return MessageBackupsScreen.PROCESS_PAYMENT
}
}
@@ -0,0 +1,198 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.lock.v2.PinKeyboardType
/**
* Screen which requires the user to enter their pin before enabling backups.
*/
@Composable
fun MessageBackupsPinConfirmationScreen(
pin: String,
onPinChanged: (String) -> Unit,
pinKeyboardType: PinKeyboardType,
onPinKeyboardTypeSelected: (PinKeyboardType) -> Unit,
onNextClick: () -> Unit
) {
val focusRequester = remember { FocusRequester() }
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
item {
Text(
text = "Enter your PIN", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 40.dp)
)
}
item {
Text(
text = "Enter your Signal PIN to enable backups", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 16.dp)
)
}
item {
// TODO [message-backups] Confirm default focus state
val keyboardType = remember(pinKeyboardType) {
when (pinKeyboardType) {
PinKeyboardType.NUMERIC -> KeyboardType.NumberPassword
PinKeyboardType.ALPHA_NUMERIC -> KeyboardType.Password
}
}
TextField(
value = pin,
onValueChange = onPinChanged,
textStyle = LocalTextStyle.current.copy(textAlign = TextAlign.Center),
keyboardActions = KeyboardActions(
onDone = { onNextClick() }
),
keyboardOptions = KeyboardOptions(
keyboardType = keyboardType
),
modifier = Modifier
.padding(top = 72.dp)
.fillMaxWidth()
.focusRequester(focusRequester)
)
}
item {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(top = 48.dp)
) {
PinKeyboardTypeToggle(
pinKeyboardType = pinKeyboardType,
onPinKeyboardTypeSelected = onPinKeyboardTypeSelected
)
}
}
}
Box(
contentAlignment = Alignment.BottomEnd,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
Buttons.LargeTonal(
onClick = onNextClick
) {
Text(
text = "Next" // TODO [message-backups] Finalized copy
)
}
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
@Preview
@Composable
private fun MessageBackupsPinConfirmationScreenPreview() {
Previews.Preview {
MessageBackupsPinConfirmationScreen(
pin = "",
onPinChanged = {},
pinKeyboardType = PinKeyboardType.ALPHA_NUMERIC,
onPinKeyboardTypeSelected = {},
onNextClick = {}
)
}
}
@Preview
@Composable
private fun PinKeyboardTypeTogglePreview() {
Previews.Preview {
var type by remember { mutableStateOf(PinKeyboardType.ALPHA_NUMERIC) }
PinKeyboardTypeToggle(
pinKeyboardType = type,
onPinKeyboardTypeSelected = { type = it }
)
}
}
@Composable
private fun PinKeyboardTypeToggle(
pinKeyboardType: PinKeyboardType,
onPinKeyboardTypeSelected: (PinKeyboardType) -> Unit
) {
val callback = remember(pinKeyboardType) {
{ onPinKeyboardTypeSelected(pinKeyboardType.other) }
}
val iconRes = remember(pinKeyboardType) {
when (pinKeyboardType) {
PinKeyboardType.NUMERIC -> R.drawable.symbol_keyboard_24
PinKeyboardType.ALPHA_NUMERIC -> R.drawable.symbol_number_pad_24
}
}
TextButton(onClick = callback) {
Icon(
painter = painterResource(id = iconRes),
tint = MaterialTheme.colorScheme.primary,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = "Switch keyboard" // TODO [message-backups] Finalized copy
)
}
}
@@ -0,0 +1,132 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.thoughtcrime.securesms.R
/**
* Explanation screen that details how the user's pin is utilized with backups,
* and how long they should make their pin.
*/
@Composable
fun MessageBackupsPinEducationScreen(
onNavigationClick: () -> Unit,
onGeneratePinClick: () -> Unit,
onUseCurrentPinClick: () -> Unit,
recommendedPinSize: Int
) {
Scaffolds.Settings(
title = "Backup type", // TODO [message-backups] Finalized copy
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
item {
Image(
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Finalized image
contentDescription = null,
modifier = Modifier
.padding(top = 48.dp)
.size(88.dp)
)
}
item {
Text(
text = "PINs protect your backup", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 16.dp)
)
}
item {
Text(
text = "Your Signal PIN lets you restore your backup when you re-install Signal. For increased security, we recommend updating to a new $recommendedPinSize-digit PIN.", // TODO [message-backups] Finalized copy
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(top = 16.dp)
)
}
item {
Text(
text = "If you forget your PIN, you will not be able to restore your backup. You can change your PIN at any time in settings.", // TODO [message-backups] Finalized copy
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(top = 16.dp)
)
}
}
Buttons.LargePrimary(
onClick = onGeneratePinClick,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "Generate a new $recommendedPinSize-digit PIN" // TODO [message-backups] Finalized copy
)
}
TextButton(
onClick = onUseCurrentPinClick,
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = "Use current Signal PIN" // TODO [message-backups] Finalized copy
)
}
}
}
}
@Preview
@Composable
private fun MessageBackupsPinScreenPreview() {
Previews.Preview {
MessageBackupsPinEducationScreen(
onNavigationClick = {},
onGeneratePinClick = {},
onUseCurrentPinClick = {},
recommendedPinSize = 16
)
}
}
@@ -0,0 +1,16 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
enum class MessageBackupsScreen {
EDUCATION,
PIN_EDUCATION,
PIN_CONFIRMATION,
TYPE_SELECTION,
CHECKOUT_SHEET,
PROCESS_PAYMENT,
COMPLETED
}
@@ -0,0 +1,302 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withAnnotation
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
import java.util.Currency
/**
* Screen which allows the user to select their preferred backup type.
*/
@OptIn(ExperimentalTextApi::class)
@Composable
fun MessageBackupsTypeSelectionScreen(
selectedBackupsType: MessageBackupsType?,
availableBackupsTypes: List<MessageBackupsType>,
onMessageBackupsTypeSelected: (MessageBackupsType) -> Unit,
onNavigationClick: () -> Unit,
onReadMoreClicked: () -> Unit,
onNextClicked: () -> Unit
) {
Scaffolds.Settings(
title = "",
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.fillMaxSize()
) {
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.weight(1f)
) {
item {
Image(
painter = painterResource(id = R.drawable.ic_signal_logo_large), // TODO [message-backups] Finalized art asset
contentDescription = null,
modifier = Modifier.size(88.dp)
)
}
item {
Text(
text = "Choose your backup type", // TODO [message-backups] Finalized copy
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 12.dp)
)
}
item {
// TODO [message-backups] Finalized copy
val primaryColor = MaterialTheme.colorScheme.primary
val readMoreString = buildAnnotatedString {
append("All backups are end-to-end encrypted. Signal is a non-profit—paying for backups helps support our mission. ")
withAnnotation(tag = "URL", annotation = "read-more") {
withStyle(
style = SpanStyle(
color = primaryColor
)
) {
append("Read more")
}
}
}
ClickableText(
text = readMoreString,
style = MaterialTheme.typography.bodyLarge.copy(textAlign = TextAlign.Center),
onClick = { offset ->
readMoreString
.getStringAnnotations(tag = "URL", start = offset, end = offset)
.firstOrNull()?.let { onReadMoreClicked() }
},
modifier = Modifier.padding(top = 8.dp)
)
}
itemsIndexed(
availableBackupsTypes,
{ _, item -> item.title }
) { index, item ->
MessageBackupsTypeBlock(
messageBackupsType = item,
isSelected = item == selectedBackupsType,
onSelected = { onMessageBackupsTypeSelected(item) },
modifier = Modifier.padding(top = if (index == 0) 20.dp else 18.dp)
)
}
}
Buttons.LargePrimary(
onClick = onNextClicked,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
Text(
text = "Next" // TODO [message-backups] Finalized copy
)
}
}
}
}
@Preview
@Composable
private fun MessageBackupsTypeSelectionScreenPreview() {
val freeTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.ZERO, Currency.getInstance("USD")),
title = "Text + 30 days of media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Last 30 days of media"
)
)
)
val paidTier = MessageBackupsType(
pricePerMonth = FiatMoney(BigDecimal.valueOf(3), Currency.getInstance("USD")),
title = "Text + All your media",
features = persistentListOf(
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "Full text message backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_album_compact_bold_16,
label = "Full media backup"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_thread_compact_bold_16,
label = "1TB of storage (~250K photos)"
),
MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_heart_compact_bold_16,
label = "Thanks for supporting Signal!"
)
)
)
var selectedBackupsType by remember { mutableStateOf(freeTier) }
Previews.Preview {
MessageBackupsTypeSelectionScreen(
selectedBackupsType = selectedBackupsType,
availableBackupsTypes = listOf(freeTier, paidTier),
onMessageBackupsTypeSelected = { selectedBackupsType = it },
onNavigationClick = {},
onReadMoreClicked = {},
onNextClicked = {}
)
}
}
@Composable
fun MessageBackupsTypeBlock(
messageBackupsType: MessageBackupsType,
isSelected: Boolean,
onSelected: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true
) {
val borderColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
Color.Transparent
}
val background = if (isSelected) {
MaterialTheme.colorScheme.secondaryContainer
} else {
SignalTheme.colors.colorSurface2
}
Column(
modifier = modifier
.fillMaxWidth()
.background(color = background, shape = RoundedCornerShape(18.dp))
.border(width = 2.dp, color = borderColor, shape = RoundedCornerShape(18.dp))
.clip(shape = RoundedCornerShape(18.dp))
.clickable(onClick = onSelected, enabled = enabled)
.padding(vertical = 16.dp, horizontal = 20.dp)
) {
Text(
text = formatCostPerMonth(messageBackupsType.pricePerMonth),
style = MaterialTheme.typography.titleSmall
)
Text(
text = messageBackupsType.title,
style = MaterialTheme.typography.titleMedium
)
Column(
verticalArrangement = spacedBy(4.dp),
modifier = Modifier
.padding(top = 8.dp)
.padding(horizontal = 16.dp)
) {
messageBackupsType.features.forEach {
MessageBackupsTypeFeatureRow(messageBackupsTypeFeature = it)
}
}
}
}
@Composable
private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
return if (pricePerMonth.amount == BigDecimal.ZERO) {
"Free"
} else {
"${FiatMoneyUtil.format(LocalContext.current.resources, pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())}/month"
}
}
@Composable
private fun MessageBackupsTypeFeatureRow(messageBackupsTypeFeature: MessageBackupsTypeFeature) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Icon(
painter = painterResource(id = messageBackupsTypeFeature.iconResourceId),
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(
text = messageBackupsTypeFeature.label,
style = MaterialTheme.typography.bodyLarge
)
}
}
data class MessageBackupsType(
val pricePerMonth: FiatMoney,
val title: String,
val features: ImmutableList<MessageBackupsTypeFeature>
)
data class MessageBackupsTypeFeature(
val iconResourceId: Int,
val label: String
)
@@ -4,6 +4,8 @@ import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView import androidx.appcompat.widget.AppCompatImageView
import androidx.core.content.res.use import androidx.core.content.res.use
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
@@ -11,8 +13,6 @@ import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageSize import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageSize
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.glide.GiftBadgeModel import org.thoughtcrime.securesms.glide.GiftBadgeModel
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.ScreenDensity import org.thoughtcrime.securesms.util.ScreenDensity
import org.thoughtcrime.securesms.util.ThemeUtil import org.thoughtcrime.securesms.util.ThemeUtil
@@ -43,35 +43,35 @@ class BadgeImageView @JvmOverloads constructor(
} }
fun setBadgeFromRecipient(recipient: Recipient?) { fun setBadgeFromRecipient(recipient: Recipient?) {
getGlideRequests()?.let { getGlideRequestManager()?.let {
setBadgeFromRecipient(recipient, it) setBadgeFromRecipient(recipient, it)
} ?: clearDrawable() } ?: clearDrawable()
} }
fun setBadgeFromRecipient(recipient: Recipient?, glideRequests: GlideRequests) { fun setBadgeFromRecipient(recipient: Recipient?, requestManager: RequestManager) {
if (recipient == null || recipient.badges.isEmpty()) { if (recipient == null || recipient.badges.isEmpty()) {
setBadge(null, glideRequests) setBadge(null, requestManager)
} else if (recipient.isSelf) { } else if (recipient.isSelf) {
val badge = recipient.featuredBadge val badge = recipient.featuredBadge
if (badge == null || !badge.visible || badge.isExpired()) { if (badge == null || !badge.visible || badge.isExpired()) {
setBadge(null, glideRequests) setBadge(null, requestManager)
} else { } else {
setBadge(badge, glideRequests) setBadge(badge, requestManager)
} }
} else { } else {
setBadge(recipient.featuredBadge, glideRequests) setBadge(recipient.featuredBadge, requestManager)
} }
} }
fun setBadge(badge: Badge?) { fun setBadge(badge: Badge?) {
getGlideRequests()?.let { getGlideRequestManager()?.let {
setBadge(badge, it) setBadge(badge, it)
} ?: clearDrawable() } ?: clearDrawable()
} }
fun setBadge(badge: Badge?, glideRequests: GlideRequests) { fun setBadge(badge: Badge?, requestManager: RequestManager) {
if (badge != null) { if (badge != null) {
glideRequests requestManager
.load(badge) .load(badge)
.downsample(DownsampleStrategy.NONE) .downsample(DownsampleStrategy.NONE)
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context))) .transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), badge.imageDensity, ThemeUtil.isDarkTheme(context)))
@@ -79,21 +79,21 @@ class BadgeImageView @JvmOverloads constructor(
isClickable = true isClickable = true
} else { } else {
glideRequests requestManager
.clear(this) .clear(this)
clearDrawable() clearDrawable()
} }
} }
fun setGiftBadge(badge: GiftBadge?, glideRequests: GlideRequests) { fun setGiftBadge(badge: GiftBadge?, requestManager: RequestManager) {
if (badge != null) { if (badge != null) {
glideRequests requestManager
.load(GiftBadgeModel(badge)) .load(GiftBadgeModel(badge))
.downsample(DownsampleStrategy.NONE) .downsample(DownsampleStrategy.NONE)
.transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), ScreenDensity.getBestDensityBucketForDevice(), ThemeUtil.isDarkTheme(context))) .transform(BadgeSpriteTransformation(BadgeSpriteTransformation.Size.fromInteger(badgeSize), ScreenDensity.getBestDensityBucketForDevice(), ThemeUtil.isDarkTheme(context)))
.into(this) .into(this)
} else { } else {
glideRequests requestManager
.clear(this) .clear(this)
clearDrawable() clearDrawable()
} }
@@ -106,9 +106,9 @@ class BadgeImageView @JvmOverloads constructor(
} }
} }
private fun getGlideRequests(): GlideRequests? { private fun getGlideRequestManager(): RequestManager? {
return try { return try {
GlideApp.with(this) Glide.with(this)
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
// View not attached to an activity or activity destroyed // View not attached to an activity or activity destroyed
null null
@@ -9,13 +9,13 @@ import android.widget.TextView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.use import androidx.core.content.res.use
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.bumptech.glide.RequestManager
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.signal.core.util.DimensionUnit import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.badges.gifts.Gifts.formatExpiry import org.thoughtcrime.securesms.badges.gifts.Gifts.formatExpiry
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
/** /**
@@ -50,7 +50,7 @@ class GiftMessageView @JvmOverloads constructor(
} }
} }
fun setGiftBadge(glideRequests: GlideRequests, giftBadge: GiftBadge, isOutgoing: Boolean, callback: Callback, fromRecipient: Recipient, toRecipient: Recipient) { fun setGiftBadge(requestManager: RequestManager, giftBadge: GiftBadge, isOutgoing: Boolean, callback: Callback, fromRecipient: Recipient, toRecipient: Recipient) {
descriptionView.text = giftBadge.formatExpiry(context) descriptionView.text = giftBadge.formatExpiry(context)
actionView.icon = null actionView.icon = null
actionView.setOnClickListener { callback.onViewGiftBadgeClicked() } actionView.setOnClickListener { callback.onViewGiftBadgeClicked() }
@@ -88,7 +88,7 @@ class GiftMessageView @JvmOverloads constructor(
) )
} }
badgeView.setGiftBadge(giftBadge, glideRequests) badgeView.setGiftBadge(giftBadge, requestManager)
} }
fun onGiftNotOpened() { fun onGiftNotOpened() {
@@ -7,6 +7,7 @@ import android.view.View
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.bumptech.glide.Glide
import com.bumptech.glide.load.Key import com.bumptech.glide.load.Key
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
@@ -14,7 +15,6 @@ import kotlinx.parcelize.Parcelize
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation import org.thoughtcrime.securesms.badges.glide.BadgeSpriteTransformation
import org.thoughtcrime.securesms.components.settings.PreferenceModel import org.thoughtcrime.securesms.components.settings.PreferenceModel
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.ThemeUtil import org.thoughtcrime.securesms.util.ThemeUtil
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -130,7 +130,7 @@ data class Badge(
badge.alpha = if (model.badge.isExpired() || model.isFaded) 0.5f else 1f badge.alpha = if (model.badge.isExpired() || model.isFaded) 0.5f else 1f
GlideApp.with(badge) Glide.with(badge)
.load(model.badge) .load(model.badge)
.downsample(DownsampleStrategy.NONE) .downsample(DownsampleStrategy.NONE)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
@@ -2,10 +2,10 @@ package org.thoughtcrime.securesms.badges.models
import android.view.View import android.view.View
import android.widget.TextView import android.widget.TextView
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
@@ -49,7 +49,7 @@ object BadgeDisplay112 {
override fun bind(model: GiftModel) { override fun bind(model: GiftModel) {
titleView.visible = false titleView.visible = false
badgeImageView.setGiftBadge(model.giftBadge, GlideApp.with(badgeImageView)) badgeImageView.setGiftBadge(model.giftBadge, Glide.with(badgeImageView))
} }
} }
} }
@@ -58,7 +58,7 @@ object CallLinks {
return false return false
} }
if (!url.startsWith(HTTPS_LINK_PREFIX) || !url.startsWith(SNGL_LINK_PREFIX)) { if (!url.startsWith(HTTPS_LINK_PREFIX) && !url.startsWith(SNGL_LINK_PREFIX)) {
return false return false
} }
@@ -7,13 +7,13 @@ import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.widget.TextViewCompat import androidx.core.widget.TextViewCompat
import com.bumptech.glide.Glide
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.CallTable import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTypes import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.databinding.CallLogAdapterItemBinding import org.thoughtcrime.securesms.databinding.CallLogAdapterItemBinding
import org.thoughtcrime.securesms.databinding.CallLogCreateCallLinkItemBinding import org.thoughtcrime.securesms.databinding.CallLogCreateCallLinkItemBinding
import org.thoughtcrime.securesms.databinding.ConversationListItemClearFilterBinding import org.thoughtcrime.securesms.databinding.ConversationListItemClearFilterBinding
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.SearchUtil import org.thoughtcrime.securesms.util.SearchUtil
@@ -272,7 +272,7 @@ class CallLogAdapter(
} }
private fun presentRecipientDetails(recipient: Recipient, searchQuery: String?) { private fun presentRecipientDetails(recipient: Recipient, searchQuery: String?) {
binding.callRecipientAvatar.setAvatar(GlideApp.with(binding.callRecipientAvatar), recipient, true) binding.callRecipientAvatar.setAvatar(Glide.with(binding.callRecipientAvatar), recipient, true)
binding.callRecipientBadge.setBadgeFromRecipient(recipient) binding.callRecipientBadge.setBadgeFromRecipient(recipient)
binding.callRecipientName.text = if (searchQuery != null) { binding.callRecipientName.text = if (searchQuery != null) {
SearchUtil.getHighlightedSpan( SearchUtil.getHighlightedSpan(
@@ -305,7 +305,7 @@ class CallLogAdapter(
val color = ContextCompat.getColor( val color = ContextCompat.getColor(
context, context,
if (call.record.event == CallTable.Event.MISSED) { if (call.record.event.isMissedCall()) {
R.color.signal_colorError R.color.signal_colorError
} else { } else {
R.color.signal_colorOnSurfaceVariant R.color.signal_colorOnSurfaceVariant
@@ -375,7 +375,7 @@ class CallLogAdapter(
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16 MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16
MessageTypes.GROUP_CALL_TYPE -> when { MessageTypes.GROUP_CALL_TYPE -> when {
call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16 call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16
call.event == CallTable.Event.MISSED -> R.drawable.symbol_missed_incoming_compact_16 call.event.isMissedCall() -> R.drawable.symbol_missed_incoming_compact_16
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16 call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16 call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16 call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16
@@ -389,8 +389,8 @@ class CallLogAdapter(
@StringRes @StringRes
private fun getCallStateStringRes(call: CallTable.Call): Int { private fun getCallStateStringRes(call: CallTable.Call): Int {
return when (call.messageType) { return when (call.messageType) {
MessageTypes.MISSED_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__missed MessageTypes.MISSED_VIDEO_CALL_TYPE,
MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__missed MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__missed_notification_profile
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__incoming MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__incoming
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__incoming MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__incoming
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
@@ -398,6 +398,7 @@ class CallLogAdapter(
MessageTypes.GROUP_CALL_TYPE -> when { MessageTypes.GROUP_CALL_TYPE -> when {
call.type == CallTable.Type.AD_HOC_CALL -> R.string.CallLogAdapter__call_link call.type == CallTable.Type.AD_HOC_CALL -> R.string.CallLogAdapter__call_link
call.event == CallTable.Event.MISSED -> R.string.CallLogAdapter__missed call.event == CallTable.Event.MISSED -> R.string.CallLogAdapter__missed
call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallLogAdapter__missed_notification_profile
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
call.direction == CallTable.Direction.INCOMING -> R.string.CallLogAdapter__incoming call.direction == CallTable.Direction.INCOMING -> R.string.CallLogAdapter__incoming
call.direction == CallTable.Direction.OUTGOING -> R.string.CallLogAdapter__outgoing call.direction == CallTable.Direction.OUTGOING -> R.string.CallLogAdapter__outgoing
@@ -16,9 +16,10 @@ import androidx.annotation.IdRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView; import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.SlidesClickedListener;
@@ -30,9 +31,9 @@ import java.util.List;
public class AlbumThumbnailView extends FrameLayout { public class AlbumThumbnailView extends FrameLayout {
private @Nullable SlideClickListener thumbnailClickListener; private @Nullable SlideClickListener thumbnailClickListener;
private @Nullable SlidesClickedListener downloadClickListener; private @Nullable SlidesClickedListener startTransferClickListener;
private @Nullable SlidesClickedListener cancelDownloadClickListener; private @Nullable SlidesClickedListener cancelTransferClickListener;
private @Nullable SlideClickListener playVideoClickListener; private @Nullable SlideClickListener playVideoClickListener;
private int currentSizeClass; private int currentSizeClass;
@@ -65,23 +66,23 @@ public class AlbumThumbnailView extends FrameLayout {
transferControlsStub = new Stub<>(findViewById(R.id.album_transfer_controls_stub)); transferControlsStub = new Stub<>(findViewById(R.id.album_transfer_controls_stub));
} }
public void setSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides, boolean showControls) { public void setSlides(@NonNull RequestManager requestManager, @NonNull List<Slide> slides, boolean showControls) {
if (slides.size() < 2) { if (slides.size() < 2) {
throw new IllegalStateException("Provided less than two slides."); throw new IllegalStateException("Provided less than two slides.");
} }
if (showControls) { if (showControls) {
transferControlsStub.get().setShowSecondaryText(true); transferControlsStub.get().setShowSecondaryText(true);
transferControlsStub.get().setDownloadClickListener( transferControlsStub.get().setTransferClickListener(
v -> { v -> {
if (downloadClickListener != null) { if (startTransferClickListener != null) {
downloadClickListener.onClick(v, slides); startTransferClickListener.onClick(v, slides);
} }
}); });
transferControlsStub.get().setCancelClickListener( transferControlsStub.get().setCancelClickListener(
v -> { v -> {
if (cancelDownloadClickListener != null) { if (cancelTransferClickListener != null) {
cancelDownloadClickListener.onClick(v, slides); cancelTransferClickListener.onClick(v, slides);
} }
}); });
transferControlsStub.get().setSlides(slides); transferControlsStub.get().setSlides(slides);
@@ -98,7 +99,7 @@ public class AlbumThumbnailView extends FrameLayout {
currentSizeClass = sizeClass; currentSizeClass = sizeClass;
} }
showSlides(glideRequests, slides); showSlides(requestManager, slides);
applyCorners(); applyCorners();
forceLayout(); forceLayout();
} }
@@ -117,12 +118,12 @@ public class AlbumThumbnailView extends FrameLayout {
thumbnailClickListener = listener; thumbnailClickListener = listener;
} }
public void setDownloadClickListener(SlidesClickedListener listener) { public void setStartTransferClickListener(SlidesClickedListener listener) {
this.downloadClickListener = listener; this.startTransferClickListener = listener;
} }
public void setCancelDownloadClickListener(SlidesClickedListener listener) { public void setCancelTransferClickListener(SlidesClickedListener listener) {
this.cancelDownloadClickListener = listener; this.cancelTransferClickListener = listener;
} }
public void setPlayVideoClickListener(SlideClickListener listener) { public void setPlayVideoClickListener(SlideClickListener listener) {
@@ -261,21 +262,21 @@ public class AlbumThumbnailView extends FrameLayout {
applyCornersForSizeClass5(); applyCornersForSizeClass5();
} }
private void showSlides(@NonNull GlideRequests glideRequests, @NonNull List<Slide> slides) { private void showSlides(@NonNull RequestManager requestManager, @NonNull List<Slide> slides) {
boolean showControls = TransferControlView.containsPlayableSlides(slides); boolean showControls = TransferControlView.containsPlayableSlides(slides);
setSlide(glideRequests, slides.get(0), R.id.album_cell_1, showControls); setSlide(requestManager, slides.get(0), R.id.album_cell_1, showControls);
setSlide(glideRequests, slides.get(1), R.id.album_cell_2, showControls); setSlide(requestManager, slides.get(1), R.id.album_cell_2, showControls);
if (slides.size() >= 3) { if (slides.size() >= 3) {
setSlide(glideRequests, slides.get(2), R.id.album_cell_3, showControls); setSlide(requestManager, slides.get(2), R.id.album_cell_3, showControls);
} }
if (slides.size() >= 4) { if (slides.size() >= 4) {
setSlide(glideRequests, slides.get(3), R.id.album_cell_4, showControls); setSlide(requestManager, slides.get(3), R.id.album_cell_4, showControls);
} }
if (slides.size() >= 5) { if (slides.size() >= 5) {
setSlide(glideRequests, slides.get(4), R.id.album_cell_5, showControls && slides.size() == 5); setSlide(requestManager, slides.get(4), R.id.album_cell_5, showControls && slides.size() == 5);
} }
if (slides.size() > 5) { if (slides.size() > 5) {
@@ -284,17 +285,17 @@ public class AlbumThumbnailView extends FrameLayout {
} }
} }
private void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide, @IdRes int id, boolean showControls) { private void setSlide(@NonNull RequestManager requestManager, @NonNull Slide slide, @IdRes int id, boolean showControls) {
ThumbnailView cell = findViewById(id); ThumbnailView cell = findViewById(id);
cell.showSecondaryText(false); cell.showSecondaryText(false);
cell.setThumbnailClickListener(defaultThumbnailClickListener); cell.setThumbnailClickListener(defaultThumbnailClickListener);
cell.setDownloadClickListener(downloadClickListener); cell.setStartTransferClickListener(startTransferClickListener);
cell.setCancelDownloadClickListener(cancelDownloadClickListener); cell.setCancelTransferClickListener(cancelTransferClickListener);
if (MediaUtil.isInstantVideoSupported(slide)) { if (MediaUtil.isInstantVideoSupported(slide)) {
cell.setPlayVideoClickListener(playVideoClickListener); cell.setPlayVideoClickListener(playVideoClickListener);
} }
cell.setOnLongClickListener(defaultLongClickListener); cell.setOnLongClickListener(defaultLongClickListener);
cell.setImageResource(glideRequests, slide, showControls, false); cell.setImageResource(requestManager, slide, showControls, false);
} }
private int sizeClass(int size) { private int sizeClass(int size) {
@@ -15,6 +15,9 @@ import androidx.annotation.Px;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.FragmentActivity; import androidx.fragment.app.FragmentActivity;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.DataSource; import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.MultiTransformation; import com.bumptech.glide.load.MultiTransformation;
import com.bumptech.glide.load.Transformation; import com.bumptech.glide.load.Transformation;
@@ -37,9 +40,6 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.conversation.colors.ChatColors;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment; import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.util.AvatarUtil; import org.thoughtcrime.securesms.util.AvatarUtil;
@@ -129,10 +129,10 @@ public final class AvatarImageView extends AppCompatImageView {
*/ */
public void setRecipient(@NonNull Recipient recipient, boolean quickContactEnabled) { public void setRecipient(@NonNull Recipient recipient, boolean quickContactEnabled) {
if (recipient.isSelf()) { if (recipient.isSelf()) {
setAvatar(GlideApp.with(this), null, quickContactEnabled); setAvatar(Glide.with(this), null, quickContactEnabled);
AvatarUtil.loadIconIntoImageView(recipient, this); AvatarUtil.loadIconIntoImageView(recipient, this);
} else { } else {
setAvatar(GlideApp.with(this), recipient, quickContactEnabled); setAvatar(Glide.with(this), recipient, quickContactEnabled);
} }
} }
@@ -144,21 +144,21 @@ public final class AvatarImageView extends AppCompatImageView {
* Shows self as the note to self icon. * Shows self as the note to self icon.
*/ */
public void setAvatar(@Nullable Recipient recipient) { public void setAvatar(@Nullable Recipient recipient) {
setAvatar(GlideApp.with(this), recipient, false); setAvatar(Glide.with(this), recipient, false);
} }
/** /**
* Shows self as the profile avatar. * Shows self as the profile avatar.
*/ */
public void setAvatarUsingProfile(@Nullable Recipient recipient) { public void setAvatarUsingProfile(@Nullable Recipient recipient) {
setAvatar(GlideApp.with(this), recipient, false, true); setAvatar(Glide.with(this), recipient, false, true);
} }
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) { public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, boolean quickContactEnabled) {
setAvatar(requestManager, recipient, quickContactEnabled, false); setAvatar(requestManager, recipient, quickContactEnabled, false);
} }
public void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) { public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, boolean quickContactEnabled, boolean useSelfProfileAvatar) {
setAvatar(requestManager, recipient, new AvatarOptions.Builder(this) setAvatar(requestManager, recipient, new AvatarOptions.Builder(this)
.withUseSelfProfileAvatar(useSelfProfileAvatar) .withUseSelfProfileAvatar(useSelfProfileAvatar)
.withQuickContactEnabled(quickContactEnabled) .withQuickContactEnabled(quickContactEnabled)
@@ -166,10 +166,10 @@ public final class AvatarImageView extends AppCompatImageView {
} }
private void setAvatar(@Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) { private void setAvatar(@Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
setAvatar(GlideApp.with(this), recipient, avatarOptions); setAvatar(Glide.with(this), recipient, avatarOptions);
} }
private void setAvatar(@NonNull GlideRequests requestManager, @Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) { private void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient, @NonNull AvatarOptions avatarOptions) {
if (recipient != null) { if (recipient != null) {
RecipientContactPhoto photo = (recipient.isSelf() && avatarOptions.useSelfProfileAvatar) ? new RecipientContactPhoto(recipient, RecipientContactPhoto photo = (recipient.isSelf() && avatarOptions.useSelfProfileAvatar) ? new RecipientContactPhoto(recipient,
new ProfileContactPhoto(Recipient.self())) new ProfileContactPhoto(Recipient.self()))
@@ -199,7 +199,7 @@ public final class AvatarImageView extends AppCompatImageView {
transforms.add(new CircleCrop()); transforms.add(new CircleCrop());
blurred = shouldBlur; blurred = shouldBlur;
GlideRequest<Drawable> request = requestManager.load(photo.contactPhoto) RequestBuilder<Drawable> request = requestManager.load(photo.contactPhoto)
.dontAnimate() .dontAnimate()
.fallback(fallbackContactPhotoDrawable) .fallback(fallbackContactPhotoDrawable)
.error(fallbackContactPhotoDrawable) .error(fallbackContactPhotoDrawable)
@@ -265,7 +265,7 @@ public final class AvatarImageView extends AppCompatImageView {
.getPhotoForGroup() .getPhotoForGroup()
.asDrawable(getContext(), color); .asDrawable(getContext(), color);
GlideApp.with(this) Glide.with(this)
.load(avatarBytes) .load(avatarBytes)
.dontAnimate() .dontAnimate()
.fallback(fallback) .fallback(fallback)
@@ -9,11 +9,9 @@ import android.widget.ImageView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.resource.bitmap.CenterInside;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.SlidesClickedListener;
@@ -55,15 +53,15 @@ public class BorderlessImageView extends FrameLayout {
image.setOnLongClickListener(l); image.setOnLongClickListener(l);
} }
public void setSlide(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { public void setSlide(@NonNull RequestManager requestManager, @NonNull Slide slide) {
boolean showControls = slide.asAttachment().getUri() == null; boolean showControls = slide.asAttachment().getUri() == null;
if (slide.hasSticker()) { if (slide.hasSticker()) {
image.setScaleType(ImageView.ScaleType.FIT_CENTER); image.setScaleType(ImageView.ScaleType.FIT_CENTER);
image.setImageResource(glideRequests, slide, showControls, false); image.setImageResource(requestManager, slide, showControls, false);
} else { } else {
image.setScaleType(ImageView.ScaleType.CENTER_CROP); image.setScaleType(ImageView.ScaleType.CENTER_CROP);
image.setImageResource(glideRequests, slide, showControls, false, slide.asAttachment().getWidth(), slide.asAttachment().getHeight()); image.setImageResource(requestManager, slide, showControls, false, slide.asAttachment().width, slide.asAttachment().height);
} }
missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE); missingShade.setVisibility(showControls ? View.VISIBLE : View.GONE);
@@ -74,6 +72,6 @@ public class BorderlessImageView extends FrameLayout {
} }
public void setDownloadClickListener(@NonNull SlidesClickedListener listener) { public void setDownloadClickListener(@NonNull SlidesClickedListener listener) {
image.setDownloadClickListener(listener); image.setStartTransferClickListener(listener);
} }
} }
@@ -114,23 +114,23 @@ public final class ContactFilterView extends FrameLayout {
int defStyle) int defStyle)
{ {
final TypedArray attributes = context.obtainStyledAttributes(attrs, final TypedArray attributes = context.obtainStyledAttributes(attrs,
R.styleable.ContactFilterToolbar, R.styleable.ContactFilterView,
defStyle, defStyle,
0); 0);
int styleResource = attributes.getResourceId(R.styleable.ContactFilterToolbar_searchTextStyle, -1); int styleResource = attributes.getResourceId(R.styleable.ContactFilterView_searchTextStyle, -1);
if (styleResource != -1) { if (styleResource != -1) {
TextViewCompat.setTextAppearance(searchText, styleResource); TextViewCompat.setTextAppearance(searchText, styleResource);
} }
if (!attributes.getBoolean(R.styleable.ContactFilterToolbar_showDialpad, true)) { if (!attributes.getBoolean(R.styleable.ContactFilterView_showDialpad, true)) {
dialpadToggle.setVisibility(GONE); dialpadToggle.setVisibility(GONE);
} }
if (attributes.getBoolean(R.styleable.ContactFilterToolbar_cfv_autoFocus, true)) { if (attributes.getBoolean(R.styleable.ContactFilterView_cfv_autoFocus, true)) {
searchText.requestFocus(); searchText.requestFocus();
} }
int backgroundRes = attributes.getResourceId(R.styleable.ContactFilterToolbar_cfv_background, -1); int backgroundRes = attributes.getResourceId(R.styleable.ContactFilterView_cfv_background, -1);
if (backgroundRes != -1) { if (backgroundRes != -1) {
findViewById(R.id.background_holder).setBackgroundResource(backgroundRes); findViewById(R.id.background_holder).setBackgroundResource(backgroundRes);
} }
@@ -31,8 +31,8 @@ import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode; import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode;
import org.thoughtcrime.securesms.conversation.v2.computed.FormattedDate; import org.thoughtcrime.securesms.conversation.v2.computed.FormattedDate;
import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@@ -302,7 +302,9 @@ public class ConversationItemFooter extends ConstraintLayout {
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale, @NonNull ConversationItemDisplayMode displayMode) { private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale, @NonNull ConversationItemDisplayMode displayMode) {
dateView.forceLayout(); dateView.forceLayout();
if (messageRecord.isFailed()) { if (messageRecord.isMediaPending()) {
dateView.setText(null);
} else if (messageRecord.isFailed()) {
int errorMsg; int errorMsg;
if (messageRecord.hasFailedWithNetworkFailures()) { if (messageRecord.hasFailedWithNetworkFailures()) {
errorMsg = R.string.ConversationItem_error_network_not_delivered; errorMsg = R.string.ConversationItem_error_network_not_delivered;
@@ -16,10 +16,10 @@ import androidx.annotation.ColorInt
import androidx.annotation.Px import androidx.annotation.Px
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.bumptech.glide.RequestManager
import org.signal.core.util.dp import org.signal.core.util.dp
import org.signal.core.util.getParcelableCompat import org.signal.core.util.getParcelableCompat
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideClickListener import org.thoughtcrime.securesms.mms.SlideClickListener
import org.thoughtcrime.securesms.mms.SlidesClickedListener import org.thoughtcrime.securesms.mms.SlidesClickedListener
@@ -192,7 +192,7 @@ class ConversationItemThumbnail @JvmOverloads constructor(
@UiThread @UiThread
fun setImageResource( fun setImageResource(
glideRequests: GlideRequests, requestManager: RequestManager,
slides: List<Slide>, slides: List<Slide>,
showControls: Boolean, showControls: Boolean,
isPreview: Boolean isPreview: Boolean
@@ -223,7 +223,7 @@ class ConversationItemThumbnail @JvmOverloads constructor(
val attachment = slides[0].asAttachment() val attachment = slides[0].asAttachment()
thumbnail.get().setImageResource(glideRequests, slides[0], showControls, isPreview, attachment.width, attachment.height) thumbnail.get().setImageResource(requestManager, slides[0], showControls, isPreview, attachment.width, attachment.height)
touchDelegate = thumbnail.get().touchDelegate touchDelegate = thumbnail.get().touchDelegate
} else { } else {
state = state.copy( state = state.copy(
@@ -232,7 +232,7 @@ class ConversationItemThumbnail @JvmOverloads constructor(
) )
state.applyState(thumbnail, album) state.applyState(thumbnail, album)
album.get().setSlides(glideRequests, slides, showControls) album.get().setSlides(requestManager, slides, showControls)
touchDelegate = album.get().touchDelegate touchDelegate = album.get().touchDelegate
} }
} }
@@ -251,10 +251,10 @@ class ConversationItemThumbnail @JvmOverloads constructor(
state.applyState(thumbnail, album) state.applyState(thumbnail, album)
} }
fun setDownloadClickListener(listener: SlidesClickedListener?) { fun setStartTransferClickListener(listener: SlidesClickedListener?) {
state = state.copy( state = state.copy(
thumbnailViewState = state.thumbnailViewState.copy(downloadClickListener = listener), thumbnailViewState = state.thumbnailViewState.copy(startTransferClickListener = listener),
albumViewState = state.albumViewState.copy(downloadClickListener = listener) albumViewState = state.albumViewState.copy(startTransferClickListener = listener)
) )
state.applyState(thumbnail, album) state.applyState(thumbnail, album)
@@ -269,10 +269,10 @@ class ConversationItemThumbnail @JvmOverloads constructor(
state.applyState(thumbnail, album) state.applyState(thumbnail, album)
} }
fun setCancelDownloadClickListener(listener: SlidesClickedListener?) { fun setCancelTransferClickListener(listener: SlidesClickedListener?) {
state = state.copy( state = state.copy(
thumbnailViewState = state.thumbnailViewState.copy(cancelDownloadClickListener = listener), thumbnailViewState = state.thumbnailViewState.copy(cancelTransferClickListener = listener),
albumViewState = state.albumViewState.copy(cancelDownloadClickListener = listener) albumViewState = state.albumViewState.copy(cancelTransferClickListener = listener)
) )
state.applyState(thumbnail, album) state.applyState(thumbnail, album)
@@ -34,9 +34,9 @@ data class ConversationItemThumbnailState(
@IgnoredOnParcel @IgnoredOnParcel
private val clickListener: SlideClickListener? = null, private val clickListener: SlideClickListener? = null,
@IgnoredOnParcel @IgnoredOnParcel
private val downloadClickListener: SlidesClickedListener? = null, private val startTransferClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel @IgnoredOnParcel
private val cancelDownloadClickListener: SlidesClickedListener? = null, private val cancelTransferClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel @IgnoredOnParcel
private val playVideoClickListener: SlideClickListener? = null, private val playVideoClickListener: SlideClickListener? = null,
@IgnoredOnParcel @IgnoredOnParcel
@@ -63,8 +63,8 @@ data class ConversationItemThumbnailState(
thumbnailView.get().isClickable = clickable thumbnailView.get().isClickable = clickable
thumbnailView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft) thumbnailView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
thumbnailView.get().setThumbnailClickListener(clickListener) thumbnailView.get().setThumbnailClickListener(clickListener)
thumbnailView.get().setDownloadClickListener(downloadClickListener) thumbnailView.get().setStartTransferClickListener(startTransferClickListener)
thumbnailView.get().setCancelDownloadClickListener(cancelDownloadClickListener) thumbnailView.get().setCancelTransferClickListener(cancelTransferClickListener)
thumbnailView.get().setPlayVideoClickListener(playVideoClickListener) thumbnailView.get().setPlayVideoClickListener(playVideoClickListener)
thumbnailView.get().setOnLongClickListener(longClickListener) thumbnailView.get().setOnLongClickListener(longClickListener)
thumbnailView.get().setBounds(minWidth, maxWidth, minHeight, maxHeight) thumbnailView.get().setBounds(minWidth, maxWidth, minHeight, maxHeight)
@@ -78,9 +78,9 @@ data class ConversationItemThumbnailState(
@IgnoredOnParcel @IgnoredOnParcel
private val clickListener: SlideClickListener? = null, private val clickListener: SlideClickListener? = null,
@IgnoredOnParcel @IgnoredOnParcel
private val downloadClickListener: SlidesClickedListener? = null, private val startTransferClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel @IgnoredOnParcel
private val cancelDownloadClickListener: SlidesClickedListener? = null, private val cancelTransferClickListener: SlidesClickedListener? = null,
@IgnoredOnParcel @IgnoredOnParcel
private val playVideoClickListener: SlideClickListener? = null, private val playVideoClickListener: SlideClickListener? = null,
@IgnoredOnParcel @IgnoredOnParcel
@@ -103,8 +103,8 @@ data class ConversationItemThumbnailState(
albumView.get().isClickable = clickable albumView.get().isClickable = clickable
albumView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft) albumView.get().setRadii(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
albumView.get().setThumbnailClickListener(clickListener) albumView.get().setThumbnailClickListener(clickListener)
albumView.get().setDownloadClickListener(downloadClickListener) albumView.get().setStartTransferClickListener(startTransferClickListener)
albumView.get().setCancelDownloadClickListener(cancelDownloadClickListener) albumView.get().setCancelTransferClickListener(cancelTransferClickListener)
albumView.get().setPlayVideoClickListener(playVideoClickListener) albumView.get().setPlayVideoClickListener(playVideoClickListener)
albumView.get().setOnLongClickListener(longClickListener) albumView.get().setOnLongClickListener(longClickListener)
albumView.get().setCellBackgroundColor(cellBackgroundColor) albumView.get().setCellBackgroundColor(cellBackgroundColor)
@@ -11,9 +11,10 @@ import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.bumptech.glide.RequestManager;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.badges.BadgeImageView; import org.thoughtcrime.securesms.badges.BadgeImageView;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import java.util.List; import java.util.List;
@@ -49,7 +50,7 @@ public class ConversationTypingView extends ConstraintLayout {
indicator = findViewById(R.id.typing_indicator); indicator = findViewById(R.id.typing_indicator);
} }
public void setTypists(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists, boolean isGroupThread, boolean hasWallpaper) { public void setTypists(@NonNull RequestManager requestManager, @NonNull List<Recipient> typists, boolean isGroupThread, boolean hasWallpaper) {
if (typists.isEmpty()) { if (typists.isEmpty()) {
indicator.stopAnimation(); indicator.stopAnimation();
return; return;
@@ -64,7 +65,7 @@ public class ConversationTypingView extends ConstraintLayout {
typistCount.setVisibility(GONE); typistCount.setVisibility(GONE);
if (isGroupThread) { if (isGroupThread) {
presentGroupThreadAvatars(glideRequests, typists); presentGroupThreadAvatars(requestManager, typists);
} }
if (hasWallpaper) { if (hasWallpaper) {
@@ -84,23 +85,23 @@ public class ConversationTypingView extends ConstraintLayout {
return indicator.isActive(); return indicator.isActive();
} }
private void presentGroupThreadAvatars(@NonNull GlideRequests glideRequests, @NonNull List<Recipient> typists) { private void presentGroupThreadAvatars(@NonNull RequestManager requestManager, @NonNull List<Recipient> typists) {
avatar1.setAvatar(glideRequests, typists.get(0), typists.size() == 1); avatar1.setAvatar(requestManager, typists.get(0), typists.size() == 1);
avatar1.setVisibility(VISIBLE); avatar1.setVisibility(VISIBLE);
badge1.setBadgeFromRecipient(typists.get(0), glideRequests); badge1.setBadgeFromRecipient(typists.get(0), requestManager);
badge1.setVisibility(VISIBLE); badge1.setVisibility(VISIBLE);
if (typists.size() > 1) { if (typists.size() > 1) {
avatar2.setAvatar(glideRequests, typists.get(1), false); avatar2.setAvatar(requestManager, typists.get(1), false);
avatar2.setVisibility(VISIBLE); avatar2.setVisibility(VISIBLE);
badge2.setBadgeFromRecipient(typists.get(1), glideRequests); badge2.setBadgeFromRecipient(typists.get(1), requestManager);
badge2.setVisibility(VISIBLE); badge2.setVisibility(VISIBLE);
} }
if (typists.size() == 3) { if (typists.size() == 3) {
avatar3.setAvatar(glideRequests, typists.get(2), false); avatar3.setAvatar(requestManager, typists.get(2), false);
avatar3.setVisibility(VISIBLE); avatar3.setVisibility(VISIBLE);
badge3.setBadgeFromRecipient(typists.get(2), glideRequests); badge3.setBadgeFromRecipient(typists.get(2), requestManager);
badge3.setVisibility(VISIBLE); badge3.setVisibility(VISIBLE);
} }
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import android.content.res.TypedArray; import android.content.res.TypedArray;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.util.AttributeSet; import android.util.AttributeSet;
@@ -146,7 +147,7 @@ public class DeliveryStatusView extends AppCompatImageView {
setVisibility(View.VISIBLE); setVisibility(View.VISIBLE);
ViewUtil.setPaddingStart(this, 0); ViewUtil.setPaddingStart(this, 0);
ViewUtil.setPaddingEnd(this, horizontalPadding); ViewUtil.setPaddingEnd(this, horizontalPadding);
setImageResource(R.drawable.ic_delivery_status_sending); setImageResource(R.drawable.symbol_messagestatus_sending_24);
updateContentDescription(); updateContentDescription();
} }
@@ -156,7 +157,7 @@ public class DeliveryStatusView extends AppCompatImageView {
ViewUtil.setPaddingStart(this, horizontalPadding); ViewUtil.setPaddingStart(this, horizontalPadding);
ViewUtil.setPaddingEnd(this, 0); ViewUtil.setPaddingEnd(this, 0);
clearAnimation(); clearAnimation();
setImageResource(R.drawable.ic_delivery_status_sent); setImageResource(R.drawable.symbol_messagestatus_sent_24);
updateContentDescription(); updateContentDescription();
} }
@@ -166,7 +167,7 @@ public class DeliveryStatusView extends AppCompatImageView {
ViewUtil.setPaddingStart(this, horizontalPadding); ViewUtil.setPaddingStart(this, horizontalPadding);
ViewUtil.setPaddingEnd(this, 0); ViewUtil.setPaddingEnd(this, 0);
clearAnimation(); clearAnimation();
setImageResource(R.drawable.ic_delivery_status_delivered); setImageResource(R.drawable.symbol_messagestatus_delivered_24);
updateContentDescription(); updateContentDescription();
} }
@@ -176,12 +177,12 @@ public class DeliveryStatusView extends AppCompatImageView {
ViewUtil.setPaddingStart(this, horizontalPadding); ViewUtil.setPaddingStart(this, horizontalPadding);
ViewUtil.setPaddingEnd(this, 0); ViewUtil.setPaddingEnd(this, 0);
clearAnimation(); clearAnimation();
setImageResource(R.drawable.ic_delivery_status_read); setImageResource(R.drawable.symbol_messagestatus_read_24);
updateContentDescription(); updateContentDescription();
} }
public void setTint(int color) { public void setTint(int color) {
setColorFilter(color); setColorFilter(color, PorterDuff.Mode.SRC_IN);
} }
private void updateContentDescription() { private void updateContentDescription() {
@@ -1,12 +1,11 @@
package org.thoughtcrime.securesms.components; package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.util.AttributeSet; import android.util.AttributeSet;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
@@ -15,11 +14,10 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView; import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.ContextUtil; import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.SpanUtil; import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.ViewUtil; import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.Objects;
public class FromTextView extends SimpleEmojiTextView { public class FromTextView extends SimpleEmojiTextView {
private static final String TAG = Log.tag(FromTextView.class); private static final String TAG = Log.tag(FromTextView.class);
@@ -71,17 +69,23 @@ public class FromTextView extends SimpleEmojiTextView {
setText(builder); setText(builder);
if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0); if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(getBlocked(), null, null, null);
else if (recipient.isMuted()) setCompoundDrawablesRelativeWithIntrinsicBounds(getMuted(), null, null, null); else if (recipient.isMuted()) setCompoundDrawablesRelativeWithIntrinsicBounds(getMuted(), null, null, null);
else setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0); else setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
} }
private Drawable getBlocked() {
return getDrawable(R.drawable.symbol_block_16);
}
private Drawable getMuted() { private Drawable getMuted() {
Drawable mutedDrawable = Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.ic_bell_disabled_16)); return getDrawable(R.drawable.ic_bell_disabled_16);
}
private Drawable getDrawable(@DrawableRes int drawable) {
Drawable mutedDrawable = ContextUtil.requireDrawable(getContext(), drawable);
mutedDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18)); mutedDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
mutedDrawable.setColorFilter(new PorterDuffColorFilter(ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary), PorterDuff.Mode.SRC_IN)); DrawableUtil.tint(mutedDrawable, ContextCompat.getColor(getContext(), R.color.signal_icon_tint_secondary));
return mutedDrawable; return mutedDrawable;
} }
} }
@@ -32,6 +32,8 @@ import androidx.lifecycle.Observer;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.signal.core.util.ThreadUtil; import org.signal.core.util.ThreadUtil;
@@ -59,8 +61,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -184,7 +184,7 @@ public class InputPanel extends ConstraintLayout
} }
}); });
stickerSuggestionAdapter = new ConversationStickerSuggestionAdapter(GlideApp.with(this), this); stickerSuggestionAdapter = new ConversationStickerSuggestionAdapter(Glide.with(this), this);
stickerSuggestion.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false)); stickerSuggestion.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
stickerSuggestion.setAdapter(stickerSuggestionAdapter); stickerSuggestion.setAdapter(stickerSuggestionAdapter);
@@ -212,14 +212,14 @@ public class InputPanel extends ConstraintLayout
composeText.setMediaListener(listener); composeText.setMediaListener(listener);
} }
public void setQuote(@NonNull GlideRequests glideRequests, public void setQuote(@NonNull RequestManager requestManager,
long id, long id,
@NonNull Recipient author, @NonNull Recipient author,
@Nullable CharSequence body, @Nullable CharSequence body,
@NonNull SlideDeck attachments, @NonNull SlideDeck attachments,
@NonNull QuoteModel.Type quoteType) @NonNull QuoteModel.Type quoteType)
{ {
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, null, quoteType); this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType);
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight() int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
: 0; : 0;
@@ -325,10 +325,10 @@ public class InputPanel extends ConstraintLayout
this.linkPreview.setNoPreview(customError); this.linkPreview.setNoPreview(customError);
} }
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull Optional<LinkPreview> preview) { public void setLinkPreview(@NonNull RequestManager requestManager, @NonNull Optional<LinkPreview> preview) {
if (preview.isPresent()) { if (preview.isPresent()) {
this.linkPreview.setVisibility(View.VISIBLE); this.linkPreview.setVisibility(View.VISIBLE);
this.linkPreview.setLinkPreview(glideRequests, preview.get(), true); this.linkPreview.setLinkPreview(requestManager, preview.get(), true);
} else { } else {
this.linkPreview.setVisibility(View.GONE); this.linkPreview.setVisibility(View.GONE);
} }
@@ -404,7 +404,7 @@ public class InputPanel extends ConstraintLayout
quoteView.setWallpaperEnabled(enabled); quoteView.setWallpaperEnabled(enabled);
} }
public void enterEditMessageMode(@NonNull GlideRequests glideRequests, @NonNull ConversationMessage conversationMessageToEdit, boolean fromDraft) { public void enterEditMessageMode(@NonNull RequestManager requestManager, @NonNull ConversationMessage conversationMessageToEdit, boolean fromDraft) {
SpannableString textToEdit = conversationMessageToEdit.getDisplayBody(getContext()); SpannableString textToEdit = conversationMessageToEdit.getDisplayBody(getContext());
if (!fromDraft) { if (!fromDraft) {
MessageStyler.convertSpoilersToComposeMode(textToEdit); MessageStyler.convertSpoilersToComposeMode(textToEdit);
@@ -415,14 +415,14 @@ public class InputPanel extends ConstraintLayout
if (quote == null) { if (quote == null) {
clearQuote(); clearQuote();
} else { } else {
setQuote(glideRequests, quote.getId(), Recipient.resolved(quote.getAuthor()), quote.getDisplayText(), quote.getAttachment(), quote.getQuoteType()); setQuote(requestManager, quote.getId(), Recipient.resolved(quote.getAuthor()), quote.getDisplayText(), quote.getAttachment(), quote.getQuoteType());
} }
this.messageToEdit = conversationMessageToEdit.getMessageRecord(); this.messageToEdit = conversationMessageToEdit.getMessageRecord();
updateEditModeThumbnail(glideRequests); updateEditModeThumbnail(requestManager);
updateEditModeUi(); updateEditModeUi();
} }
private void updateEditModeThumbnail(@NonNull GlideRequests glideRequests) { private void updateEditModeThumbnail(@NonNull RequestManager requestManager) {
if (messageToEdit instanceof MmsMessageRecord) { if (messageToEdit instanceof MmsMessageRecord) {
MmsMessageRecord mediaEditMessage = (MmsMessageRecord) messageToEdit; MmsMessageRecord mediaEditMessage = (MmsMessageRecord) messageToEdit;
SlideDeck slideDeck = mediaEditMessage.getSlideDeck(); SlideDeck slideDeck = mediaEditMessage.getSlideDeck();
@@ -430,7 +430,7 @@ public class InputPanel extends ConstraintLayout
if (imageVideoSlide != null && imageVideoSlide.getUri() != null) { if (imageVideoSlide != null && imageVideoSlide.getUri() != null) {
editMessageThumbnail.setVisibility(VISIBLE); editMessageThumbnail.setVisibility(VISIBLE);
glideRequests.load(new DecryptableStreamUriLoader.DecryptableUri(imageVideoSlide.getUri())) requestManager.load(new DecryptableStreamUriLoader.DecryptableUri(imageVideoSlide.getUri()))
.centerCrop() .centerCrop()
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.into(editMessageThumbnail); .into(editMessageThumbnail);
@@ -16,13 +16,14 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import com.bumptech.glide.RequestManager;
import org.signal.ringrtc.CallLinkRootKey; import org.signal.ringrtc.CallLinkRootKey;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.calls.links.CallLinks; import org.thoughtcrime.securesms.calls.links.CallLinks;
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash; import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash;
import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository; import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.SlidesClickedListener; import org.thoughtcrime.securesms.mms.SlidesClickedListener;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@@ -162,11 +163,11 @@ public class LinkPreviewView extends FrameLayout {
noPreview.setText(getLinkPreviewErrorString(customError)); noPreview.setText(getLinkPreviewErrorString(customError));
} }
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail) { public void setLinkPreview(@NonNull RequestManager requestManager, @NonNull LinkPreview linkPreview, boolean showThumbnail) {
setLinkPreview(glideRequests, linkPreview, showThumbnail, true, false); setLinkPreview(requestManager, linkPreview, showThumbnail, true, false);
} }
public void setLinkPreview(@NonNull GlideRequests glideRequests, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription, boolean scheduleMessageMode) { public void setLinkPreview(@NonNull RequestManager requestManager, @NonNull LinkPreview linkPreview, boolean showThumbnail, boolean showDescription, boolean scheduleMessageMode) {
spinner.setVisibility(GONE); spinner.setVisibility(GONE);
noPreview.setVisibility(GONE); noPreview.setVisibility(GONE);
@@ -216,13 +217,13 @@ public class LinkPreviewView extends FrameLayout {
if (showThumbnail && linkPreview.getThumbnail().isPresent()) { if (showThumbnail && linkPreview.getThumbnail().isPresent()) {
thumbnail.setVisibility(VISIBLE); thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail); thumbnailState.applyState(thumbnail);
thumbnail.get().setImageResource(glideRequests, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false); thumbnail.get().setImageResource(requestManager, new ImageSlide(linkPreview.getThumbnail().get()), type == TYPE_CONVERSATION && !scheduleMessageMode, false);
thumbnail.get().showSecondaryText(false); thumbnail.get().showSecondaryText(false);
} else if (callLinkRootKey != null) { } else if (callLinkRootKey != null) {
thumbnail.setVisibility(VISIBLE); thumbnail.setVisibility(VISIBLE);
thumbnailState.applyState(thumbnail); thumbnailState.applyState(thumbnail);
thumbnail.get().setImageDrawable( thumbnail.get().setImageDrawable(
glideRequests, requestManager,
Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER Recipient.DEFAULT_FALLBACK_PHOTO_PROVIDER
.getPhotoForCallLink() .getPhotoForCallLink()
.asDrawable(getContext(), .asDrawable(getContext(),
@@ -22,7 +22,7 @@ data class LinkPreviewViewThumbnailState(
fun applyState(thumbnail: Stub<OutlinedThumbnailView>) { fun applyState(thumbnail: Stub<OutlinedThumbnailView>) {
if (thumbnail.resolved()) { if (thumbnail.resolved()) {
thumbnail.get().setCorners(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft) thumbnail.get().setCorners(cornerTopLeft, cornerTopRight, cornerBottomRight, cornerBottomLeft)
thumbnail.get().setDownloadClickListener(downloadListener) thumbnail.get().setStartTransferClickListener(downloadListener)
} }
} }
} }
@@ -13,16 +13,19 @@ import androidx.annotation.RequiresApi
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.databinding.PromptBatterySaverBottomSheetBinding import org.thoughtcrime.securesms.databinding.PromptBatterySaverBottomSheetBinding
import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.BottomSheetUtil import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.LocalMetrics
import org.thoughtcrime.securesms.util.PowerManagerCompat import org.thoughtcrime.securesms.util.PowerManagerCompat
@RequiresApi(23) @RequiresApi(23)
class PromptBatterySaverDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() { class PromptBatterySaverDialogFragment : FixedRoundedCornerBottomSheetDialogFragment() {
companion object { companion object {
private val TAG = Log.tag(PromptBatterySaverDialogFragment::class.java)
@JvmStatic @JvmStatic
fun show(fragmentManager: FragmentManager) { fun show(fragmentManager: FragmentManager) {
@@ -51,8 +54,11 @@ class PromptBatterySaverDialogFragment : FixedRoundedCornerBottomSheetDialogFrag
binding.continueButton.setOnClickListener { binding.continueButton.setOnClickListener {
PowerManagerCompat.requestIgnoreBatteryOptimizations(requireContext()) PowerManagerCompat.requestIgnoreBatteryOptimizations(requireContext())
Log.i(TAG, "Requested to ignore battery optimizations, clearing local metrics.")
LocalMetrics.clear()
} }
binding.dismissButton.setOnClickListener { binding.dismissButton.setOnClickListener {
Log.i(TAG, "User denied request to ignore battery optimizations.")
SignalStore.uiHints().markDismissedBatterySaverPrompt() SignalStore.uiHints().markDismissedBatterySaverPrompt()
dismiss() dismiss()
} }
@@ -15,6 +15,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.android.material.imageview.ShapeableImageView; import com.google.android.material.imageview.ShapeableImageView;
import com.google.android.material.shape.CornerFamily; import com.google.android.material.shape.CornerFamily;
@@ -32,7 +33,6 @@ import org.thoughtcrime.securesms.conversation.MessageStyler;
import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.QuoteModel;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.mms.SlideDeck;
@@ -193,7 +193,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
} }
} }
public void setQuote(GlideRequests glideRequests, public void setQuote(RequestManager requestManager,
long id, long id,
@NonNull Recipient author, @NonNull Recipient author,
@Nullable CharSequence body, @Nullable CharSequence body,
@@ -213,7 +213,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
this.author.observeForever(this); this.author.observeForever(this);
setQuoteAuthor(author); setQuoteAuthor(author);
setQuoteText(resolveBody(body, quoteType), attachments, originalMissing, storyReaction); setQuoteText(resolveBody(body, quoteType), attachments, originalMissing, storyReaction);
setQuoteAttachment(glideRequests, body, attachments, originalMissing); setQuoteAttachment(requestManager, body, attachments, originalMissing);
setQuoteMissingFooter(originalMissing); setQuoteMissingFooter(originalMissing);
applyColorTheme(); applyColorTheme();
} }
@@ -347,7 +347,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
} }
} }
private void setQuoteAttachment(@NonNull GlideRequests glideRequests, @NonNull CharSequence body, @NonNull SlideDeck slideDeck, boolean originalMissing) { private void setQuoteAttachment(@NonNull RequestManager requestManager, @NonNull CharSequence body, @NonNull SlideDeck slideDeck, boolean originalMissing) {
boolean outgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING; boolean outgoing = messageType != MessageType.INCOMING && messageType != MessageType.STORY_REPLY_INCOMING;
boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW; boolean preview = messageType == MessageType.PREVIEW || messageType == MessageType.STORY_REPLY_PREVIEW;
@@ -359,7 +359,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
attachmentVideoOVerlayStub.setVisibility(GONE); attachmentVideoOVerlayStub.setVisibility(GONE);
attachmentNameViewStub.setVisibility(GONE); attachmentNameViewStub.setVisibility(GONE);
thumbnailView.setVisibility(VISIBLE); thumbnailView.setVisibility(VISIBLE);
glideRequests.load(model) requestManager.load(model)
.centerCrop() .centerCrop()
.override(thumbWidth, thumbHeight) .override(thumbWidth, thumbHeight)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
@@ -377,7 +377,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
attachmentVideoOVerlayStub.setVisibility(GONE); attachmentVideoOVerlayStub.setVisibility(GONE);
attachmentNameViewStub.setVisibility(GONE); attachmentNameViewStub.setVisibility(GONE);
thumbnailView.setVisibility(VISIBLE); thumbnailView.setVisibility(VISIBLE);
glideRequests.load(R.drawable.ic_gift_thumbnail) requestManager.load(R.drawable.ic_gift_thumbnail)
.centerCrop() .centerCrop()
.override(thumbWidth, thumbHeight) .override(thumbWidth, thumbHeight)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
@@ -404,7 +404,7 @@ public class QuoteView extends ConstraintLayout implements RecipientForeverObser
if (imageVideoSlide.hasVideo() && !imageVideoSlide.isVideoGif()) { if (imageVideoSlide.hasVideo() && !imageVideoSlide.isVideoGif()) {
attachmentVideoOVerlayStub.setVisibility(VISIBLE); attachmentVideoOVerlayStub.setVisibility(VISIBLE);
} }
glideRequests.load(new DecryptableUri(imageVideoSlide.getUri())) requestManager.load(new DecryptableUri(imageVideoSlide.getUri()))
.centerCrop() .centerCrop()
.override(thumbWidth, thumbHeight) .override(thumbWidth, thumbHeight)
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
@@ -22,6 +22,7 @@ import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.Key; import com.bumptech.glide.load.Key;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions; import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
@@ -31,7 +32,6 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter; import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader; import org.thoughtcrime.securesms.database.loaders.RecentPhotosLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks<Cursor> { public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.LoaderCallbacks<Cursor> {
@@ -119,7 +119,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
Key signature = new MediaStoreSignature(mimeType, dateModified, orientation); Key signature = new MediaStoreSignature(mimeType, dateModified, orientation);
GlideApp.with(getContext().getApplicationContext()) Glide.with(getContext().getApplicationContext())
.load(uri) .load(uri)
.signature(signature) .signature(signature)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
@@ -17,6 +17,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi; import androidx.annotation.RequiresApi;
import com.annimon.stream.Stream; import com.annimon.stream.Stream;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
@@ -24,7 +25,6 @@ import org.thoughtcrime.securesms.contactshare.Contact;
import org.thoughtcrime.securesms.contactshare.ContactUtil; import org.thoughtcrime.securesms.contactshare.ContactUtil;
import org.thoughtcrime.securesms.database.RecipientTable; import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient; import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver; import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
@@ -45,13 +45,13 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
private TextView actionButtonView; private TextView actionButtonView;
private ConversationItemFooter footer; private ConversationItemFooter footer;
private Contact contact; private Contact contact;
private Locale locale; private Locale locale;
private GlideRequests glideRequests; private RequestManager requestManager;
private EventListener eventListener; private EventListener eventListener;
private CornerMask cornerMask; private CornerMask cornerMask;
private int bigCornerRadius; private int bigCornerRadius;
private int smallCornerRadius; private int smallCornerRadius;
private final Map<RecipientId, LiveRecipient> activeRecipients = new HashMap<>(); private final Map<RecipientId, LiveRecipient> activeRecipients = new HashMap<>();
@@ -111,10 +111,10 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
cornerMask.mask(canvas); cornerMask.mask(canvas);
} }
public void setContact(@NonNull Contact contact, @NonNull GlideRequests glideRequests, @NonNull Locale locale) { public void setContact(@NonNull Contact contact, @NonNull RequestManager requestManager, @NonNull Locale locale) {
this.glideRequests = glideRequests; this.requestManager = requestManager;
this.locale = locale; this.locale = locale;
this.contact = contact; this.contact = contact;
Stream.of(activeRecipients.values()).forEach(recipient -> recipient.removeForeverObserver(this)); Stream.of(activeRecipients.values()).forEach(recipient -> recipient.removeForeverObserver(this));
this.activeRecipients.clear(); this.activeRecipients.clear();
@@ -172,17 +172,17 @@ public class SharedContactView extends LinearLayout implements RecipientForeverO
private void presentAvatar(@Nullable Uri uri) { private void presentAvatar(@Nullable Uri uri) {
if (uri != null) { if (uri != null) {
glideRequests.load(new DecryptableUri(uri)) requestManager.load(new DecryptableUri(uri))
.fallback(R.drawable.ic_contact_picture) .fallback(R.drawable.symbol_person_display_40)
.circleCrop() .circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL) .diskCacheStrategy(DiskCacheStrategy.ALL)
.dontAnimate() .dontAnimate()
.into(avatarView); .into(avatarView);
} else { } else {
glideRequests.load(R.drawable.ic_contact_picture) requestManager.load(R.drawable.symbol_person_display_40)
.circleCrop() .circleCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL) .diskCacheStrategy(DiskCacheStrategy.ALL)
.into(avatarView); .into(avatarView);
} }
} }
@@ -2,7 +2,6 @@ package org.thoughtcrime.securesms.components;
import android.content.Context; import android.content.Context;
import android.database.Cursor;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
@@ -15,11 +14,12 @@ import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.RequestManager;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.MediaTable; import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.mediapreview.MediaPreviewCache; import org.thoughtcrime.securesms.mediapreview.MediaPreviewCache;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil;
@@ -58,8 +58,8 @@ public class ThreadPhotoRailView extends FrameLayout {
} }
} }
public void setMediaRecords(@NonNull GlideRequests glideRequests, @NonNull List<MediaTable.MediaRecord> mediaRecords) { public void setMediaRecords(@NonNull RequestManager requestManager, @NonNull List<MediaTable.MediaRecord> mediaRecords) {
this.recyclerView.setAdapter(new ThreadPhotoRailAdapter(getContext(), glideRequests, mediaRecords, this.listener)); this.recyclerView.setAdapter(new ThreadPhotoRailAdapter(getContext(), requestManager, mediaRecords, this.listener));
} }
private static class ThreadPhotoRailAdapter extends RecyclerView.Adapter<ThreadPhotoRailAdapter.ThreadPhotoViewHolder> { private static class ThreadPhotoRailAdapter extends RecyclerView.Adapter<ThreadPhotoRailAdapter.ThreadPhotoViewHolder> {
@@ -67,18 +67,18 @@ public class ThreadPhotoRailView extends FrameLayout {
@SuppressWarnings("unused") @SuppressWarnings("unused")
private static final String TAG = Log.tag(ThreadPhotoRailAdapter.class); private static final String TAG = Log.tag(ThreadPhotoRailAdapter.class);
@NonNull private final GlideRequests glideRequests; @NonNull private final RequestManager requestManager;
@Nullable private OnItemClickedListener clickedListener; @Nullable private OnItemClickedListener clickedListener;
private final List<MediaTable.MediaRecord> mediaRecords = new ArrayList<>(); private final List<MediaTable.MediaRecord> mediaRecords = new ArrayList<>();
private ThreadPhotoRailAdapter(@NonNull Context context, private ThreadPhotoRailAdapter(@NonNull Context context,
@NonNull GlideRequests glideRequests, @NonNull RequestManager requestManager,
@NonNull List<MediaTable.MediaRecord> mediaRecords, @NonNull List<MediaTable.MediaRecord> mediaRecords,
@Nullable OnItemClickedListener listener) @Nullable OnItemClickedListener listener)
{ {
this.glideRequests = glideRequests; this.requestManager = requestManager;
this.clickedListener = listener; this.clickedListener = listener;
this.mediaRecords.clear(); this.mediaRecords.clear();
@@ -103,7 +103,7 @@ public class ThreadPhotoRailView extends FrameLayout {
MediaTable.MediaRecord mediaRecord = mediaRecords.get(position); MediaTable.MediaRecord mediaRecord = mediaRecords.get(position);
Slide slide = MediaUtil.getSlideForAttachment(mediaRecord.getAttachment()); Slide slide = MediaUtil.getSlideForAttachment(mediaRecord.getAttachment());
viewHolder.imageView.setImageResource(glideRequests, slide, false, false); viewHolder.imageView.setImageResource(requestManager, slide, false, false);
viewHolder.imageView.setOnClickListener(v -> { viewHolder.imageView.setOnClickListener(v -> {
MediaPreviewCache.INSTANCE.setDrawable(viewHolder.imageView.getImageDrawable()); MediaPreviewCache.INSTANCE.setDrawable(viewHolder.imageView.getImageDrawable());
if (clickedListener != null) clickedListener.onItemClicked(viewHolder.imageView, mediaRecord); if (clickedListener != null) clickedListener.onItemClicked(viewHolder.imageView, mediaRecord);
@@ -27,6 +27,7 @@ import androidx.annotation.UiThread;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import com.bumptech.glide.RequestBuilder; import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.Request; import com.bumptech.glide.request.Request;
import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.RequestListener;
@@ -39,8 +40,6 @@ import org.thoughtcrime.securesms.blurhash.BlurHash;
import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView; import org.thoughtcrime.securesms.components.transfercontrols.TransferControlView;
import org.thoughtcrime.securesms.database.AttachmentTable; import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequest;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.ImageSlide; import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener; import org.thoughtcrime.securesms.mms.SlideClickListener;
@@ -86,12 +85,12 @@ public class ThumbnailView extends FrameLayout {
private final CornerMask cornerMask; private final CornerMask cornerMask;
private final Stub<TransferControlView> transferControlViewStub; private final Stub<TransferControlView> transferControlViewStub;
private SlideClickListener thumbnailClickListener = null; private SlideClickListener thumbnailClickListener = null;
private SlidesClickedListener downloadClickListener = null; private SlidesClickedListener startTransferClickListener = null;
private SlidesClickedListener cancelDownloadClickListener = null; private SlidesClickedListener cancelTransferClickListener = null;
private SlideClickListener playVideoClickListener = null; private SlideClickListener playVideoClickListener = null;
private Slide slide = null; private Slide slide = null;
public ThumbnailView(Context context) { public ThumbnailView(Context context) {
@@ -314,23 +313,23 @@ public class ThumbnailView extends FrameLayout {
} }
} }
public void setImageDrawable(@NonNull GlideRequests glideRequests, @Nullable Drawable drawable) { public void setImageDrawable(@NonNull RequestManager requestManager, @Nullable Drawable drawable) {
glideRequests.clear(image); requestManager.clear(image);
glideRequests.clear(blurHash); requestManager.clear(blurHash);
image.setImageDrawable(drawable); image.setImageDrawable(drawable);
blurHash.setImageDrawable(null); blurHash.setImageDrawable(null);
} }
@UiThread @UiThread
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Slide slide,
boolean showControls, boolean isPreview) boolean showControls, boolean isPreview)
{ {
return setImageResource(glideRequests, slide, showControls, isPreview, 0, 0); return setImageResource(requestManager, slide, showControls, isPreview, 0, 0);
} }
@UiThread @UiThread
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Slide slide, public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Slide slide,
boolean showControls, boolean isPreview, boolean showControls, boolean isPreview,
int naturalWidth, int naturalHeight) int naturalWidth, int naturalHeight)
{ {
@@ -340,10 +339,10 @@ public class ThumbnailView extends FrameLayout {
transferControlViewStub.setVisibility(View.GONE); transferControlViewStub.setVisibility(View.GONE);
playOverlay.setVisibility(View.GONE); playOverlay.setVisibility(View.GONE);
glideRequests.clear(blurHash); requestManager.clear(blurHash);
blurHash.setImageDrawable(null); blurHash.setImageDrawable(null);
glideRequests.clear(image); requestManager.clear(image);
image.setImageDrawable(null); image.setImageDrawable(null);
int errorImageResource; int errorImageResource;
@@ -363,7 +362,7 @@ public class ThumbnailView extends FrameLayout {
} }
if (showControls) { if (showControls) {
transferControlViewStub.get().setDownloadClickListener(new DownloadClickDispatcher()); transferControlViewStub.get().setTransferClickListener(new DownloadClickDispatcher());
transferControlViewStub.get().setCancelClickListener(new CancelClickDispatcher()); transferControlViewStub.get().setCancelClickListener(new CancelClickDispatcher());
if (MediaUtil.isInstantVideoSupported(slide)) { if (MediaUtil.isInstantVideoSupported(slide)) {
transferControlViewStub.get().setInstantPlaybackClickListener(new InstantVideoClickDispatcher()); transferControlViewStub.get().setInstantPlaybackClickListener(new InstantVideoClickDispatcher());
@@ -397,7 +396,7 @@ public class ThumbnailView extends FrameLayout {
Log.i(TAG, "loading part with id " + slide.asAttachment().getUri() Log.i(TAG, "loading part with id " + slide.asAttachment().getUri()
+ ", progress " + slide.getTransferState() + ", fast preflight id: " + + ", progress " + slide.getTransferState() + ", fast preflight id: " +
slide.asAttachment().getFastPreflightId()); slide.asAttachment().fastPreflightId);
BlurHash previousBlurHash = this.slide != null ? this.slide.getPlaceholderBlur() : null; BlurHash previousBlurHash = this.slide != null ? this.slide.getPlaceholderBlur() : null;
@@ -414,10 +413,10 @@ public class ThumbnailView extends FrameLayout {
boolean resultHandled = false; boolean resultHandled = false;
if (slide.hasPlaceholder() && (previousBlurHash == null || !Objects.equals(slide.getPlaceholderBlur(), previousBlurHash))) { if (slide.hasPlaceholder() && (previousBlurHash == null || !Objects.equals(slide.getPlaceholderBlur(), previousBlurHash))) {
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(blurHash, result)); buildPlaceholderRequestBuilder(requestManager, slide).into(new GlideBitmapListeningTarget(blurHash, result));
resultHandled = true; resultHandled = true;
} else if (!slide.hasPlaceholder()) { } else if (!slide.hasPlaceholder()) {
glideRequests.clear(blurHash); requestManager.clear(blurHash);
blurHash.setImageDrawable(null); blurHash.setImageDrawable(null);
} }
@@ -425,14 +424,14 @@ public class ThumbnailView extends FrameLayout {
if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) { if (!MediaUtil.isJpegType(slide.getContentType()) && !MediaUtil.isVideoType(slide.getContentType())) {
SettableFuture<Boolean> thumbnailFuture = new SettableFuture<>(); SettableFuture<Boolean> thumbnailFuture = new SettableFuture<>();
thumbnailFuture.deferTo(result); thumbnailFuture.deferTo(result);
thumbnailFuture.addListener(new BlurHashClearListener(glideRequests, blurHash)); thumbnailFuture.addListener(new BlurHashClearListener(requestManager, blurHash));
} }
buildThumbnailGlideRequest(glideRequests, slide).into(new GlideDrawableListeningTarget(image, result)); buildThumbnailRequestBuilder(requestManager, slide).into(new GlideDrawableListeningTarget(image, result));
resultHandled = true; resultHandled = true;
} else { } else {
glideRequests.clear(image); requestManager.clear(image);
image.setImageDrawable(null); image.setImageDrawable(null);
} }
@@ -443,20 +442,20 @@ public class ThumbnailView extends FrameLayout {
return result; return result;
} }
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) { public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Uri uri) {
return setImageResource(glideRequests, uri, 0, 0); return setImageResource(requestManager, uri, 0, 0);
} }
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height) { public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Uri uri, int width, int height) {
return setImageResource(glideRequests, uri, width, height, true, null); return setImageResource(requestManager, uri, width, height, true, null);
} }
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height, boolean animate, @Nullable ThumbnailRequestListener listener) { public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull Uri uri, int width, int height, boolean animate, @Nullable ThumbnailRequestListener listener) {
SettableFuture<Boolean> future = new SettableFuture<>(); SettableFuture<Boolean> future = new SettableFuture<>();
transferControlViewStub.setVisibility(View.GONE); transferControlViewStub.setVisibility(View.GONE);
GlideRequest<Drawable> request = glideRequests.load(new DecryptableUri(uri)) RequestBuilder<Drawable> request = requestManager.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE) .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.listener(listener); .listener(listener);
@@ -483,12 +482,12 @@ public class ThumbnailView extends FrameLayout {
return future; return future;
} }
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull StoryTextPostModel model, int width, int height) { public ListenableFuture<Boolean> setImageResource(@NonNull RequestManager requestManager, @NonNull StoryTextPostModel model, int width, int height) {
SettableFuture<Boolean> future = new SettableFuture<>(); SettableFuture<Boolean> future = new SettableFuture<>();
transferControlViewStub.setVisibility(View.GONE); transferControlViewStub.setVisibility(View.GONE);
GlideRequest<Drawable> request = glideRequests.load(model) RequestBuilder<Drawable> request = requestManager.load(model)
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.placeholder(model.getPlaceholder()) .placeholder(model.getPlaceholder())
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE) .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
@@ -502,7 +501,7 @@ public class ThumbnailView extends FrameLayout {
return future; return future;
} }
private <T> GlideRequest<T> override(@NonNull GlideRequest<T> request, int width, int height) { private <T> RequestBuilder<T> override(@NonNull RequestBuilder<T> request, int width, int height) {
if (width > 0 && height > 0) { if (width > 0 && height > 0) {
Log.d(TAG, "override: apply w" + width + "xh" + height); Log.d(TAG, "override: apply w" + width + "xh" + height);
return request.override(width, height); return request.override(width, height);
@@ -516,12 +515,12 @@ public class ThumbnailView extends FrameLayout {
this.thumbnailClickListener = listener; this.thumbnailClickListener = listener;
} }
public void setDownloadClickListener(SlidesClickedListener listener) { public void setStartTransferClickListener(SlidesClickedListener listener) {
this.downloadClickListener = listener; this.startTransferClickListener = listener;
} }
public void setCancelDownloadClickListener(SlidesClickedListener listener) { public void setCancelTransferClickListener(SlidesClickedListener listener) {
this.cancelDownloadClickListener = listener; this.cancelTransferClickListener = listener;
} }
public void setPlayVideoClickListener(SlideClickListener listener) { public void setPlayVideoClickListener(SlideClickListener listener) {
@@ -532,8 +531,8 @@ public class ThumbnailView extends FrameLayout {
if (Util.equals(slide, other)) { if (Util.equals(slide, other)) {
if (slide != null && other != null) { if (slide != null && other != null) {
byte[] digestLeft = slide.asAttachment().getDigest(); byte[] digestLeft = slide.asAttachment().remoteDigest;
byte[] digestRight = other.asAttachment().getDigest(); byte[] digestRight = other.asAttachment().remoteDigest;
return Arrays.equals(digestLeft, digestRight); return Arrays.equals(digestLeft, digestRight);
} }
@@ -542,8 +541,8 @@ public class ThumbnailView extends FrameLayout {
return false; return false;
} }
private GlideRequest<Drawable> buildThumbnailGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { private RequestBuilder<Drawable> buildThumbnailRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) {
GlideRequest<Drawable> request = applySizing(glideRequests.load(new DecryptableUri(Objects.requireNonNull(slide.getUri()))) RequestBuilder<Drawable> requestBuilder = applySizing(requestManager.load(new DecryptableUri(Objects.requireNonNull(slide.getUri())))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE) .downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.transition(withCrossFade())); .transition(withCrossFade()));
@@ -551,21 +550,21 @@ public class ThumbnailView extends FrameLayout {
boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23; boolean doNotShowMissingThumbnailImage = Build.VERSION.SDK_INT < 23;
if (slide.isInProgress() || doNotShowMissingThumbnailImage) { if (slide.isInProgress() || doNotShowMissingThumbnailImage) {
return request; return requestBuilder;
} else { } else {
return request.apply(RequestOptions.errorOf(R.drawable.ic_missing_thumbnail_picture)); return requestBuilder.apply(RequestOptions.errorOf(R.drawable.missing_thumbnail));
} }
} }
public void clear(GlideRequests glideRequests) { public void clear(RequestManager requestManager) {
glideRequests.clear(image); requestManager.clear(image);
image.setImageDrawable(null); image.setImageDrawable(null);
if (transferControlViewStub.resolved()) { if (transferControlViewStub.resolved()) {
transferControlViewStub.get().clear(); transferControlViewStub.get().clear();
} }
glideRequests.clear(blurHash); requestManager.clear(blurHash);
blurHash.setImageDrawable(null); blurHash.setImageDrawable(null);
slide = null; slide = null;
@@ -594,9 +593,9 @@ public class ThumbnailView extends FrameLayout {
} }
private RequestBuilder<Bitmap> buildPlaceholderGlideRequest(@NonNull GlideRequests glideRequests, @NonNull Slide slide) { private RequestBuilder<Bitmap> buildPlaceholderRequestBuilder(@NonNull RequestManager requestManager, @NonNull Slide slide) {
GlideRequest<Bitmap> bitmap = glideRequests.asBitmap(); RequestBuilder<Bitmap> bitmap = requestManager.asBitmap();
BlurHash placeholderBlur = slide.getPlaceholderBlur(); BlurHash placeholderBlur = slide.getPlaceholderBlur();
if (placeholderBlur != null) { if (placeholderBlur != null) {
bitmap = bitmap.load(placeholderBlur); bitmap = bitmap.load(placeholderBlur);
@@ -604,7 +603,7 @@ public class ThumbnailView extends FrameLayout {
bitmap = bitmap.load(slide.getPlaceholderRes(getContext().getTheme())); bitmap = bitmap.load(slide.getPlaceholderRes(getContext().getTheme()));
} }
final GlideRequest<Bitmap> resizedRequest = applySizing(bitmap.diskCacheStrategy(DiskCacheStrategy.NONE)); final RequestBuilder<Bitmap> resizedRequest = applySizing(bitmap.diskCacheStrategy(DiskCacheStrategy.NONE));
if (placeholderBlur != null) { if (placeholderBlur != null) {
return resizedRequest.centerCrop(); return resizedRequest.centerCrop();
} else { } else {
@@ -612,7 +611,7 @@ public class ThumbnailView extends FrameLayout {
} }
} }
private <TranscodeType> GlideRequest<TranscodeType> applySizing(@NonNull GlideRequest<TranscodeType> request) { private <TranscodeType> RequestBuilder<TranscodeType> applySizing(@NonNull RequestBuilder<TranscodeType> request) {
int[] size = new int[2]; int[] size = new int[2];
fillTargetDimensions(size, dimens, bounds); fillTargetDimensions(size, dimens, bounds);
if (size[WIDTH] == 0 && size[HEIGHT] == 0) { if (size[WIDTH] == 0 && size[HEIGHT] == 0) {
@@ -667,10 +666,10 @@ public class ThumbnailView extends FrameLayout {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
Log.i(TAG, "onClick() for download button"); Log.i(TAG, "onClick() for download button");
if (downloadClickListener != null && slide != null) { if (startTransferClickListener != null && slide != null) {
downloadClickListener.onClick(view, Collections.singletonList(slide)); startTransferClickListener.onClick(view, Collections.singletonList(slide));
} else { } else {
Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + slide + " downloadClickListener: " + downloadClickListener); Log.w(TAG, "Received a download button click, but unable to execute it. slide: " + slide + " downloadClickListener: " + startTransferClickListener);
} }
} }
} }
@@ -679,10 +678,10 @@ public class ThumbnailView extends FrameLayout {
@Override @Override
public void onClick(View view) { public void onClick(View view) {
Log.i(TAG, "onClick() for cancel button"); Log.i(TAG, "onClick() for cancel button");
if (cancelDownloadClickListener != null && slide != null) { if (cancelTransferClickListener != null && slide != null) {
cancelDownloadClickListener.onClick(view, Collections.singletonList(slide)); cancelTransferClickListener.onClick(view, Collections.singletonList(slide));
} else { } else {
Log.w(TAG, "Received a cancel button click, but unable to execute it. slide: " + slide + " cancelDownloadClickListener: " + cancelDownloadClickListener); Log.w(TAG, "Received a cancel button click, but unable to execute it. slide: " + slide + " cancelDownloadClickListener: " + cancelTransferClickListener);
} }
} }
} }
@@ -701,23 +700,23 @@ public class ThumbnailView extends FrameLayout {
private static class BlurHashClearListener implements ListenableFuture.Listener<Boolean> { private static class BlurHashClearListener implements ListenableFuture.Listener<Boolean> {
private final GlideRequests glideRequests; private final RequestManager requestManager;
private final ImageView blurHash; private final ImageView blurHash;
private BlurHashClearListener(@NonNull GlideRequests glideRequests, @NonNull ImageView blurHash) { private BlurHashClearListener(@NonNull RequestManager requestManager, @NonNull ImageView blurHash) {
this.glideRequests = glideRequests; this.requestManager = requestManager;
this.blurHash = blurHash; this.blurHash = blurHash;
} }
@Override @Override
public void onSuccess(Boolean result) { public void onSuccess(Boolean result) {
glideRequests.clear(blurHash); requestManager.clear(blurHash);
blurHash.setImageDrawable(null); blurHash.setImageDrawable(null);
} }
@Override @Override
public void onFailure(ExecutionException e) { public void onFailure(ExecutionException e) {
glideRequests.clear(blurHash); requestManager.clear(blurHash);
blurHash.setImageDrawable(null); blurHash.setImageDrawable(null);
} }
} }
@@ -18,13 +18,13 @@ import androidx.annotation.Px;
import androidx.annotation.StringRes; import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.bumptech.glide.Glide;
import com.google.android.material.shape.MaterialShapeDrawable; import com.google.android.material.shape.MaterialShapeDrawable;
import com.google.android.material.shape.ShapeAppearanceModel; import com.google.android.material.shape.ShapeAppearanceModel;
import org.signal.core.util.DimensionUnit; import org.signal.core.util.DimensionUnit;
import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.GlideApp;
/** /**
* Class for creating simple tooltips to show throughout the app. Utilizes a popup window so you * Class for creating simple tooltips to show throughout the app. Utilizes a popup window so you
@@ -101,7 +101,7 @@ public class TooltipPopup extends PopupWindow {
if (iconGlideModel != null) { if (iconGlideModel != null) {
ImageView iconView = getContentView().findViewById(R.id.tooltip_icon); ImageView iconView = getContentView().findViewById(R.id.tooltip_icon);
iconView.setVisibility(View.VISIBLE); iconView.setVisibility(View.VISIBLE);
GlideApp.with(anchor.getContext()).load(iconGlideModel).into(iconView); Glide.with(anchor.getContext()).load(iconGlideModel).into(iconView);
} }
setElevation(10); setElevation(10);
@@ -11,6 +11,7 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.exifinterface.media.ExifInterface; import androidx.exifinterface.media.ExifInterface;
import com.bumptech.glide.RequestManager;
import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.Target; import com.bumptech.glide.request.target.Target;
import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.ImageSource;
@@ -23,7 +24,6 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder; import org.thoughtcrime.securesms.components.subsampling.AttachmentBitmapDecoder;
import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder; import org.thoughtcrime.securesms.components.subsampling.AttachmentRegionDecoder;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.PartAuthority; import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.util.ActionRequestListener; import org.thoughtcrime.securesms.util.ActionRequestListener;
import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapDecodingException;
@@ -81,7 +81,7 @@ public class ZoomingImageView extends FrameLayout {
} }
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
public void setImageUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri, @NonNull String contentType, @NonNull Runnable onMediaReady) public void setImageUri(@NonNull RequestManager requestManager, @NonNull Uri uri, @NonNull String contentType, @NonNull Runnable onMediaReady)
{ {
final Context context = getContext(); final Context context = getContext();
final int maxTextureSize = BitmapUtil.getMaxTextureSize(); final int maxTextureSize = BitmapUtil.getMaxTextureSize();
@@ -103,7 +103,7 @@ public class ZoomingImageView extends FrameLayout {
if (dimensions == null || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) { if (dimensions == null || (dimensions.first <= maxTextureSize && dimensions.second <= maxTextureSize)) {
Log.i(TAG, "Loading in standard image view..."); Log.i(TAG, "Loading in standard image view...");
setImageViewUri(glideRequests, uri, onMediaReady); setImageViewUri(requestManager, uri, onMediaReady);
} else { } else {
Log.i(TAG, "Loading in subsampling image view..."); Log.i(TAG, "Loading in subsampling image view...");
setSubsamplingImageViewUri(uri); setSubsamplingImageViewUri(uri);
@@ -112,11 +112,11 @@ public class ZoomingImageView extends FrameLayout {
}); });
} }
private void setImageViewUri(@NonNull GlideRequests glideRequests, @NonNull Uri uri, @NonNull Runnable onMediaReady) { private void setImageViewUri(@NonNull RequestManager requestManager, @NonNull Uri uri, @NonNull Runnable onMediaReady) {
photoView.setVisibility(View.VISIBLE); photoView.setVisibility(View.VISIBLE);
subsamplingImageView.setVisibility(View.GONE); subsamplingImageView.setVisibility(View.GONE);
glideRequests.load(new DecryptableUri(uri)) requestManager.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE) .diskCacheStrategy(DiskCacheStrategy.NONE)
.dontTransform() .dontTransform()
.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
@@ -34,6 +34,7 @@ public class EmojiImageView extends AppCompatImageView {
setImageResource(R.drawable.ic_emoji); setImageResource(R.drawable.ic_emoji);
} else { } else {
setImageDrawable(EmojiProvider.getEmojiDrawable(getContext(), emoji, forceJumboEmoji)); setImageDrawable(EmojiProvider.getEmojiDrawable(getContext(), emoji, forceJumboEmoji));
setContentDescription(emoji);
} }
} }
} }
@@ -32,6 +32,7 @@ import androidx.core.view.GestureDetectorCompat;
import androidx.core.view.ViewKt; import androidx.core.view.ViewKt;
import androidx.core.widget.TextViewCompat; import androidx.core.widget.TextViewCompat;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser; import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation; import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
@@ -73,6 +74,7 @@ public class EmojiTextView extends AppCompatTextView {
private boolean isJumbomoji; private boolean isJumbomoji;
private boolean forceJumboEmoji; private boolean forceJumboEmoji;
private boolean renderSpoilers; private boolean renderSpoilers;
private boolean shrinkWrap;
private MentionRendererDelegate mentionRendererDelegate; private MentionRendererDelegate mentionRendererDelegate;
private SpoilerRendererDelegate spoilerRendererDelegate; private SpoilerRendererDelegate spoilerRendererDelegate;
@@ -96,6 +98,7 @@ public class EmojiTextView extends AppCompatTextView {
measureLastLine = a.getBoolean(R.styleable.EmojiTextView_measureLastLine, false); measureLastLine = a.getBoolean(R.styleable.EmojiTextView_measureLastLine, false);
forceJumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false); forceJumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
renderSpoilers = a.getBoolean(R.styleable.EmojiTextView_emoji_renderSpoilers, false); renderSpoilers = a.getBoolean(R.styleable.EmojiTextView_emoji_renderSpoilers, false);
shrinkWrap = a.getBoolean(R.styleable.EmojiTextView_emoji_shrinkWrap, false);
a.recycle(); a.recycle();
a = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.textSize }); a = context.obtainStyledAttributes(attrs, new int[] { android.R.attr.textSize });
@@ -224,6 +227,25 @@ public class EmojiTextView extends AppCompatTextView {
widthMeasureSpec = applyWidthMeasureRoundingFix(widthMeasureSpec); widthMeasureSpec = applyWidthMeasureRoundingFix(widthMeasureSpec);
super.onMeasure(widthMeasureSpec, heightMeasureSpec); super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
if (shrinkWrap && getLayout() != null && mode == MeasureSpec.AT_MOST) {
Layout layout = getLayout();
float maxLineWidth = 0f;
for (int i = 0; i < layout.getLineCount(); i++) {
if (layout.getLineWidth(i) > maxLineWidth) {
maxLineWidth = layout.getLineWidth(i);
}
}
int desiredWidth = (int) maxLineWidth + getPaddingLeft() + getPaddingRight();
if (getMeasuredWidth() > desiredWidth) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(desiredWidth, mode);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
CharSequence text = getText(); CharSequence text = getText();
if (getLayout() == null || !measureLastLine || text == null || text.length() == 0) { if (getLayout() == null || !measureLastLine || text == null || text.length() == 0) {
lastLineWidth = -1; lastLineWidth = -1;
@@ -32,7 +32,7 @@ public class UntrustedSendDialog extends AlertDialog.Builder implements DialogIn
this.resendListener = resendListener; this.resendListener = resendListener;
setTitle(R.string.UntrustedSendDialog_send_message); setTitle(R.string.UntrustedSendDialog_send_message);
setIcon(R.drawable.ic_warning); setIcon(R.drawable.symbol_error_triangle_fill_24);
setMessage(message); setMessage(message);
setPositiveButton(R.string.UntrustedSendDialog_send, this); setPositiveButton(R.string.UntrustedSendDialog_send, this);
setNegativeButton(android.R.string.cancel, null); setNegativeButton(android.R.string.cancel, null);
@@ -31,7 +31,7 @@ public class UnverifiedSendDialog extends AlertDialog.Builder implements DialogI
this.resendListener = resendListener; this.resendListener = resendListener;
setTitle(R.string.UnverifiedSendDialog_send_message); setTitle(R.string.UnverifiedSendDialog_send_message);
setIcon(R.drawable.ic_warning); setIcon(R.drawable.symbol_error_triangle_fill_24);
setMessage(message); setMessage(message);
setPositiveButton(R.string.UnverifiedSendDialog_send, this); setPositiveButton(R.string.UnverifiedSendDialog_send, this);
setNegativeButton(android.R.string.cancel, null); setNegativeButton(android.R.string.cancel, null);

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