Compare commits

..

302 Commits

Author SHA1 Message Date
Cody Henthorne
3644466263 Bump version to 7.42.2 2025-05-19 14:44:07 -04:00
Cody Henthorne
e9b43e7c25 Update baseline profile. 2025-05-19 14:43:05 -04:00
Cody Henthorne
e9e53d6d84 Update translations and other static files. 2025-05-19 14:38:15 -04:00
Cody Henthorne
03f2feb335 Fix megaphone missing asset crash. 2025-05-19 14:31:48 -04:00
Cody Henthorne
3b657ea7bd Bump version to 7.42.1 2025-05-16 13:32:56 -04:00
Cody Henthorne
2635ffcbc9 Update baseline profile. 2025-05-16 13:29:24 -04:00
Cody Henthorne
927c079cc4 Update translations and other static files. 2025-05-16 13:23:44 -04:00
Cody Henthorne
a2f7afcb68 Detect and recycle WebSockets stuck in connecting state. 2025-05-16 13:13:32 -04:00
Alex Hart
b6c033b075 Fix timer updating on chats list. 2025-05-15 13:52:18 -03:00
Jeffrey Starke
31d0b81624 Sticker Management v2 - Exit multi select mode after all items are deselected.
To match the behavior of the conversations and messages screens.
2025-05-15 11:08:08 -04:00
Cody Henthorne
96ece3f424 Allow REST fallback via remote config. 2025-05-15 10:52:36 -04:00
Alex Hart
2bc9926d97 Fix crash when backing out of archive. 2025-05-15 11:40:00 -03:00
Alex Hart
f1537cb8a9 Fix rationale dialog height on small devices. 2025-05-15 11:30:23 -03:00
andrew-signal
af8dee9c38 Bump to libsignal v0.71.1 2025-05-15 10:28:45 -04:00
Cody Henthorne
39f78273c0 Async start network and message retrieval. 2025-05-15 09:22:46 -04:00
Michelle Tang
d66a0f618d Bump version to 7.42.0 2025-05-14 17:35:09 -04:00
Michelle Tang
497a8188fd Update translations and other static files. 2025-05-14 17:20:57 -04:00
Jeffrey Starke
f3a475d0c8 Sticker management v2 – Improve list animations and state transitions.
- Uninstall selected packs in a single database transaction to avoid UI flickering.
- Add section header keys to prevent them from animating wildly while scrolling.
2025-05-14 17:10:41 -04:00
Michelle Tang
8b828677de Rotate libsignal net config flag. 2025-05-14 16:43:17 -04:00
Alex Hart
a050b37f3a Remove bank selection from iDEAL. 2025-05-14 16:43:17 -04:00
Doug Melton
c865ed0cdc Improve handling of 12/24 hour timestamps on configuration change.
This fixes an edge case seen on ConversationFragment, where if the
device time format is switched between 12/24 hour format while the app
is running, the old time format will still be displayed when the app
is resumed.

This is due to a design flaw in `DateTimeFormatter.ofLocalizedTime`,
where the time format is statically cached and not updated upon
configuration change. The `LocalTime.formatHours()` extension method
was updated to no longer rely on the misbehaving `ofLocalTime` method.

In addition, `ConversationMessaageComputeWorkers.recomputeFormattedDate`
was designed to skip recomputing non-relative timestamps. This works
in most cases but not this specific edge case. A `force: Boolean` flag
was added to force all items to be updated. And the `force = true` flag
was passed upon `onResume` of the fragment.

Closes #14121
2025-05-14 16:43:17 -04:00
Alex Hart
918b792d83 Fix filter display when returning to conversation list from another tab. 2025-05-14 16:43:17 -04:00
Alex Hart
28ecb37103 Add additional logging for stopped processing. 2025-05-14 16:43:17 -04:00
Alex Hart
7c43462771 Disconnect groupCall object when network is unavailable. 2025-05-14 16:43:17 -04:00
Cody Henthorne
7e00739240 Remove MMS related encryption error messaging. 2025-05-14 16:43:17 -04:00
Cody Henthorne
feae417af5 Flag username for restore on re-register. 2025-05-14 16:43:17 -04:00
Michelle Tang
e5d55418ac Restore chat colors after backup. 2025-05-14 16:43:17 -04:00
Cody Henthorne
5d8e0e370d Fix pnp settings not properly syncing or dynamically updating. 2025-05-14 16:43:17 -04:00
Michelle Tang
e2bffd0fd3 Fix mentions being displayed as obj. 2025-05-14 16:43:17 -04:00
Alex Hart
8d2979d8ce Fix camera rotation / phone orientation syncing. 2025-05-14 16:43:17 -04:00
Jeffrey Starke
288eda5bb1 Add support for animated images to GlideImage.
Our GlideImage implementation doesn't support animated images, because it loads them as bitmaps and therefore only displays the first image frame as a static image. This change works around that issue by having GlideImage wrap an ImageView to handle cases where we need to display animated images.
2025-05-14 16:43:17 -04:00
Cody Henthorne
fb111619d7 Downgrade notify of key warning to info. 2025-05-14 16:43:17 -04:00
Alex Hart
fb68f3fed1 Work around off-by-one error causing thin line to display when it shouldn't. 2025-05-14 16:43:17 -04:00
Alex Hart
791f1677fa Update RecyclerView to 1.4.0 2025-05-14 16:43:17 -04:00
Alex Hart
632b76081a Fix tab scroll-to-top on click. 2025-05-14 16:43:16 -04:00
Alex Hart
a474666ea7 Hide create call link button while in multiselect mode. 2025-05-14 16:43:16 -04:00
Greyson Parrelli
b3c9ec9691 Migrate to new SVR2 enclave.
Co-authored-by: Cody Henthorne <cody@signal.org>
2025-05-14 16:43:16 -04:00
Cody Henthorne
778db277c8 Update retrieve profile job. 2025-05-14 16:43:16 -04:00
Michelle Tang
1800507604 Add beta label to backups. 2025-05-14 16:43:16 -04:00
Alex Hart
b0aee1db05 Add proper title to expiration sheet. 2025-05-14 16:43:16 -04:00
Alex Hart
919cbbd7ca Use dispatch queue and join to ensure single-threaded requests. 2025-05-14 16:43:16 -04:00
Cody Henthorne
93403a0d2c Implement stop/resume media restore and update restore over cellular. 2025-05-14 16:43:16 -04:00
Jeffrey Starke
9867fa3f50 Add round checkbox composable.
Adds `RoundCheckbox` composable, which is styled to match the appearance of the other view checkboxes used in the app.
2025-05-14 16:43:16 -04:00
Michelle Tang
b79ec79644 Fix backups not being scheduled. 2025-05-14 16:43:16 -04:00
Cody Henthorne
961e9fd4b9 Fix shortcut update job crash for old installs without an aci. 2025-05-14 16:43:16 -04:00
Alex Hart
6d04c8ba42 Remove MainListHostFragment and rescope list vms to the activity. 2025-05-14 16:43:16 -04:00
andrew-signal
bc94a92f68 Remove pendingResponses; libsignal-net now completes futures with disconnectReason. 2025-05-14 16:43:16 -04:00
andrew-signal
9b9888565b Bump to libsignal v0.71.0. 2025-05-14 16:43:16 -04:00
Alex Hart
a2a3dd28ee Remove activity override in favour of alias down the road. 2025-05-14 16:43:16 -04:00
Jeffrey Starke
844dec06b1 Delete old/unused sticker management v1 code.
Deletes the old code related to sticker management v1 and removes the v2 prefix from the new classes.
2025-05-14 16:43:16 -04:00
Sagar
5306a9dd7a Fix system emoji not showing in video call reactions. 2025-05-14 16:43:16 -04:00
Sagar
cdd595432b Update header on recipient change. 2025-05-14 16:43:00 -04:00
Alex Hart
fabec719ab Prevent multiple activity instances and fix strange launch behavior. 2025-05-06 17:58:48 -04:00
Jeffrey Starke
04c14a82be Sticker management v2 - Implement remaining functionality.
- Fix bottom action bar shadow clipping during visibility animations.
- Show snackbar after installing/uninstalling sticker packs.
- Navigate to sticker preview on row click.
- Add top app bar menu to enable multi-select mode.
- Start StickerManagementActivityV2 instead of the old StickerManagementActivity
2025-05-06 17:58:48 -04:00
Sagar
51851fa5fe Fix crash for leave gv1. 2025-05-06 17:58:48 -04:00
Jeffrey Starke
3c77a3d7aa Sticker management v2 - Implement multi-delete. 2025-05-06 17:58:48 -04:00
andrew-signal
7c9bab421a Pass down RemoteConfig for TLS minimum version enforcement to libsignal. 2025-05-06 17:58:48 -04:00
Michelle Tang
9d1960f065 Clear aep from clipboard after 60 seconds. 2025-05-06 17:58:48 -04:00
Sagar
ae4c0d1242 Add paging for getArchivedRecipients. 2025-05-06 17:58:48 -04:00
Alex Hart
df3396633b Add nav spacing to action bar in compact mode. 2025-05-06 17:58:48 -04:00
Michelle Tang
9aea264305 Fix backup dialog color. 2025-05-06 17:58:48 -04:00
Michelle Tang
866c232045 Convert InviteActivity to a fragment. 2025-05-06 17:58:48 -04:00
Alex Hart
524ffd9d79 Save search query to savedinstancestate. 2025-05-06 17:58:48 -04:00
Alex Hart
46ca979e59 Fix navigation bar offset. 2025-05-06 17:58:48 -04:00
Jeffrey Starke
c8bfc88bed Sticker management v2 - Implement multi-select. 2025-05-06 17:58:48 -04:00
Sagar
030678b029 Fix UI update on non-UI thread exception. 2025-05-06 17:58:48 -04:00
Sagar
e4b99e5cef Reapply query after contact refresh. 2025-05-06 17:58:48 -04:00
andrew-signal
367c0d0a8d Rotate libsignal-net trial RemoteConfig. 2025-05-06 17:58:48 -04:00
Sagar
6dfe3b9c33 Fix color resource linking in SignalSymbols. 2025-05-06 17:58:48 -04:00
Michelle Tang
3aa4e75ef3 Remove wrapped fragments from settings. 2025-05-06 17:58:48 -04:00
Jim Gustafson
570a475229 Add new remote config support for calling audio configuration. 2025-05-06 17:58:48 -04:00
Cody Henthorne
2421bbdabb Fix invalid constraint handling sql when calling update. 2025-05-06 17:58:48 -04:00
Sagar
39756fd0d4 Avoid recording empty voice messages during an ongoing call. 2025-05-06 17:58:48 -04:00
Sagar
7a69c96746 Add accessibility label on buttons. 2025-05-06 17:58:48 -04:00
Sagar
f0acc39829 Hide camera toggle button in PIP mode. 2025-05-06 17:58:48 -04:00
Sagar
a27daddb70 Fix media player incorrect state when switching videos in album. 2025-05-06 17:58:48 -04:00
Jeffrey Starke
fd47d28026 Sticker management v2 - Implement context menus.
Adds the context menus that appear when long pressing available or installed sticker pack list items.
2025-05-06 17:58:48 -04:00
Jeffrey Starke
fe853f7b65 Add missing long press haptic feedback to composables.
As recommended by https://developer.android.com/develop/ui/compose/touch-input/pointer-input/tap-and-press

> As a best practice, you should include haptic feedback when the user long-presses elements.
2025-05-06 17:58:48 -04:00
Cody Henthorne
c89fbbe49f Fix unread count asserts in read sync tests. 2025-05-06 17:58:48 -04:00
Cody Henthorne
5453f101ff Fix BackupRestoreMediaJob not correctly paging through attachments. 2025-05-06 17:58:48 -04:00
Cody Henthorne
87cbe305f0 Support accounts without pins in AEP restore flows. 2025-05-06 17:58:48 -04:00
Jeffrey Starke
b298cb6f89 Prevent sending sticker attachments with a blank contentType. 2025-05-06 17:58:48 -04:00
Sagar
65e1ffaed4 Do not play a media item if it was deleted. 2025-05-06 17:58:47 -04:00
Sagar
43b5cb0641 Fix crash when leaving group. 2025-05-06 17:58:47 -04:00
Greyson Parrelli
f73d929feb Add additional CDN reconciliations to BackupMediaSnapshotSyncJob.
Co-authored-by: Cody Henthorne <cody@signal.org>
2025-05-06 17:58:47 -04:00
andrew-signal
85647f1258 Bump to libsignal v0.70.1 2025-05-06 17:58:47 -04:00
Sagar
9164668b8b Duck and recover external audio on video play. 2025-05-06 17:58:47 -04:00
Sagar
76aaf22429 Duck and recover external audio on voice note play. 2025-05-06 17:58:47 -04:00
Miriam Zimmerman
3d7162cdd3 Implement remote mute receive; Update to RingRTC v2.52.0
Co-authored-by: Alex Hart <alex@signal.org>
Co-authored-by: Cody Henthorne <cody@signal.org>
2025-05-06 17:58:47 -04:00
Alex Hart
ed9a945f05 Fix issue where a test user could have a tier but no subscriber. 2025-05-06 17:58:47 -04:00
Cody Henthorne
f8d7c27583 Bump version to 7.41.3 2025-05-06 17:58:08 -04:00
Cody Henthorne
4e1072b8da Update baseline profile. 2025-05-06 16:53:06 -04:00
Cody Henthorne
057715226f Update translations and other static files. 2025-05-06 16:48:07 -04:00
Cody Henthorne
0f8fdda884 Revert "Remove message send REST fallback."
This reverts commit 7bdfec77ca.
2025-05-06 16:39:43 -04:00
Michelle Tang
393b88fb1f Bump version to 7.41.2 2025-04-30 15:55:00 -04:00
Michelle Tang
639c3ef883 Update translations and other static files. 2025-04-30 15:44:25 -04:00
Sagar
ad4142db1a Fix class cast crash for banners. 2025-04-30 15:38:41 -04:00
Cody Henthorne
5182987735 Fix cds crash by translating libsignal-net CDS protocol exception to IOException. 2025-04-30 15:37:47 -04:00
Michelle Tang
7f5bfc210b Fix story text previews. 2025-04-30 15:25:14 -04:00
Michelle Tang
daf87915d6 Bump version to 7.41.1 2025-04-28 16:49:36 -04:00
Michelle Tang
06996540cd Update translations and other static files. 2025-04-28 16:43:54 -04:00
andrew-signal
58ad3c746a Don't call single.onError with IOException in LibSignalChatConnection::sendRequest. 2025-04-28 12:46:52 -06:00
Sagar
a7ebe41570 Fix MediaSelectionViewModel crash. 2025-04-28 19:30:58 +05:30
Michelle Tang
b6cc702107 Add more logging for chat folders during storage sync. 2025-04-25 16:11:21 -04:00
Greyson Parrelli
9163c0ca4d Improve envelope timestamp validation. 2025-04-24 16:45:26 -04:00
Cody Henthorne
18290c1301 Bump version to 7.41.0 2025-04-24 16:15:11 -04:00
Cody Henthorne
347abe14ae Update baseline profile. 2025-04-24 16:09:51 -04:00
Cody Henthorne
eba55755ff Update translations and other static files. 2025-04-24 16:04:38 -04:00
Michelle Tang
7043558657 Add fixes for streamable videos. 2025-04-24 15:55:40 -04:00
Alex Hart
3aefd3bdc6 Prevent search state from clearing if user did not send a message. 2025-04-24 15:55:40 -04:00
Sagar
d6eb675fd0 Trim text before performing username search. 2025-04-24 15:55:40 -04:00
Alex Hart
ae90b2ecd9 Add support for conversation intent routing to MainActivity. 2025-04-24 15:55:39 -04:00
Jeffrey Starke
9d593bcaff Fix chat folders flickering during drag and drop.
Fixes the UI flickering that occurs when reordering chat folders. The issue was caused by the ViewModel updating the database each time a list item position changes when we were already updating list order in the UI state manually at the same time.
2025-04-24 15:55:39 -04:00
Jeffrey Starke
62ed823e42 Sticker management v2 - Implement drag and drop.
Adds the ability to use drag and drop to rearrange installed sticker packs.
2025-04-24 15:55:39 -04:00
Cody Henthorne
a53479e50d Do not process messages while pending restore decision. 2025-04-24 15:55:39 -04:00
Cody Henthorne
91140c41fd Revert "Depend on libsignal-net's connection backoff instead of duplicating at app-level."
This reverts commit 1aed82d5b7.
2025-04-24 15:55:39 -04:00
Cody Henthorne
68f567b0b7 Fix a few random crashes when using libsignal-net. 2025-04-24 15:55:39 -04:00
Cody Henthorne
501e169210 Make e164 formatter more leinent with + prefix. 2025-04-24 15:55:39 -04:00
Greyson Parrelli
09b818b048 Limit work that happens in LiveRecipientCache lock. 2025-04-24 15:55:39 -04:00
Sagar
7b3897cac6 Fix incorrect span indices for normalised search text. 2025-04-24 15:55:39 -04:00
Alex Hart
64239962fc Implement activated state for conversation list items. 2025-04-24 15:55:39 -04:00
Alex Hart
dac3a332d7 Remove main-thread usage of Recipient.self. 2025-04-24 15:55:39 -04:00
Sagar
83bbcd0618 Avoid message click listeners in Scheduled messages sheet. 2025-04-24 15:55:39 -04:00
andrew-signal
c7c0374c11 Add remote config for libsignal-net rollout. 2025-04-24 15:55:39 -04:00
Sagar
847f3bf08c Pause and play video correctly on TimeBar scrub drag. 2025-04-24 15:55:39 -04:00
Cody Henthorne
d02c610237 Fix unreads for new unread count scheme. 2025-04-24 15:55:39 -04:00
Cody Henthorne
8007045ca8 Convert change number back to WebSocket. 2025-04-24 15:55:39 -04:00
Sagar
901b4b469d Show correct time for Story view item. 2025-04-24 15:55:39 -04:00
Sagar
fa50696815 Ensure story viewed list in proper alphabetical order. 2025-04-24 15:55:39 -04:00
Alex Hart
be035456f7 Ensures chat folder is remembered when we leave page. 2025-04-24 15:55:39 -04:00
Sagar
252a4afa79 Update banner message for debug log. 2025-04-24 15:55:39 -04:00
Sagar
f5f56536bc Fix unread count for edited messages.
Co-authored-by: Cody Henthorne <cody@signal.org>
2025-04-24 15:55:39 -04:00
Michelle Tang
9e89d688f1 Send error message after cancelling a link+sync. 2025-04-24 15:55:39 -04:00
Sagar
2bb94089f7 Move to quoted message on quote preview click. 2025-04-24 15:55:39 -04:00
Jeffrey Starke
3fc386d4a3 Add StickerPackId and StickerPackKey value classes. 2025-04-24 15:55:39 -04:00
Sagar
3779dfd290 Open keyboard for a Draft message. 2025-04-24 15:55:39 -04:00
Jeffrey Starke
a5f766a333 Sticker management v2 - Implement sticker pack installation.
Adds the ability to install sticker packs using `StickerManagementActivityV2`.

When the install button is clicked, it will morph into an indeterminate progress bar, which will then animate into a checkmark once the installation completes successfully. Then a couple seconds later, the sticker pack row will be removed from the available sticker packs list.
2025-04-24 15:55:39 -04:00
Sagar
9f40bfc645 Replace glyphs in group update messages. 2025-04-24 15:55:39 -04:00
Greyson Parrelli
919f03522a Upgrade to mobilecoin to 6.1.2 for 16kb alignment. 2025-04-24 15:55:39 -04:00
Cody Henthorne
8aa6d0bbca Include AEP in link device provisioning message. 2025-04-24 15:55:39 -04:00
Cody Henthorne
4304ae2a96 Add notification profile id for backupsv2. 2025-04-24 15:55:39 -04:00
Sagar
b4a9189068 Add close icon in search toolbar. 2025-04-24 15:55:39 -04:00
Greyson Parrelli
ec6448bd1b Address possible invalid e164's in storage service splits. 2025-04-24 15:55:39 -04:00
Greyson Parrelli
8c5811581e Add additional logging around storage batch sizes. 2025-04-24 15:55:39 -04:00
Greyson Parrelli
4b4d3d33b1 Add additional safeguards around storage sync types. 2025-04-24 15:55:39 -04:00
Jeffrey Starke
dd6c39f7eb Update TransferProgressIndicator to support indeterminate progress.
Showing exact progress for sticker pack downloads is more complicated than necessary. This PR updates `TransferProgressIndicator` to support displaying indeterminate progress.

#### Changeset
- Display indeterminate progress when installing a sticker pack.
- Remove cancel button from `AvailableStickerPackRow`.
- Decrease progress indicator size to match updated design.
2025-04-24 15:55:39 -04:00
Sagar
b246e62504 Avoid setting blank folder name. 2025-04-24 15:55:39 -04:00
Sagar
ba08399d35 Add accessibility labels for MainToolbar. 2025-04-24 15:55:39 -04:00
Sagar
3f1bb7eac7 Improve choose chats save button enabled state. 2025-04-24 15:55:39 -04:00
Greyson Parrelli
a2a10fb0c1 Filter out bad E164s from GV1 groups. 2025-04-24 15:55:39 -04:00
Greyson Parrelli
e45eabc714 Convert avatar migration to just be a force push. 2025-04-24 15:55:39 -04:00
Alex Hart
138dae0484 Align pin reminder skip behavior with iOS. 2025-04-24 15:55:39 -04:00
Alex Hart
893725e304 Dynamic split pane support via internal setting. 2025-04-24 15:55:39 -04:00
Jeffrey Starke
2cfe321274 Convert StickerManagementRepository to kotlin.
Converts `StickerManagementRepository` to kotlin, so `getStickerPacks()` can return a `Flow` that emits updates after the database is changed.

This change simplifies the implementation of `StickerManagmentViewModelV2`, since `StickerManagementRepository.getStickerPacks()` will now automatically register and unregister the database observer.
2025-04-24 15:55:39 -04:00
Sagar
050dcb3eb1 Show correct message for empty archived screen. 2025-04-24 15:55:39 -04:00
Alex Hart
6ce01c6b0e Return an empty list instead of crashing when calling participantAcis. 2025-04-24 15:55:39 -04:00
Sagar
d2f44fee87 Avoid opening Media preview for not sent media. 2025-04-24 15:55:39 -04:00
Sagar
1228da8665 Fix transfer controls logic for checking isUpload. 2025-04-24 15:55:39 -04:00
Sagar
479632d6a8 Fix message info screen updates. 2025-04-24 15:55:39 -04:00
Greyson Parrelli
619d2997f6 Add additional local metrics around storage service writes/reads. 2025-04-24 15:55:39 -04:00
Alex Hart
c5e795b176 Wire up nav rail fabs and fix animation playing on leaving a tab. 2025-04-24 15:55:39 -04:00
andrew-signal
8b7b184224 Tweak Network.transformAndSetRemoteConfig to match changes to libsignal's RemoteConfig spec. 2025-04-24 15:55:39 -04:00
Jeffrey Starke
48d26beb77 Add TransferProgressIndicator composable.
Adds a composable version of `TransferProgressView`.
2025-04-24 15:55:39 -04:00
Jeffrey Starke
3d1895500c Sticker management v2 - Display available and installed stickers. 2025-04-24 15:55:39 -04:00
Alex Hart
e442c27555 Separate sheet that requires payment flow. 2025-04-24 15:55:39 -04:00
Alex Hart
c3d61bece1 Add MainContentLayoutData object and proper scaffolding directive. 2025-04-24 15:55:39 -04:00
Alex Hart
49853b2cca Move background color selection into theme. 2025-04-24 15:55:39 -04:00
Sagar
cd838c4bee Fix Video call screen bottom sheet weird animation and height. 2025-04-24 15:55:39 -04:00
Greyson Parrelli
2e50699a2d Make system keyboard sticker detection more reliable. 2025-04-24 15:55:39 -04:00
Michelle Tang
fe97c969ae Ensure keystore operations happen on the same thread. 2025-04-24 15:55:39 -04:00
Alex Hart
c70a8d48a8 Hide keyboard when navigating back to chat list via toolbar. 2025-04-24 15:55:39 -04:00
Alex Hart
322ea97377 Add logging to BackupsSettingsViewModel to help track down data loading race. 2025-04-24 15:55:39 -04:00
Sagar
e3a402394f Avoid message failed notification when bubble thread is visible. 2025-04-24 15:55:39 -04:00
Sagar
16b4b3b6b7 Fix SearchView loosing focus. 2025-04-24 15:55:39 -04:00
Jim Gustafson
cd98ccbf00 Update to RingRTC v2.50.6 2025-04-24 15:55:39 -04:00
Sagar
eecb18b436 Add correct dialog message while blocking group. 2025-04-24 15:55:39 -04:00
Sagar
d13a803dcd Fix resend button visibility logic. 2025-04-24 15:55:39 -04:00
Alex Hart
bd03f21cdf Allow specification of whether we utilize windowTypes to lay out inset guidelines. 2025-04-24 15:55:39 -04:00
Alex Hart
b46d891183 Dialog color fixes. 2025-04-24 15:55:38 -04:00
Alex Hart
54191433e0 Remove ConversationTabs* and migrate to MainActivity. 2025-04-24 15:55:38 -04:00
Sagar
462fcdce16 Add glyph icons and SignalSymbol methods. 2025-04-24 15:55:38 -04:00
Greyson Parrelli
f68bb2dc88 Add storage service optimization to avoid manifest reads. 2025-04-24 15:55:38 -04:00
andrew-signal
fe70637140 Bump to libsignal v0.70.0 2025-04-24 15:55:38 -04:00
Greyson Parrelli
1028d293a0 Temporarily remove bad assertion in MessageBackupsFlowViewModel. 2025-04-24 15:55:38 -04:00
andrew-signal
74c6e76808 Add system HTTP proxy support to libsignal-net.
Co-authored-by: Cody Henthorne <cody@signal.org>
2025-04-24 15:55:38 -04:00
Greyson Parrelli
8e880fe117 Fix another syncing crash when no archived media exists. 2025-04-24 15:55:38 -04:00
Greyson Parrelli
6525662071 Fix syncing crash when no archived media exists. 2025-04-24 15:55:38 -04:00
Alex Hart
94d07f7012 Decouple InlineQueryViewModelV2 instance from activity, parent to fragment instead. 2025-04-24 15:55:38 -04:00
Sagar
e3297ab593 Add accessibility labels for GIF categories and correct emoji labels. 2025-04-24 15:55:38 -04:00
Sagar
3ff7f89ef6 Support hiding image caption with press and hold. 2025-04-24 15:55:38 -04:00
Sagar
ac1165c8fd Avoid blocking yourself. 2025-04-24 15:55:38 -04:00
Sagar
69153cf339 Support drag multi-selection for media gallery. 2025-04-16 11:22:23 -03:00
Sagar
852541c361 Avoid setting blank custom story name. 2025-04-16 11:22:23 -03:00
Sagar
399a613c25 Avoid sending blank story. 2025-04-16 11:22:23 -03:00
Sagar
003c1082a9 Avoid setting blank group names. 2025-04-16 11:22:23 -03:00
Jeffrey Starke
885588db86 Create new sticker management screen with tabbed interface.
Adds a skeleton implementation of `StickerManagementActivityV2`. This new activity is not currently connected to anything, but once complete it will replace `StickerManagementActivity`.
2025-04-16 11:22:23 -03:00
Milan Stevanovic
90a356b29d Fix incorrect embedded druation in certain MP4 files.
The root cause:
- some MP4 files come with H.264/H.265 streams which explicitly
  state their timescale. In such cases, it is wise that MP4 muxer
  adopts these values
- unfortunately, the recent trend has been that such values coming
  from video stream SPS (vui_parameters/timing info) are exorbitantly
  high - instead of being FPS *1000, they tend to be FPS * 100,000,000
- when trying to express the duration of the movie, the MP4 muxer
  normally tries to find the adequate timescale value which will
  fit both audio and video timescaling domains. The most suitable
  approach is that the LCM (least common multiplier) value is taken
  which mathematically will be the least disruptive.

HOWEVER:
- in cases when video and timescale numeric values are mutually 'odd',
  say 30*100,000,000 and 44100, the LCM ends up being a huge number,
  which outgrows the 32-bit storage capacity granted by the ISO MP4
  spec (MVHD box).

Problem solution:
1) identifying when the LCM timescale exceeds 32-bit storage space
2) scaling down its value by nearest larger 10X factor, which will
   guarantee its value fitting the 32-bit space. Given the afore
   mentioned video timescale factors, dividing by 10X is harmless
3) rescaling the duration 64-bit value based on the new timescale
2025-04-16 11:22:23 -03:00
Greyson Parrelli
597623d23a Update conscrypt to 2.5.3 2025-04-16 11:22:23 -03:00
Greyson Parrelli
2028afc941 Update aesgcmprovider to 0.0.4 2025-04-16 11:22:23 -03:00
Greyson Parrelli
915580ddd3 Enable backups v2 for internal users. 2025-04-16 11:22:23 -03:00
Greyson Parrelli
9432cca14a Fix some media not appearing in the gallery picker.
Works around the glide issue by using the straight URI when possible,
which allows glide to not have to keep a buffer. However, as soon as you
select it, it'll be an encrypted file, and we'll run into this same
issue where glide needs to keep a buffer for the input stream.

Related to #11014
2025-04-16 11:22:23 -03:00
Sagar
4e07ac0300 Fix InputAwareLayout incorrectly overriding height in bubble mode. 2025-04-16 11:22:23 -03:00
Sagar
ad21c349cd Update quote icon for scheduled send. 2025-04-16 11:22:23 -03:00
Greyson Parrelli
383da335d8 Do not send sync messages if we have no linked devices. 2025-04-16 11:22:23 -03:00
Jim Gustafson
ebdffc171e Update to RingRTC v2.50.5 2025-04-16 11:22:23 -03:00
Cody Henthorne
721b70b7b7 Fallback to local reglock data if available when registering a previously verified session. 2025-04-16 11:22:23 -03:00
Greyson Parrelli
556bcda58a Bump version to 7.40.2 2025-04-15 17:20:29 -04:00
Greyson Parrelli
4cb5bd9edd Fix potential bad state with change numbers. 2025-04-15 17:20:29 -04:00
Cody Henthorne
193f6460b0 Convert change number back to REST. 2025-04-15 17:05:19 -04:00
Alex Hart
f8d8c8af2d Add internal preference for large screen UI. 2025-04-15 15:29:37 -03:00
Alex Hart
efac6990c8 Apply display cutout for chat list. 2025-04-15 15:24:09 -03:00
Alex Hart
250ac481c8 Move cancel and retry to bg thread. 2025-04-15 13:15:16 -03:00
Michelle Tang
44bfa514a5 Fix member count description. 2025-04-15 11:05:58 -04:00
Alex Hart
74cedf99d8 Fix snackbar vertical offset on archive screen. 2025-04-15 11:33:11 -03:00
Alex Hart
4c81c321be Utilize root window insets for grabbing system bar sizes. 2025-04-15 11:14:16 -03:00
Alex Hart
d00fbcd886 Fix snackbar dismissal. 2025-04-15 10:25:47 -03:00
Alex Hart
416f80e745 Fix bad assumption about attachment count in validator. 2025-04-15 10:14:54 -03:00
Michelle Tang
6805826472 Bump version to 7.40.1 2025-04-11 11:03:55 -04:00
Michelle Tang
ce5d234186 Update translations and other static files. 2025-04-11 10:57:41 -04:00
Michelle Tang
c95c6e6ef0 Schedule storage sync job for add/remove from folder. 2025-04-11 10:49:33 -04:00
Cody Henthorne
904f8da8af Update settings for unregistered state. 2025-04-11 08:34:10 -04:00
Alex Hart
645e9bf16a Allow back press to return from archive to converation list. 2025-04-10 14:26:35 -03:00
Alex Hart
35235509ca Prevent wallpaper mode from overwriting navbar color. 2025-04-10 14:12:04 -03:00
Michelle Tang
021330a25d Fix adding to chats for chat folders. 2025-04-10 12:47:13 -04:00
Alex Hart
6613d5fccb Fix nav bar spacing and coloring. 2025-04-10 13:10:37 -03:00
Alex Hart
9d6e7560f0 Fix touch target for app toolbar avatar. 2025-04-10 12:14:18 -03:00
Alex Hart
09e36e0ed8 Fix 3 button nav styling. 2025-04-10 11:47:40 -03:00
Alex Hart
8dde5ccd2e Fix padding below toolbar in search mode with chat folders enabled. 2025-04-10 10:13:23 -03:00
Alex Hart
f1ed2156e3 Prevent scaffold from being used if we do not have flag enabled. 2025-04-10 10:00:58 -03:00
Michelle Tang
40b9a60f6c Bump version to 7.40.0 2025-04-09 16:54:03 -04:00
Michelle Tang
59a135a1db Update translations and other static files. 2025-04-09 16:53:42 -04:00
Michelle Tang
0123c17e7e Remove unnecessary boolean return for conversations. 2025-04-09 15:51:10 -04:00
Cody Henthorne
ac36eeb84d Use unauth WebSocket after quick restore for transfer mode decision. 2025-04-09 15:29:55 -04:00
Alex Hart
143b2b5bd5 Move live state into if statement for AvatarImage. 2025-04-09 15:29:54 -04:00
Michelle Tang
6006c047d8 Remove old deleted folders from storage service. 2025-04-09 15:29:54 -04:00
Alex Hart
94d5fe3e43 Fix how navigation bar colors are set and interacted with in MainActivity. 2025-04-09 15:29:54 -04:00
Alex Hart
e0ba8a1d60 Fix color issue on call toast popup. 2025-04-09 15:29:54 -04:00
Alex Hart
2f8b0ff3a8 Set corner radius of nav bar icons to half height. 2025-04-09 15:29:54 -04:00
Greyson Parrelli
4700846fad Align the libnative-utils to 16kb pages. 2025-04-09 15:29:54 -04:00
Greyson Parrelli
6ddf2ab5f8 Update to NDK r28 for 16kb page support. 2025-04-09 15:29:54 -04:00
Alex Hart
545a26ff04 Fix conversation nav bar click. 2025-04-09 15:29:54 -04:00
Sagar
f0f6b80f43 Prevent child clickable in message selection state. 2025-04-09 15:29:54 -04:00
Alex Hart
0227af199b Clear window insets listener when view is detached from window. 2025-04-09 15:29:54 -04:00
Alex Hart
970f5f2480 Add progress dialog support to bottom snackbar. 2025-04-09 15:29:54 -04:00
Sagar
13d0d25f77 Notify conversations for deleted stories. 2025-04-09 15:29:54 -04:00
Alex Hart
b64f3a48bf Add proper adaptive material app scaffolding. 2025-04-09 15:29:54 -04:00
Sagar
86ea3e8572 Fix thumbPositon jitter while editing video. 2025-04-09 15:29:54 -04:00
andrew-signal
f15a67c8b2 Remove outdated config check affecting proximity lock behavior during calling. 2025-04-09 15:29:54 -04:00
Alex Hart
659ae75a20 Fix content width shrinking megaphones. 2025-04-09 15:29:54 -04:00
Sagar
0d686b2f44 Fix Image expanded caption scroll to top. 2025-04-09 15:29:54 -04:00
andrew-signal
0d611cf4c9 Bump libsignal to v0.69.1. 2025-04-09 15:29:54 -04:00
Sagar
6afeb45f43 Remove duplicate error handling in MediaSelection. 2025-04-09 15:29:54 -04:00
Alex Hart
d81616d23c Prevent conversation re-launch on reconfiguration of screen. 2025-04-09 15:29:54 -04:00
Sagar
6ea63f3e34 Avoid sending blank replies and do not clear input when sending reactions. 2025-04-09 15:29:54 -04:00
Sagar
af52765821 Support opening scheduled document files. 2025-04-09 15:29:54 -04:00
Cody Henthorne
acbab9e736 Allow long text to be sent via notification replies. 2025-04-09 15:29:54 -04:00
Alex Hart
5bce2884a7 Add predictive back gesture support to MainActivity. 2025-04-09 15:29:52 -04:00
Alex Hart
b92998be13 Fix image loading for megaphones. 2025-04-09 15:27:46 -04:00
Michelle Tang
1339929de4 Update chat folder tests. 2025-04-09 15:27:46 -04:00
Alex Hart
b0cd27e203 Add compose megaphone stuff to MainBottomChrome composable. 2025-04-09 15:27:46 -04:00
Sagar
65e7c4c053 Support zoom for avatar preview. 2025-04-09 15:27:46 -04:00
Sagar
8d8519b52e Linkify story captions. 2025-04-09 15:27:46 -04:00
Sagar
9c95cfd64b Fix donation pills UI for large Font and other UI improvements. 2025-04-09 15:27:46 -04:00
Sagar
b0a903b17d Make FABs stack scrollable for small height in landscape mode. 2025-04-09 15:27:46 -04:00
Alex Hart
855b315067 Reimplement megaphone UI in compose. 2025-04-09 15:27:46 -04:00
Jeffrey Starke
aa7b61ecb1 Consolidate duplicated logic to retrieve groups in common.
Merges all of these into GroupsInCommonRepository:
- ConversationSettingsRepository.getGroupsInCommon()
- CallLinkIncomingRequestRepository.getGroupsInCommon()
- ContactSearchPagedDataSourceRepository.getGroupsInCommon()
- ReviewUtil.getGroupsInCommonCount()
- AboutSheetRepository.getGroupsInCommonCount()
2025-04-09 15:27:46 -04:00
Alex Hart
c9795141df Pass InAppPayments around by ID instead of passing the entire object. 2025-04-09 15:27:46 -04:00
andrew-signal
1aed82d5b7 Depend on libsignal-net's connection backoff instead of duplicating at app-level. 2025-04-09 15:27:46 -04:00
Michelle Tang
752ed93b6f Update blocked string for groups. 2025-04-09 15:27:46 -04:00
Ciphreon
de3088f706 Show "declined" for declined voice and video calls instead of "missed".
Closes #14081
Fixes #14080
2025-04-09 15:27:46 -04:00
Jeffrey Starke
2608e9165c Fix group member review avatar and "other groups in common" copy. (#4813)
- Fixes `ReviewBannerView` erroneously using the note to self icon instead of the current user's profile photo.
- Fixes the "other groups in common" copy, which was missing the word "other".
2025-04-09 15:27:46 -04:00
Cody Henthorne
1e0e165eaf Fix decryptionDrained flag if race for WebSocket state emission is lost. 2025-04-09 15:27:46 -04:00
Michelle Tang
eff90aaa64 Fix job checks when syncing folders with storage service id. 2025-04-09 15:27:46 -04:00
Jeffrey Starke
77078e1844 Add the ability to navigate to conversations by tapping groups in common rows. 2025-04-09 15:27:46 -04:00
Michelle Tang
5929021166 Fix null storageIds in chat folder crash. 2025-04-09 15:27:46 -04:00
andrew-signal
8317e2e055 Correct RemoteConfig to enable libsignalWebSocket for nightly builds. 2025-04-09 15:27:46 -04:00
Michelle Tang
eb1cf8d62f Add chat folder support to storage service. 2025-04-09 15:27:46 -04:00
Cody Henthorne
f6ecb572b1 Fix lint for IAP test and main toolbar. 2025-04-09 15:27:46 -04:00
Alex Bakon
8b9fc30b97 Migrate calls to deprecated libsignal methods. 2025-04-09 15:27:46 -04:00
Sagar
d65954c26f Improve AvatarImage to update on recipient changes. 2025-04-09 15:27:46 -04:00
Cody Henthorne
8a0e260061 Re-migrate delete account to WebSocket. 2025-04-09 15:27:46 -04:00
Jeffrey Starke
bb608dbfa7 Fix missing timestamps on undownloaded media messages. 2025-04-09 15:27:46 -04:00
Michelle Tang
ec5a7e1e48 Prevent recipient hot loop on main thread. 2025-04-09 15:27:46 -04:00
Sagar
6251dad6e0 Update MyStoryItem on profile change. 2025-04-09 15:27:46 -04:00
Sagar
3982f5a4db Remove prefix before username aci fetch. 2025-04-09 15:27:46 -04:00
Sagar
a8f8760a11 Support scroll for call link screens. 2025-04-09 15:27:46 -04:00
Sagar
fb571ffdbf fixup! Update profile initials after name change. 2025-04-09 15:27:46 -04:00
Sagar
dc2956d05b Update quoteIds for edited message and ignore stale messages in isQuoted. 2025-04-09 15:27:46 -04:00
Jeffrey Starke
85b19bfe23 Fix incorrectly oriented back navigation icons for top app bar RTL layouts.
Replace `symbol_arrow_left_24` with `symbol_arrow_start_24` (which has auto-mirroring enabled) for top app bar navigation back icons to properly support RTL layouts.
2025-04-09 15:27:46 -04:00
andrew-signal
5b04107447 Update to and integrate with libsignal v0.69.0. 2025-04-09 15:27:46 -04:00
Cody Henthorne
7a5790a6ce Attempt to reclaim username in more places during/after registration. 2025-04-09 15:27:46 -04:00
Jeffrey Starke
9d3f4ffa08 Add groups in common screen.
Adds a new screen to show which groups the user has in common with another user.
2025-04-09 15:27:46 -04:00
Sagar
bc2d4a0415 Fix badge bottomsheet color and scrolling. 2025-04-09 15:27:46 -04:00
Alex Hart
cc346351f7 Use state to support back pressed callback. 2025-04-09 15:27:46 -04:00
Cody Henthorne
fcc6032ee0 Generalize preventing WebSocket from connecting in various app states. 2025-04-09 15:27:46 -04:00
Cody Henthorne
ecb040ce98 Convert donations apis to WebSocket. 2025-04-09 15:27:46 -04:00
Sagar
2f9692a1a0 Prevent wrong closing animation for stories. 2025-04-09 15:27:46 -04:00
Sagar
042ab95738 Fix EmojiView scroll in bottomsheet. 2025-04-09 15:27:45 -04:00
Sagar
13be8d511c Focus on correct textfield when adding a description. 2025-04-09 15:27:45 -04:00
Cody Henthorne
7bdfec77ca Remove message send REST fallback. 2025-04-09 15:27:45 -04:00
Alex Hart
bc176b8c50 Fix application crash when failing to download backup types. 2025-04-09 15:27:45 -04:00
Alex Hart
68c0307b73 Upgrade compose bom to latest stable. 2025-04-09 15:27:45 -04:00
688 changed files with 31787 additions and 15338 deletions

View File

@@ -8,7 +8,7 @@ permissions:
pull-requests: write # to comment on PR
env:
NDK_VERSION: '27.2.12479018'
NDK_VERSION: '28.0.13004108'
jobs:
assemble-base:

View File

@@ -21,9 +21,9 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1535
val canonicalVersionName = "7.39.5"
val currentHotfixVersion = 1
val canonicalVersionCode = 1545
val canonicalVersionName = "7.42.2"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
val keystores: Map<String, Properties?> = mapOf("debug" to loadKeystoreProperties("keystore.debug.properties"))
@@ -210,7 +210,8 @@ android {
buildConfigField("String[]", "SIGNAL_CDSI_IPS", rootProject.extra["cdsi_ips"] as String)
buildConfigField("String[]", "SIGNAL_SVR2_IPS", rootProject.extra["svr2_ips"] as String)
buildConfigField("String", "SIGNAL_AGENT", "\"OWA\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570\"")
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY", "\"9314436a9a144992bb3680770ea5fd7934a7ffd29257844a33763a238903d570\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"093be9ea32405e85ae28dbb48eb668aebeb7dbe29517b9b86ad4bec4dfe0e6a6\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BXu6QIKVz5MA8gstzfOgRQGqyLqOwNKHL6INkv3IHWMF\"")
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"AMhf5ywVwITZMsff/eCyudZx9JDmkkkbV6PInzG4p8x3VqVJSFiMvnvlEKWuRob/1eaIetR31IYeAbm0NdOuHH8Qi+Rexi1wLlpzIo1gstHWBfZzy1+qHRV5A4TqPp15YzBPm0WSggW6PbSn+F4lf57VCnHF7p8SvzAA2ZZJPYJURt8X7bbg+H3i+PEjH9DXItNEqs2sNcug37xZQDLm7X36nOoGPs54XsEGzPdEV+itQNGUFEjY6X9Uv+Acuks7NpyGvCoKxGwgKgE5XyJ+nNKlyHHOLb6N1NuHyBrZrgtY/JYJHRooo5CEqYKBqdFnmbTVGEkCvJKxLnjwKWf+fEPoWeQFj5ObDjcKMZf2Jm2Ae69x+ikU5gBXsRmoF94GXTLfN0/vLt98KDPnxwAQL9j5V1jGOY8jQl6MLxEs56cwXN0dqCnImzVH3TZT1cJ8SW1BRX6qIVxEzjsSGx3yxF3suAilPMqGRp4ffyopjMD1JXiKR2RwLKzizUe5e8XyGOy9fplzhw3jVzTRyUZTRSZKkMLWcQ/gv0E4aONNqs4P+NameAZYOD12qRkxosQQP5uux6B2nRyZ7sAV54DgFyLiRcq1FvwKw2EPQdk4HDoePrO/RNUbyNddnM/mMgj4FW65xCoT1LmjrIjsv/Ggdlx46ueczhMgtBunx1/w8k8V+l8LVZ8gAT6wkU5J+DPQalQguMg12Jzug3q4TbdHiGCmD9EunCwOmsLuLJkz6EcSYXtrlDEnAM+hicw7iergYLLlMXpfTdGxJCWJmP4zqUFeTTmsmhsjGBt7NiEB/9pFFEB3pSbf4iiUukw63Eo8Aqnf4iwob6X1QviCWuc8t0LUlT9vALgh/f2DPVOOmR0RW6bgRvc7DSF20V/omg+YBw==\"")
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AByD873dTilmOSG0TjKrvpeaKEsUmIO8Vx9BeMmftwUs9v7ikPwM8P3OHyT0+X3EUMZrSe9VUp26Wai51Q9I8mdk0hX/yo7CeFGJyzoOqn8e/i4Ygbn5HoAyXJx5eXfIbqpc0bIxzju4H/HOQeOpt6h742qii5u/cbwOhFZCsMIbElZTaeU+BWMBQiZHIGHT5IE0qCordQKZ5iPZom0HeFa8Yq0ShuEyAl0WINBiY6xE3H/9WnvzXBbMuuk//eRxXgzO8ieCeK8FwQNxbfXqZm6Ro1cMhCOF3u7xoX83QhpN\"")
@@ -370,6 +371,7 @@ android {
buildConfigField("boolean", "MANAGES_APP_UPDATES", "true")
buildConfigField("String", "APK_UPDATE_MANIFEST_URL", "\"${apkUpdateManifestUrl}\"")
buildConfigField("String", "BUILD_DISTRIBUTION_TYPE", "\"nightly\"")
buildConfigField("boolean", "MESSAGE_BACKUP_RESTORE_ENABLED", "true")
}
create("prod") {
@@ -393,7 +395,8 @@ android {
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3-staging.signal.org\"")
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3\"")
buildConfigField("String", "SVR2_MRENCLAVE_LEGACY", "\"38e01eff4fe357dc0b0e8ef7a44b4abc5489fbccba3a78780f3872c277f62bf3\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"2e8cefe6e3f389d8426adb24e9b7fb7adf10902c96f06f7bbcee36277711ed91\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")
buildConfigField("String", "ZKGROUP_SERVER_PUBLIC_PARAMS", "\"ABSY21VckQcbSXVNCGRYJcfWHiAMZmpTtTELcDmxgdFbtp/bWsSxZdMKzfCp8rvIs8ocCU3B37fT3r4Mi5qAemeGeR2X+/YmOGR5ofui7tD5mDQfstAI9i+4WpMtIe8KC3wU5w3Inq3uNWVmoGtpKndsNfwJrCg0Hd9zmObhypUnSkfYn2ooMOOnBpfdanRtrvetZUayDMSC5iSRcXKpdlukrpzzsCIvEwjwQlJYVPOQPj4V0F4UXXBdHSLK05uoPBCQG8G9rYIGedYsClJXnbrgGYG3eMTG5hnx4X4ntARBgELuMWWUEEfSK0mjXg+/2lPmWcTZWR9nkqgQQP0tbzuiPm74H2wMO4u1Wafe+UwyIlIT9L7KLS19Aw8r4sPrXZSSsOZ6s7M1+rTJN0bI5CKY2PX29y5Ok3jSWufIKcgKOnWoP67d5b2du2ZVJjpjfibNIHbT/cegy/sBLoFwtHogVYUewANUAXIaMPyCLRArsKhfJ5wBtTminG/PAvuBdJ70Z/bXVPf8TVsR292zQ65xwvWTejROW6AZX6aqucUjlENAErBme1YHmOSpU6tr6doJ66dPzVAWIanmO/5mgjNEDeK7DDqQdB1xd03HT2Qs2TxY3kCK8aAb/0iM0HQiXjxZ9HIgYhbtvGEnDKW5ILSUydqH/KBhW4Pb0jZWnqN/YgbWDKeJxnDbYcUob5ZY5Lt5ZCMKuaGUvCJRrCtuugSMaqjowCGRempsDdJEt+cMaalhZ6gczklJB/IbdwENW9KeVFPoFNFzhxWUIS5ML9riVYhAtE6JE5jX0xiHNVIIPthb458cfA8daR0nYfYAUKogQArm0iBezOO+mPk5vCNWI+wwkyFCqNDXz/qxl1gAntuCJtSfq9OC3NkdhQlgYQ==\"")
buildConfigField("String", "GENERIC_SERVER_PUBLIC_PARAMS", "\"AHILOIrFPXX9laLbalbA9+L1CXpSbM/bTJXZGZiuyK1JaI6dK5FHHWL6tWxmHKYAZTSYmElmJ5z2A5YcirjO/yfoemE03FItyaf8W1fE4p14hzb5qnrmfXUSiAIVrhaXVwIwSzH6RL/+EO8jFIjJ/YfExfJ8aBl48CKHgu1+A6kWynhttonvWWx6h7924mIzW0Czj2ROuh4LwQyZypex4GuOPW8sgIT21KNZaafgg+KbV7XM1x1tF3XA17B4uGUaDbDw2O+nR1+U5p6qHPzmJ7ggFjSN6Utu+35dS1sS0P9N\"")

View File

@@ -21,8 +21,6 @@ import assertk.assertions.isEqualTo
import assertk.assertions.isNull
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import io.mockk.mockkStatic
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.runBlocking
@@ -42,12 +40,10 @@ import org.thoughtcrime.securesms.database.InAppPaymentTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.testing.CoroutineDispatcherRule
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.util.RemoteConfig
import org.whispersystems.signalservice.api.NetworkResult
import java.math.BigDecimal
import java.util.Currency
@@ -72,11 +68,6 @@ class MessageBackupsCheckoutActivityTest {
coEvery { AppDependencies.billingApi.queryProduct() } returns BillingProduct(price = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")))
coEvery { AppDependencies.billingApi.launchBillingFlow(any()) } returns Unit
mockkObject(SignalNetwork)
every { SignalNetwork.archive } returns mockk {
every { triggerBackupIdReservation(any(), any(), any()) } returns NetworkResult.Success(Unit)
}
mockkStatic(RemoteConfig::class)
every { RemoteConfig.messageBackups } returns true
}

View File

@@ -11,7 +11,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import io.mockk.every
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -20,15 +20,13 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.database.model.InAppPaymentSubscriberRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.testing.Delete
import org.thoughtcrime.securesms.testing.Get
import org.thoughtcrime.securesms.testing.InAppPaymentsRule
import org.thoughtcrime.securesms.testing.RxTestSchedulerRule
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.testing.actions.RecyclerViewScrollToBottomAction
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.subscriptions.ActiveSubscription
import org.whispersystems.signalservice.api.subscriptions.SubscriberId
import java.math.BigDecimal
@@ -118,32 +116,28 @@ class CheckoutFlowActivityTest__RecurringDonations {
InAppPaymentsRepository.setSubscriber(subscriber)
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
MockResponse().success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
true,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"active",
"STRIPE",
"CARD",
false
),
null
)
AppDependencies.donationsApi.apply {
every { getSubscription(subscriber.subscriberId) } returns NetworkResult.Success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
true,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"active",
"STRIPE",
"CARD",
false
),
null
)
},
Delete("/v1/subscription/${subscriber.subscriberId.serialize()}") {
Thread.sleep(10000)
MockResponse().success()
}
)
)
every { deleteSubscription(subscriber.subscriberId) } returns NetworkResult.Success(Unit)
}
}
private fun initialisePendingSubscription() {
@@ -160,27 +154,25 @@ class CheckoutFlowActivityTest__RecurringDonations {
InAppPaymentsRepository.setSubscriber(subscriber)
SignalStore.inAppPayments.setRecurringDonationCurrency(currency)
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/${subscriber.subscriberId.serialize()}") {
MockResponse().success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"incomplete",
"STRIPE",
"CARD",
false
),
null
)
AppDependencies.donationsApi.apply {
every { getSubscription(subscriber.subscriberId) } returns NetworkResult.Success(
ActiveSubscription(
ActiveSubscription.Subscription(
200,
currency.currencyCode,
BigDecimal.ONE,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
System.currentTimeMillis().milliseconds.inWholeSeconds + 30.days.inWholeSeconds,
false,
"incomplete",
"STRIPE",
"CARD",
false
),
null
)
}
)
)
}
}
}

View File

@@ -238,6 +238,23 @@ class AttachmentTableTest {
assertArrayEquals(digest, newDigest)
}
@Test
fun resetArchiveTransferStateByDigest_singleMatch() {
// Given an attachment with some digest
val blob = BlobProvider.getInstance().forData(byteArrayOf(1, 2, 3, 4, 5)).createForSingleSessionInMemory()
val attachment = createAttachment(1, blob, AttachmentTable.TransformProperties.empty())
val attachmentId = SignalDatabase.attachments.insertAttachmentsForMessage(-1L, listOf(attachment), emptyList()).values.first()
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, AttachmentTableTestUtil.createUploadResult(attachmentId))
SignalDatabase.attachments.setArchiveTransferState(attachmentId, AttachmentTable.ArchiveTransferState.FINISHED)
// Reset the transfer state by digest
val digest = SignalDatabase.attachments.getAttachment(attachmentId)!!.remoteDigest!!
SignalDatabase.attachments.resetArchiveTransferStateByDigest(digest)
// Verify it's been reset
assertThat(SignalDatabase.attachments.getAttachment(attachmentId)!!.archiveTransferState).isEqualTo(AttachmentTable.ArchiveTransferState.NONE)
}
private fun createAttachmentPointer(key: ByteArray, digest: ByteArray, size: Int): Attachment {
return PointerAttachment.forPointer(
pointer = Optional.of(

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.database
import org.signal.core.util.Base64
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import kotlin.random.Random
object AttachmentTableTestUtil {
fun createUploadResult(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): AttachmentUploadResult {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
return AttachmentUploadResult(
remoteId = SignalServiceAttachmentRemoteId.V4("somewhere-${Random.nextLong()}"),
cdnNumber = Cdn.CDN_3.cdnNumber,
key = databaseAttachment.remoteKey?.let { Base64.decode(it) } ?: Util.getSecretBytes(64),
iv = databaseAttachment.remoteIv ?: Util.getSecretBytes(16),
digest = Random.nextBytes(32),
incrementalDigest = Random.nextBytes(16),
incrementalDigestChunkSize = 5,
uploadTimestamp = uploadTimestamp,
dataSize = databaseAttachment.size,
blurHash = databaseAttachment.blurHash?.hash
)
}
}

View File

@@ -28,8 +28,6 @@ import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.attachment.AttachmentUploadResult
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import java.io.File
@@ -729,7 +727,7 @@ class AttachmentTableTest_deduping {
fun upload(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()) {
SignalDatabase.attachments.createKeyIvIfNecessary(attachmentId)
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, createUploadResult(attachmentId, uploadTimestamp))
SignalDatabase.attachments.finalizeAttachmentAfterUpload(attachmentId, AttachmentTableTestUtil.createUploadResult(attachmentId, uploadTimestamp))
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
SignalDatabase.attachments.setArchiveCdn(
@@ -875,23 +873,6 @@ class AttachmentTableTest_deduping {
private fun ByteArray.asMediaStream(): MediaStream {
return MediaStream(this.inputStream(), MediaUtil.IMAGE_JPEG, 2, 2)
}
private fun createUploadResult(attachmentId: AttachmentId, uploadTimestamp: Long = System.currentTimeMillis()): AttachmentUploadResult {
val databaseAttachment = SignalDatabase.attachments.getAttachment(attachmentId)!!
return AttachmentUploadResult(
remoteId = SignalServiceAttachmentRemoteId.V4("somewhere-${Random.nextLong()}"),
cdnNumber = Cdn.CDN_3.cdnNumber,
key = databaseAttachment.remoteKey?.let { Base64.decode(it) } ?: Util.getSecretBytes(64),
iv = databaseAttachment.remoteIv ?: Util.getSecretBytes(16),
digest = Random.nextBytes(32),
incrementalDigest = Random.nextBytes(16),
incrementalDigestChunkSize = 5,
uploadTimestamp = uploadTimestamp,
dataSize = databaseAttachment.size,
blurHash = databaseAttachment.blurHash?.hash
)
}
}
private fun test(content: TestContext.() -> Unit) {

View File

@@ -6,16 +6,26 @@
package org.thoughtcrime.securesms.database
import androidx.test.ext.junit.runners.AndroidJUnit4
import okio.ByteString.Companion.toByteString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.deleteAll
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderId
import org.thoughtcrime.securesms.components.settings.app.chats.folders.ChatFolderRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.storage.SignalChatFolderRecord
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.api.util.UuidUtil
import java.util.UUID
import org.whispersystems.signalservice.internal.storage.protos.ChatFolderRecord as RemoteChatFolderRecord
@RunWith(AndroidJUnit4::class)
class ChatFolderTablesTest {
@@ -31,15 +41,19 @@ class ChatFolderTablesTest {
private lateinit var folder2: ChatFolderRecord
private lateinit var folder3: ChatFolderRecord
private lateinit var recipientIds: List<RecipientId>
private var aliceThread: Long = 0
private var bobThread: Long = 0
private var charlieThread: Long = 0
@Before
fun setUp() {
alice = harness.others[1]
bob = harness.others[2]
charlie = harness.others[3]
recipientIds = createRecipients(5)
alice = recipientIds[0]
bob = recipientIds[1]
charlie = recipientIds[2]
aliceThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
bobThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(bob))
@@ -48,32 +62,40 @@ class ChatFolderTablesTest {
folder1 = ChatFolderRecord(
id = 2,
name = "folder1",
position = 1,
position = 0,
includedChats = listOf(aliceThread, bobThread),
excludedChats = listOf(charlieThread),
showUnread = true,
showMutedChats = true,
showIndividualChats = true,
folderType = ChatFolderRecord.FolderType.CUSTOM
folderType = ChatFolderRecord.FolderType.CUSTOM,
chatFolderId = ChatFolderId.generate(),
storageServiceId = StorageId.forChatFolder(byteArrayOf(1, 2, 3))
)
folder2 = ChatFolderRecord(
name = "folder2",
position = 2,
includedChats = listOf(bobThread),
showUnread = true,
showMutedChats = true,
showIndividualChats = true,
folderType = ChatFolderRecord.FolderType.INDIVIDUAL
folderType = ChatFolderRecord.FolderType.INDIVIDUAL,
chatFolderId = ChatFolderId.generate(),
storageServiceId = StorageId.forChatFolder(byteArrayOf(2, 3, 4))
)
folder3 = ChatFolderRecord(
name = "folder3",
position = 3,
includedChats = listOf(bobThread),
excludedChats = listOf(aliceThread, charlieThread),
showUnread = true,
showMutedChats = true,
showGroupChats = true,
folderType = ChatFolderRecord.FolderType.GROUP
folderType = ChatFolderRecord.FolderType.GROUP,
chatFolderId = ChatFolderId.generate(),
storageServiceId = StorageId.forChatFolder(byteArrayOf(3, 4, 5))
)
SignalDatabase.chatFolders.writableDatabase.deleteAll(ChatFolderTables.ChatFolderTable.TABLE_NAME)
@@ -83,7 +105,7 @@ class ChatFolderTablesTest {
@Test
fun givenChatFolder_whenIGetFolder_thenIExpectFolderWithChats() {
SignalDatabase.chatFolders.createFolder(folder1)
val actualFolders = SignalDatabase.chatFolders.getChatFolders()
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
assertEquals(listOf(folder1), actualFolders)
}
@@ -91,7 +113,7 @@ class ChatFolderTablesTest {
@Test
fun givenChatFolder_whenIUpdateFolder_thenIExpectUpdatedFolderWithChats() {
SignalDatabase.chatFolders.createFolder(folder2)
val folder = SignalDatabase.chatFolders.getChatFolders().first()
val folder = SignalDatabase.chatFolders.getCurrentChatFolders().first()
val updatedFolder = folder.copy(
name = "updatedFolder2",
position = 1,
@@ -100,7 +122,7 @@ class ChatFolderTablesTest {
)
SignalDatabase.chatFolders.updateFolder(updatedFolder)
val actualFolder = SignalDatabase.chatFolders.getChatFolders().first()
val actualFolder = SignalDatabase.chatFolders.getCurrentChatFolders().first()
assertEquals(updatedFolder, actualFolder)
}
@@ -109,11 +131,77 @@ class ChatFolderTablesTest {
fun givenADeletedChatFolder_whenIGetFolders_thenIExpectAListWithoutThatFolder() {
SignalDatabase.chatFolders.createFolder(folder1)
SignalDatabase.chatFolders.createFolder(folder2)
val folders = SignalDatabase.chatFolders.getChatFolders()
val folders = SignalDatabase.chatFolders.getCurrentChatFolders()
SignalDatabase.chatFolders.deleteChatFolder(folders.last())
val actualFolders = SignalDatabase.chatFolders.getChatFolders()
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
assertEquals(listOf(folder1), actualFolders)
}
@Test
fun givenChatFolders_whenIUpdateTheirStorageSyncIds_thenIExpectAnUpdatedList() {
val existingMap = SignalDatabase.chatFolders.getStorageSyncIdsMap()
existingMap.forEach { (id, _) ->
SignalDatabase.chatFolders.applyStorageIdUpdate(id, StorageId.forChatFolder(StorageSyncHelper.generateKey()))
}
val updatedMap = SignalDatabase.chatFolders.getStorageSyncIdsMap()
existingMap.forEach { (id, storageId) ->
assertNotEquals(storageId, updatedMap[id])
}
}
@Test
fun givenARemoteFolder_whenIInsertLocally_thenIExpectAListWithThatFolder() {
val remoteRecord =
SignalChatFolderRecord(
folder1.storageServiceId!!,
RemoteChatFolderRecord(
identifier = UuidUtil.toByteArray(folder1.chatFolderId.uuid).toByteString(),
name = folder1.name,
position = folder1.position,
showOnlyUnread = folder1.showUnread,
showMutedChats = folder1.showMutedChats,
includeAllIndividualChats = folder1.showIndividualChats,
includeAllGroupChats = folder1.showGroupChats,
folderType = RemoteChatFolderRecord.FolderType.CUSTOM,
deletedAtTimestampMs = folder1.deletedTimestampMs,
includedRecipients = listOf(
RemoteChatFolderRecord.Recipient(RemoteChatFolderRecord.Recipient.Contact(Recipient.resolved(alice).serviceId.get().toString())),
RemoteChatFolderRecord.Recipient(RemoteChatFolderRecord.Recipient.Contact(Recipient.resolved(bob).serviceId.get().toString()))
),
excludedRecipients = listOf(
RemoteChatFolderRecord.Recipient(RemoteChatFolderRecord.Recipient.Contact(Recipient.resolved(charlie).serviceId.get().toString()))
)
)
)
SignalDatabase.chatFolders.insertChatFolderFromStorageSync(remoteRecord)
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
assertEquals(listOf(folder1), actualFolders)
}
@Test
fun givenADeletedChatFolder_whenIGetPositions_thenIExpectPositionsToStillBeConsecutive() {
SignalDatabase.chatFolders.createFolder(folder1)
SignalDatabase.chatFolders.createFolder(folder2)
SignalDatabase.chatFolders.createFolder(folder3)
val folders = SignalDatabase.chatFolders.getCurrentChatFolders()
SignalDatabase.chatFolders.deleteChatFolder(folders[1])
val actualFolders = SignalDatabase.chatFolders.getCurrentChatFolders()
actualFolders.forEachIndexed { index, folder ->
assertEquals(folder.position, index)
}
}
private fun createRecipients(count: Int): List<RecipientId> {
return (1..count).map {
SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
}
}
}

View File

@@ -26,10 +26,13 @@ import org.thoughtcrime.securesms.testing.runSync
import org.thoughtcrime.securesms.testing.success
import org.whispersystems.signalservice.api.SignalServiceDataStore
import org.whispersystems.signalservice.api.SignalServiceMessageSender
import org.whispersystems.signalservice.api.archive.ArchiveApi
import org.whispersystems.signalservice.api.attachment.AttachmentApi
import org.whispersystems.signalservice.api.donations.DonationsApi
import org.whispersystems.signalservice.api.keys.KeysApi
import org.whispersystems.signalservice.api.message.MessageApi
import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.api.websocket.SignalWebSocket
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
@@ -94,6 +97,7 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
networkInterceptors = emptyList(),
dns = Optional.of(SignalServiceNetworkAccess.DNS),
signalProxy = Optional.empty(),
systemHttpProxy = Optional.empty(),
zkGroupServerPublicParams = Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
genericServerPublicParams = Base64.decode(BuildConfig.GENERIC_SERVER_PUBLIC_PARAMS),
backupServerPublicParams = Base64.decode(BuildConfig.BACKUP_SERVER_PUBLIC_PARAMS),
@@ -122,6 +126,14 @@ class InstrumentationApplicationDependencyProvider(val application: Application,
return recipientCache
}
override fun provideArchiveApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket, pushServiceSocket: PushServiceSocket): ArchiveApi {
return mockk()
}
override fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi {
return mockk()
}
override fun provideSignalServiceMessageSender(
protocolStore: SignalServiceDataStore,
pushServiceSocket: PushServiceSocket,

View File

@@ -77,7 +77,7 @@ class SyncMessageProcessorTest_readSyncs {
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.alice)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
assertThat(threadRecord.unreadCount).isEqualTo(2)
assertThat(threadRecord.unreadCount).isEqualTo(1)
messageHelper.syncReadMessage(messageHelper.alice to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)
@@ -98,7 +98,7 @@ class SyncMessageProcessorTest_readSyncs {
val threadId = SignalDatabase.threads.getThreadIdFor(messageHelper.group.recipientId)!!
var threadRecord = SignalDatabase.threads.getThreadRecord(threadId)!!
assertThat(threadRecord.unreadCount).isEqualTo(2)
assertThat(threadRecord.unreadCount).isEqualTo(1)
messageHelper.syncReadMessage(messageHelper.bob to message2Timestamp, messageHelper.alice to editMessage1Timestamp1, messageHelper.alice to editMessage1Timestamp2)

View File

@@ -9,6 +9,7 @@ import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.ecc.ECKeyPair
import org.signal.libsignal.protocol.groups.state.SenderKeyRecord
import org.signal.libsignal.protocol.state.IdentityKeyStore
import org.signal.libsignal.protocol.state.IdentityKeyStore.IdentityChange
import org.signal.libsignal.protocol.state.KyberPreKeyRecord
import org.signal.libsignal.protocol.state.PreKeyBundle
import org.signal.libsignal.protocol.state.PreKeyRecord
@@ -137,7 +138,7 @@ class BobClient(val serviceId: ServiceId, val e164: String, val identityKeyPair:
override fun getLocalRegistrationId(): Int = registrationId
override fun isTrustedIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?, direction: IdentityKeyStore.Direction?): Boolean = true
override fun loadSession(address: SignalProtocolAddress?): SessionRecord = aliceSessionRecord ?: SessionRecord()
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): Boolean = false
override fun saveIdentity(address: SignalProtocolAddress?, identityKey: IdentityKey?): IdentityKeyStore.IdentityChange = IdentityChange.NEW_OR_UNCHANGED
override fun storeSession(address: SignalProtocolAddress?, record: SessionRecord?) {
aliceSessionRecord = record
}

View File

@@ -6,10 +6,11 @@
package org.thoughtcrime.securesms.testing
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockResponse
import io.mockk.every
import org.junit.rules.ExternalResource
import org.thoughtcrime.securesms.dependencies.InstrumentationApplicationDependencyProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
/**
@@ -23,29 +24,25 @@ class InAppPaymentsRule : ExternalResource() {
}
private fun initialiseConfigurationResponse() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Get("/v1/subscription/configuration") {
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
assets.open("inAppPaymentsTests/configuration.json").use { stream ->
MockResponse().success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
}
}
)
val assets = InstrumentationRegistry.getInstrumentation().context.resources.assets
val response = assets.open("inAppPaymentsTests/configuration.json").use { stream ->
NetworkResult.Success(JsonUtils.fromJson(stream, SubscriptionsConfiguration::class.java))
}
AppDependencies.donationsApi.apply {
every { getDonationsConfiguration(any()) } returns response
}
}
private fun initialisePutSubscription() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/subscription/") {
MockResponse().success()
}
)
AppDependencies.donationsApi.apply {
every { putSubscription(any()) } returns NetworkResult.Success(Unit)
}
}
private fun initialiseSetArchiveBackupId() {
InstrumentationApplicationDependencyProvider.addMockWebRequestHandlers(
Put("/v1/archives/backupid") {
MockResponse().success()
}
)
AppDependencies.archiveApi.apply {
every { triggerBackupIdReservation(any(), any(), any()) } returns NetworkResult.Success(Unit)
}
}
}

View File

@@ -67,7 +67,7 @@ private fun Content(
Scaffolds.Settings(
title = "Conversation Test Springboard",
onNavigationClick = onBackPressed,
navigationIconPainter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.symbol_arrow_left_24))
navigationIconPainter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.symbol_arrow_start_24))
) {
Column(modifier = Modifier.padding(it)) {
Rows.TextRow(

View File

@@ -101,6 +101,7 @@
android:supportsRtl="true"
android:resizeableActivity="true"
android:fullBackupOnly="false"
android:enableOnBackInvokedCallback="false"
android:allowBackup="true"
android:backupAgent=".absbackup.SignalBackupAgent"
android:theme="@style/TextSecure.LightTheme"
@@ -145,17 +146,6 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false" />
<activity android:name=".InviteActivity"
android:theme="@style/Signal.Light.NoActionBar.Invite"
android:windowSoftInputMode="stateHidden"
android:parentActivityName=".MainActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".MainActivity" />
</activity>
<activity android:name=".DeviceProvisioningActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="true">
@@ -888,11 +878,8 @@
android:exported="false"/>
<activity android:name=".stickers.StickerManagementActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightTheme"
android:windowSoftInputMode="stateUnchanged"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar" />
<activity android:name=".logsubmit.SubmitDebugLogActivity"
android:windowSoftInputMode="stateHidden"
@@ -1069,9 +1056,12 @@
android:exported="false"/>
<activity android:name=".MainActivity"
android:enableOnBackInvokedCallback="true"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout"
android:windowSoftInputMode="stateUnchanged"
android:resizeableActivity="true"
android:launchMode="singleTask"
android:exported="false"/>
<activity android:name=".pin.PinRestoreActivity"
@@ -1148,6 +1138,10 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".groups.ui.incommon.GroupsInCommonActivity"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar" />
<service
android:enabled="true"
android:exported="false"
@@ -1343,6 +1337,12 @@
</intent-filter>
</receiver>
<receiver android:name=".service.MessageBackupListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />

File diff suppressed because it is too large Load Diff

View File

@@ -38,6 +38,7 @@ import org.signal.core.util.logging.Log;
import org.signal.core.util.logging.Scrubber;
import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.libsignal.net.ChatServiceException;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
@@ -363,7 +364,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
e = e.getCause();
}
if (wasWrapped && (e instanceof SocketException || e instanceof InterruptedException || e instanceof InterruptedIOException)) {
if (wasWrapped && (e instanceof SocketException || e instanceof InterruptedException || e instanceof InterruptedIOException || e instanceof ChatServiceException)) {
return;
}
@@ -383,7 +384,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
}
public void initializeMessageRetrieval() {
AppDependencies.startNetwork();
SignalExecutors.UNBOUNDED.execute(AppDependencies::startNetwork);
}
@VisibleForTesting

View File

@@ -9,7 +9,6 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.transition.TransitionInflater;
import android.view.View;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -26,6 +25,7 @@ import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition;
import com.github.chrisbanes.photoview.PhotoView;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.avatar.fallback.FallbackAvatar;
@@ -46,6 +46,12 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
private static final String RECIPIENT_ID_EXTRA = "recipient_id";
private static final int ZOOM_TRANSITION_DURATION = 300;
private static final float ZOOM_LEVEL_MIN = 1.0f;
private static final float SMALL_IMAGES_ZOOM_LEVEL_MID = 3.0f;
private static final float SMALL_IMAGES_ZOOM_LEVEL_MAX = 8.0f;
public static @NonNull Intent intentFromRecipientId(@NonNull Context context,
@NonNull RecipientId recipientId)
{
@@ -78,7 +84,10 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
Toolbar toolbar = findViewById(R.id.toolbar);
EmojiTextView title = findViewById(R.id.title);
ImageView avatar = findViewById(R.id.avatar);
PhotoView avatar = findViewById(R.id.avatar);
avatar.setZoomTransitionDuration(ZOOM_TRANSITION_DURATION);
avatar.setScaleLevels(ZOOM_LEVEL_MIN, SMALL_IMAGES_ZOOM_LEVEL_MID, SMALL_IMAGES_ZOOM_LEVEL_MAX);
setSupportActionBar(toolbar);
@@ -134,7 +143,7 @@ public final class AvatarPreviewActivity extends PassphraseRequiredActivity {
FullscreenHelper fullscreenHelper = new FullscreenHelper(this);
findViewById(android.R.id.content).setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
avatar.setOnClickListener(v -> fullscreenHelper.toggleUiVisibility());
fullscreenHelper.configureToolbarLayout(findViewById(R.id.toolbar_cutout_spacer), toolbar);

View File

@@ -17,9 +17,11 @@ public interface BindableConversationListItem extends Unbindable {
@NonNull ThreadRecord thread,
@NonNull RequestManager requestManager, @NonNull Locale locale,
@NonNull Set<Long> typingThreads,
@NonNull ConversationSet selectedConversations);
@NonNull ConversationSet selectedConversations,
long activeThreadId);
void setSelectedConversations(@NonNull ConversationSet conversations);
void setActiveThreadId(long activeThreadId);
void updateTypingIndicator(@NonNull Set<Long> typingThreads);
void updateTimestamp();
}

View File

@@ -170,7 +170,7 @@ public abstract class ContactSelectionActivity extends PassphraseRequiredActivit
ContactSelectionActivity activity = this.activity.get();
if (activity != null && !activity.isFinishing()) {
activity.contactsFragment.resetQueryFilter();
activity.contactsFragment.onDataRefreshed();
}
}
}

View File

@@ -556,9 +556,11 @@ public final class ContactSelectionListFragment extends LoggingFragment {
public void resetQueryFilter() {
setQueryFilter(null);
onDataRefreshed();
}
public void onDataRefreshed() {
this.resetPositionOnCommit = true;
swipeRefresh.setRefreshing(false);
}

View File

@@ -1,267 +0,0 @@
package org.thoughtcrime.securesms;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.AnimRes;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.concurrent.ListenableFuture.Listener;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.components.ContactFilterView.OnFilterChangedListener;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.SelectedContact;
import org.thoughtcrime.securesms.contacts.paged.ChatType;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.mms.OutgoingMessage;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.util.DynamicNoActionBarInviteTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
public class InviteActivity extends PassphraseRequiredActivity implements ContactSelectionListFragment.OnContactSelectedListener {
private ContactSelectionListFragment contactsFragment;
private EditText inviteText;
private ViewGroup smsSendFrame;
private Button smsSendButton;
private Animation slideInAnimation;
private Animation slideOutAnimation;
private final DynamicTheme dynamicTheme = new DynamicNoActionBarInviteTheme();
@Override
protected void onPreCreate() {
super.onPreCreate();
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
getIntent().putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactSelectionDisplayMode.FLAG_SMS);
getIntent().putExtra(ContactSelectionListFragment.SELECTION_LIMITS, SelectionLimits.NO_LIMITS);
getIntent().putExtra(ContactSelectionListFragment.HIDE_COUNT, true);
getIntent().putExtra(ContactSelectionListFragment.REFRESHABLE, false);
setContentView(R.layout.invite_activity);
initializeAppBar();
initializeResources();
}
@Override
protected void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
private void initializeAppBar() {
final Toolbar primaryToolbar = findViewById(R.id.toolbar);
setSupportActionBar(primaryToolbar);
assert getSupportActionBar() != null;
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.AndroidManifest__invite_friends);
}
private void initializeResources() {
slideInAnimation = loadAnimation(R.anim.slide_from_bottom);
slideOutAnimation = loadAnimation(R.anim.slide_to_bottom);
View shareButton = findViewById(R.id.share_button);
TextView shareText = findViewById(R.id.share_text);
View smsButton = findViewById(R.id.sms_button);
Button smsCancelButton = findViewById(R.id.cancel_sms_button);
ContactFilterView contactFilter = findViewById(R.id.contact_filter_edit_text);
inviteText = findViewById(R.id.invite_text);
smsSendFrame = findViewById(R.id.sms_send_frame);
smsSendButton = findViewById(R.id.send_sms_button);
contactsFragment = (ContactSelectionListFragment)getSupportFragmentManager().findFragmentById(R.id.contact_selection_list_fragment);
inviteText.setText(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)));
inviteText.addTextChangedListener(new AfterTextChanged(editable -> {
boolean isEnabled = editable.length() > 0;
smsButton.setEnabled(isEnabled);
shareButton.setEnabled(isEnabled);
smsButton.animate().alpha(isEnabled ? 1f : 0.5f);
shareButton.animate().alpha(isEnabled ? 1f : 0.5f);
}));
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
smsCancelButton.setOnClickListener(new SmsCancelClickListener());
smsSendButton.setOnClickListener(new SmsSendClickListener());
contactFilter.setOnFilterChangedListener(new ContactFilterChangedListener());
smsButton.setVisibility(View.GONE);
shareText.setText(R.string.InviteActivity_share);
shareButton.setOnClickListener(new ShareClickListener());
}
private Animation loadAnimation(@AnimRes int animResId) {
final Animation animation = AnimationUtils.loadAnimation(this, animResId);
animation.setInterpolator(new FastOutSlowInInterpolator());
return animation;
}
@Override
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Optional<ChatType> chatType, @NonNull Consumer<Boolean> callback) {
updateSmsButtonText(contactsFragment.getSelectedContacts().size() + 1);
callback.accept(true);
}
@Override
public void onContactDeselected(@NonNull Optional<RecipientId> recipientId, String number, @NonNull Optional<ChatType> chatType) {
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
}
@Override
public void onSelectionChanged() {
}
private void sendSmsInvites() {
new SendSmsInvitesAsyncTask(this, inviteText.getText().toString())
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,
contactsFragment.getSelectedContacts()
.toArray(new SelectedContact[0]));
}
private void updateSmsButtonText(int count) {
smsSendButton.setText(getResources().getString(R.string.InviteActivity_send_sms, count));
smsSendButton.setEnabled(count > 0);
}
@Override public void onBackPressed() {
if (smsSendFrame.getVisibility() == View.VISIBLE) {
cancelSmsSelection();
} else {
super.onBackPressed();
}
}
@Override public boolean onSupportNavigateUp() {
if (smsSendFrame.getVisibility() == View.VISIBLE) {
cancelSmsSelection();
return false;
} else {
return super.onSupportNavigateUp();
}
}
private void cancelSmsSelection() {
contactsFragment.reset();
updateSmsButtonText(contactsFragment.getSelectedContacts().size());
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
}
private class ShareClickListener implements OnClickListener {
@Override
public void onClick(View v) {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, inviteText.getText().toString());
sendIntent.setType("text/plain");
if (sendIntent.resolveActivity(getPackageManager()) != null) {
startActivity(Intent.createChooser(sendIntent, getString(R.string.InviteActivity_invite_to_signal)));
} else {
Toast.makeText(InviteActivity.this, R.string.InviteActivity_no_app_to_share_to, Toast.LENGTH_LONG).show();
}
}
}
private class SmsCancelClickListener implements OnClickListener {
@Override
public void onClick(View v) {
cancelSmsSelection();
}
}
private class SmsSendClickListener implements OnClickListener {
@Override
public void onClick(View v) {
new MaterialAlertDialogBuilder(InviteActivity.this)
.setTitle(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_invites,
contactsFragment.getSelectedContacts().size(),
contactsFragment.getSelectedContacts().size()))
.setMessage(inviteText.getText().toString())
.setPositiveButton(R.string.yes, (dialog, which) -> sendSmsInvites())
.setNegativeButton(R.string.no, (dialog, which) -> dialog.dismiss())
.show();
}
}
private class ContactFilterChangedListener implements OnFilterChangedListener {
@Override
public void onFilterChanged(String filter) {
contactsFragment.setQueryFilter(filter);
}
}
@SuppressLint("StaticFieldLeak")
private class SendSmsInvitesAsyncTask extends ProgressDialogAsyncTask<SelectedContact,Void,Void> {
private final String message;
SendSmsInvitesAsyncTask(Context context, String message) {
super(context, R.string.InviteActivity_sending, R.string.InviteActivity_sending);
this.message = message;
}
@Override
protected Void doInBackground(SelectedContact... contacts) {
final Context context = getContext();
if (context == null) return null;
for (SelectedContact contact : contacts) {
RecipientId recipientId = contact.getOrCreateRecipientId();
Recipient recipient = Recipient.resolved(recipientId);
MessageSender.send(context, OutgoingMessage.sms(recipient, message), -1L, MessageSender.SendType.SMS, null, null);
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
super.onPostExecute(aVoid);
final Context context = getContext();
if (context == null) return;
ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE).addListener(new Listener<Boolean>() {
@Override
public void onSuccess(Boolean result) {
contactsFragment.reset();
}
@Override
public void onFailure(ExecutionException e) {}
});
Toast.makeText(context, R.string.InviteActivity_invitations_sent, Toast.LENGTH_LONG).show();
}
}
}

View File

@@ -0,0 +1,123 @@
package org.thoughtcrime.securesms
import android.content.Intent
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.compose.ComposeFragment
/**
* Fragment when inviting someone to use Signal
*/
class InviteFragment : ComposeFragment() {
@Composable
override fun FragmentContent() {
Scaffolds.Settings(
title = stringResource(id = R.string.AndroidManifest__invite_friends),
onNavigationClick = { requireActivity().onNavigateUp() },
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_start_24),
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close)
) { contentPadding: PaddingValues ->
InviteScreen(
onShare = { inviteText -> onShare(inviteText) },
modifier = Modifier.padding(contentPadding)
)
}
}
private fun onShare(inviteText: String) {
val sendIntent = Intent()
.setAction(Intent.ACTION_SEND)
.putExtra(Intent.EXTRA_TEXT, inviteText)
.setType("text/plain")
if (sendIntent.resolveActivity(requireContext().packageManager) != null) {
startActivity(Intent.createChooser(sendIntent, getString(R.string.InviteActivity_invite_to_signal)))
} else {
Toast.makeText(requireContext(), R.string.InviteActivity_no_app_to_share_to, Toast.LENGTH_LONG).show()
}
}
}
@Composable
fun InviteScreen(
onShare: (String) -> Unit = {},
modifier: Modifier = Modifier
) {
val default = stringResource(R.string.InviteActivity_lets_switch_to_signal, stringResource(R.string.install_url))
var inviteText by remember { mutableStateOf(TextFieldValue(default, TextRange(default.length))) }
Column(
modifier = modifier.padding(16.dp).fillMaxHeight()
) {
TextField(
value = inviteText,
onValueChange = { inviteText = it },
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
colors = TextFieldDefaults.colors(
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant
),
modifier = Modifier.fillMaxWidth().padding(bottom = 16.dp),
shape = RoundedCornerShape(12.dp)
)
Row(
modifier = Modifier
.clickable(onClick = { onShare(inviteText.text) })
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_share_android_24),
contentDescription = stringResource(R.string.InviteActivity_share)
)
Text(
text = stringResource(id = R.string.InviteActivity_share),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
}
}
@SignalPreview
@Composable
private fun InviteScreenPreview() {
Previews.Preview {
InviteScreen()
}
}

View File

@@ -5,78 +5,148 @@
package org.thoughtcrime.securesms
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.BoxWithConstraintsScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.displayCutoutPadding
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.fragment.app.DialogFragment
import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.rememberFragmentState
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getSerializableCompat
import org.signal.core.util.logging.Log
import org.signal.donations.StripeApi
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
import org.thoughtcrime.securesms.calls.log.CallLogFilter
import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet
import org.thoughtcrime.securesms.calls.log.CallLogFragment
import org.thoughtcrime.securesms.calls.new.NewCallActivity
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment
import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment
import org.thoughtcrime.securesms.components.compose.ConnectivityWarningBottomSheet
import org.thoughtcrime.securesms.components.compose.DeviceSpecificNotificationBottomSheet
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity.Companion.manageSubscriptions
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.conversation.v2.ConversationFragment
import org.thoughtcrime.securesms.conversation.v2.MotionEventRelay
import org.thoughtcrime.securesms.conversation.v2.ShareDataTimestampViewModel
import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment
import org.thoughtcrime.securesms.conversationlist.ConversationListFragment
import org.thoughtcrime.securesms.conversationlist.RelinkDevicesReminderBottomSheetFragment
import org.thoughtcrime.securesms.conversationlist.RestoreCompleteBottomSheetDialog
import org.thoughtcrime.securesms.conversationlist.model.ConversationFilter
import org.thoughtcrime.securesms.conversationlist.model.UnreadPaymentsLiveData
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceExitActivity
import org.thoughtcrime.securesms.groups.ui.creategroup.CreateGroupActivity
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.MainActivityListHostFragment
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity
import org.thoughtcrime.securesms.main.MainBottomChrome
import org.thoughtcrime.securesms.main.MainBottomChromeCallback
import org.thoughtcrime.securesms.main.MainBottomChromeState
import org.thoughtcrime.securesms.main.MainContentLayoutData
import org.thoughtcrime.securesms.main.MainMegaphoneState
import org.thoughtcrime.securesms.main.MainNavigationBar
import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationViewModel
import org.thoughtcrime.securesms.main.MainToolbar
import org.thoughtcrime.securesms.main.MainToolbarCallback
import org.thoughtcrime.securesms.main.MainToolbarMode
import org.thoughtcrime.securesms.main.MainToolbarState
import org.thoughtcrime.securesms.main.MainToolbarViewModel
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
import org.thoughtcrime.securesms.main.NavigationBarSpacerCompat
import org.thoughtcrime.securesms.main.SnackbarState
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
import org.thoughtcrime.securesms.megaphone.Megaphone
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
import org.thoughtcrime.securesms.megaphone.Megaphones
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor
import org.thoughtcrime.securesms.notifications.VitalsViewModel
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment
import org.thoughtcrime.securesms.stories.settings.StorySettingsActivity
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabRepository
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsFragment
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
import org.thoughtcrime.securesms.util.AppForegroundObserver
import org.thoughtcrime.securesms.util.AppStartup
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CachedInflater
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme
import org.thoughtcrime.securesms.util.DynamicTheme
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.SplashScreenUtil
import org.thoughtcrime.securesms.util.WindowUtil
import org.thoughtcrime.securesms.util.TopToastPopup
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.util.viewModel
import org.thoughtcrime.securesms.window.AppScaffold
import org.thoughtcrime.securesms.window.WindowSizeClass
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider {
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider, Material3OnScrollHelperBinder, ConversationListFragment.Callback, CallLogFragment.Callback {
companion object {
private val TAG = Log.tag(MainActivity::class)
private const val KEY_STARTING_TAB = "STARTING_TAB"
const val RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901
@@ -87,23 +157,23 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
@JvmStatic
fun clearTopAndOpenTab(context: Context, startingTab: MainNavigationDestination): Intent {
fun clearTopAndOpenTab(context: Context, startingTab: MainNavigationListLocation): Intent {
return clearTop(context).putExtra(KEY_STARTING_TAB, startingTab)
}
}
private val dynamicTheme = DynamicNoActionBarTheme()
private val navigator = MainNavigator(this)
private val lifecycleDisposable = LifecycleDisposable()
private lateinit var mediaController: VoiceNoteMediaController
private lateinit var navigator: MainNavigator
override val voiceNoteMediaController: VoiceNoteMediaController
get() = mediaController
private val conversationListTabsViewModel: ConversationListTabsViewModel by viewModel {
val startingTab = intent.extras?.getSerializableCompat(KEY_STARTING_TAB, MainNavigationDestination::class.java)
ConversationListTabsViewModel(startingTab ?: MainNavigationDestination.CHATS, ConversationListTabRepository())
private val mainNavigationViewModel: MainNavigationViewModel by viewModel {
val startingTab = intent.extras?.getSerializableCompat(KEY_STARTING_TAB, MainNavigationListLocation::class.java)
MainNavigationViewModel(startingTab ?: MainNavigationListLocation.CHATS)
}
private val vitalsViewModel: VitalsViewModel by viewModel {
@@ -118,60 +188,226 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
private val toolbarViewModel: MainToolbarViewModel by viewModels()
private val toolbarCallback = ToolbarCallback()
private val shareDataTimestampViewModel: ShareDataTimestampViewModel by viewModels()
private val motionEventRelay: MotionEventRelay by viewModels()
private var onFirstRender = false
private var previousTopToastPopup: TopToastPopup? = null
private val mainBottomChromeCallback = BottomChromeCallback()
private val megaphoneActionController = MainMegaphoneActionController()
private val mainNavigationCallback = MainNavigationCallback()
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return motionEventRelay.offer(ev) || super.dispatchTouchEvent(ev)
}
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) {
enableEdgeToEdge()
AppStartup.getInstance().onCriticalRenderEventStart()
enableEdgeToEdge(
navigationBarStyle = if (DynamicTheme.isDarkTheme(this)) {
SystemBarStyle.dark(0)
} else {
SystemBarStyle.light(0, 0)
}
)
super.onCreate(savedInstanceState, ready)
conversationListTabsViewModel
navigator = MainNavigator(this, mainNavigationViewModel)
setContent {
val navState = rememberFragmentState()
val listHostState = rememberFragmentState()
val detailLocation by navigator.viewModel.detailLocation.collectAsStateWithLifecycle()
AppForegroundObserver.addListener(object : AppForegroundObserver.Listener {
override fun onForeground() {
mainNavigationViewModel.getNextMegaphone()
}
})
LaunchedEffect(detailLocation) {
if (detailLocation is MainNavigationDetailLocation.Conversation) {
startActivity((detailLocation as MainNavigationDetailLocation.Conversation).intent)
overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out)
UnreadPaymentsLiveData().observe(this) { unread ->
toolbarViewModel.setHasUnreadPayments(unread.isPresent)
}
lifecycleScope.launch {
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
mainNavigationViewModel.navigationEvents.collectLatest {
when (it) {
MainNavigationViewModel.NavigationEvent.STORY_CAMERA_FIRST -> {
mainBottomChromeCallback.onCameraClick(MainNavigationListLocation.STORIES)
}
}
}
}
}
AppScaffold(
bottomNavContent = {
AndroidFragment(
clazz = ConversationListTabsFragment::class.java,
fragmentState = navState
)
},
navRailContent = {
AndroidFragment(
clazz = ConversationListTabsFragment::class.java,
fragmentState = navState
)
}
) {
Column {
val state by toolbarViewModel.state.collectAsStateWithLifecycle()
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(LocalContext.current)) {
MainToolbar(
state = state,
callback = toolbarCallback
)
launch {
mainNavigationViewModel.getNotificationProfiles().collectLatest { profiles ->
withContext(Dispatchers.Main) {
updateNotificationProfileStatus(profiles)
}
AndroidFragment(
clazz = MainActivityListHostFragment::class.java,
fragmentState = listHostState,
modifier = Modifier.fillMaxSize()
)
}
}
}
shareDataTimestampViewModel.setTimestampFromActivityCreation(savedInstanceState, intent)
setContent {
val snackbar by mainNavigationViewModel.snackbar.collectAsStateWithLifecycle()
val mainToolbarState by toolbarViewModel.state.collectAsStateWithLifecycle()
val megaphone by mainNavigationViewModel.megaphone.collectAsStateWithLifecycle()
val mainNavigationState by mainNavigationViewModel.mainNavigationState.collectAsStateWithLifecycle()
LaunchedEffect(mainNavigationState.selectedDestination) {
when (mainNavigationState.selectedDestination) {
MainNavigationListLocation.CHATS -> toolbarViewModel.presentToolbarForConversationListFragment()
MainNavigationListLocation.ARCHIVE -> toolbarViewModel.presentToolbarForConversationListArchiveFragment()
MainNavigationListLocation.CALLS -> toolbarViewModel.presentToolbarForCallLogFragment()
MainNavigationListLocation.STORIES -> toolbarViewModel.presentToolbarForStoriesLandingFragment()
}
}
val isNavigationVisible = remember(mainToolbarState.mode) {
mainToolbarState.mode == MainToolbarMode.FULL
}
val mainBottomChromeState = remember(mainToolbarState.destination, snackbar, mainToolbarState.mode, megaphone) {
MainBottomChromeState(
destination = mainToolbarState.destination,
snackbarState = snackbar,
mainToolbarMode = mainToolbarState.mode,
megaphoneState = MainMegaphoneState(
megaphone = megaphone,
mainToolbarMode = mainToolbarState.mode
)
)
}
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
val contentLayoutData = MainContentLayoutData.rememberContentLayoutData()
MainContainer {
val wrappedNavigator = rememberNavigator(windowSizeClass, contentLayoutData, maxWidth)
AppScaffold(
navigator = wrappedNavigator,
bottomNavContent = {
if (isNavigationVisible) {
Column(
modifier = Modifier
.clip(contentLayoutData.navigationBarShape)
.background(color = SignalTheme.colors.colorSurface2)
) {
MainNavigationBar(
state = mainNavigationState,
onDestinationSelected = mainNavigationCallback
)
if (!windowSizeClass.isSplitPane()) {
NavigationBarSpacerCompat()
}
}
}
},
navRailContent = {
if (isNavigationVisible) {
MainNavigationRail(
state = mainNavigationState,
mainFloatingActionButtonsCallback = mainBottomChromeCallback,
onDestinationSelected = mainNavigationCallback
)
}
},
listContent = {
val listContainerColor = if (windowSizeClass.isMedium()) {
SignalTheme.colors.colorSurface1
} else {
MaterialTheme.colorScheme.surface
}
Column(
modifier = Modifier
.padding(start = contentLayoutData.listPaddingStart)
.fillMaxSize()
.background(listContainerColor)
.clip(contentLayoutData.shape)
) {
MainToolbar(
state = mainToolbarState,
callback = toolbarCallback
)
Box(
modifier = Modifier.weight(1f)
) {
when (val destination = mainNavigationState.selectedDestination) {
MainNavigationListLocation.CHATS -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
clazz = ConversationListFragment::class.java,
fragmentState = state,
modifier = Modifier.fillMaxSize()
)
}
MainNavigationListLocation.ARCHIVE -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
clazz = ConversationListArchiveFragment::class.java,
fragmentState = state,
modifier = Modifier.fillMaxSize()
)
}
MainNavigationListLocation.CALLS -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
clazz = CallLogFragment::class.java,
fragmentState = state,
modifier = Modifier.fillMaxSize()
)
}
MainNavigationListLocation.STORIES -> {
val state = key(destination) { rememberFragmentState() }
AndroidFragment(
clazz = StoriesLandingFragment::class.java,
fragmentState = state,
modifier = Modifier.fillMaxSize()
)
}
}
MainBottomChrome(
state = mainBottomChromeState,
callback = mainBottomChromeCallback,
megaphoneActionController = megaphoneActionController,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
},
detailContent = {
when (val destination = wrappedNavigator.currentDestination?.contentKey) {
is MainNavigationDetailLocation.Conversation -> {
val fragmentState = key(destination) { rememberFragmentState() }
AndroidFragment(
clazz = ConversationFragment::class.java,
fragmentState = fragmentState,
arguments = requireNotNull(destination.intent.extras) { "Handed null Conversation intent arguments." },
modifier = Modifier
.padding(end = contentLayoutData.detailPaddingEnd)
.clip(contentLayoutData.shape)
.background(color = MaterialTheme.colorScheme.surface)
.fillMaxSize()
)
}
}
},
paneExpansionDragHandle = if (contentLayoutData.hasDragHandle()) {
{ }
} else null
)
}
}
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
@@ -191,11 +427,72 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
handleDeepLinkIntent(intent)
CachedInflater.from(this).clear()
updateNavigationBarColor()
lifecycleDisposable += vitalsViewModel.vitalsState.subscribe(this::presentVitalsState)
}
/**
* Creates and wraps a scaffold navigator such that we can use it to operate with both
* our split pane and legacy activities.
*/
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
private fun rememberNavigator(
windowSizeClass: WindowSizeClass,
contentLayoutData: MainContentLayoutData,
maxWidth: Dp
): ThreePaneScaffoldNavigator<Any> {
val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator<Any>(
scaffoldDirective = calculatePaneScaffoldDirective(
currentWindowAdaptiveInfo()
).copy(
maxHorizontalPartitions = if (windowSizeClass.isSplitPane()) 2 else 1,
horizontalPartitionSpacerSize = contentLayoutData.partitionWidth,
defaultPanePreferredWidth = contentLayoutData.rememberDefaultPanePreferredWidth(maxWidth)
)
)
val coroutine = rememberCoroutineScope()
return remember(scaffoldNavigator, coroutine) {
mainNavigationViewModel.wrapNavigator(coroutine, scaffoldNavigator) { detailLocation ->
when (detailLocation) {
is MainNavigationDetailLocation.Conversation -> {
startActivity(detailLocation.intent)
}
MainNavigationDetailLocation.Empty -> Unit
}
}
}
}
@Composable
private fun MainContainer(content: @Composable BoxWithConstraintsScope.() -> Unit) {
val windowSizeClass = WindowSizeClass.rememberWindowSizeClass()
SignalTheme(isDarkMode = DynamicTheme.isDarkTheme(this)) {
val backgroundColor = if (windowSizeClass.isCompact()) {
MaterialTheme.colorScheme.surface
} else {
SignalTheme.colors.colorSurface1
}
val modifier = if (windowSizeClass.isSplitPane()) {
Modifier.systemBarsPadding().displayCutoutPadding()
} else {
Modifier
}
BoxWithConstraints(
modifier = Modifier
.background(color = backgroundColor)
.then(modifier)
) {
content()
}
}
}
override fun getIntent(): Intent {
return super.getIntent().setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
@@ -205,14 +502,15 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
handleDeepLinkIntent(intent)
val extras = intent.extras ?: return
val startingTab = extras.getSerializableCompat(KEY_STARTING_TAB, MainNavigationDestination::class.java)
val startingTab = extras.getSerializableCompat(KEY_STARTING_TAB, MainNavigationListLocation::class.java)
when (startingTab) {
MainNavigationDestination.CHATS -> conversationListTabsViewModel.onChatsSelected()
MainNavigationDestination.CALLS -> conversationListTabsViewModel.onCallsSelected()
MainNavigationDestination.STORIES -> {
MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected()
MainNavigationListLocation.ARCHIVE -> mainNavigationViewModel.onArchiveSelected()
MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected()
MainNavigationListLocation.STORIES -> {
if (Stories.isFeatureEnabled()) {
conversationListTabsViewModel.onStoriesSelected()
mainNavigationViewModel.onStoriesSelected()
}
}
@@ -229,6 +527,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
super.onResume()
dynamicTheme.onResume(this)
toolbarViewModel.refresh()
if (SignalStore.misc.shouldShowLinkedDevicesReminder) {
SignalStore.misc.shouldShowLinkedDevicesReminder = false
RelinkDevicesReminderBottomSheetFragment.show(supportFragmentManager)
@@ -250,9 +550,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
.show()
}
updateNavigationBarColor()
vitalsViewModel.checkSlowNotificationHeuristics()
mainNavigationViewModel.refreshNavigationBarState()
}
override fun onStop() {
@@ -260,10 +559,8 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
SplashScreenUtil.setSplashScreenThemeIfNecessary(this, SignalStore.settings.theme)
}
override fun onBackPressed() {
if (!navigator.onBackPressed()) {
super.onBackPressed()
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray, deviceId: Int) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@@ -271,6 +568,20 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
if (requestCode == MainNavigator.REQUEST_CONFIG_CHANGES && resultCode == RESULT_CONFIG_CHANGED) {
recreate()
}
if (resultCode == RESULT_OK && requestCode == CreateSvrPinActivity.REQUEST_NEW_PIN) {
mainNavigationViewModel.setSnackbar(SnackbarState(message = getString(R.string.ConfirmKbsPinFragment__pin_created)))
mainNavigationViewModel.onMegaphoneCompleted(Megaphones.Event.PINS_FOR_ALL)
}
if (resultCode == RESULT_OK && requestCode == UsernameEditFragment.REQUEST_CODE) {
val snackbarString = getString(R.string.ConversationListFragment_username_recovered_toast, SignalStore.account.username)
mainNavigationViewModel.setSnackbar(
SnackbarState(
message = snackbarString
)
)
}
}
override fun onFirstRender() {
@@ -281,7 +592,58 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
return navigator
}
override fun bindScrollHelper(recyclerView: RecyclerView, lifecycleOwner: LifecycleOwner) {
Material3OnScrollHelper(
activity = this,
views = listOf(),
viewStubs = listOf(),
onSetToolbarColor = {
toolbarViewModel.setToolbarColor(it)
},
setStatusBarColor = {},
lifecycleOwner = lifecycleOwner
).attach(recyclerView)
}
override fun bindScrollHelper(recyclerView: RecyclerView, lifecycleOwner: LifecycleOwner, chatFolders: RecyclerView, setChatFolder: (Int) -> Unit) {
Material3OnScrollHelper(
activity = this,
views = listOf(chatFolders),
viewStubs = listOf(),
setStatusBarColor = {},
onSetToolbarColor = {
toolbarViewModel.setToolbarColor(it)
},
lifecycleOwner = lifecycleOwner,
setChatFolderColor = setChatFolder
).attach(recyclerView)
}
override fun updateProxyStatus(state: WebSocketConnectionState) {
if (SignalStore.proxy.isProxyEnabled) {
val proxyState: MainToolbarState.ProxyState = when (state) {
WebSocketConnectionState.CONNECTING, WebSocketConnectionState.DISCONNECTING, WebSocketConnectionState.DISCONNECTED -> MainToolbarState.ProxyState.CONNECTING
WebSocketConnectionState.CONNECTED -> MainToolbarState.ProxyState.CONNECTED
WebSocketConnectionState.AUTHENTICATION_FAILED, WebSocketConnectionState.FAILED, WebSocketConnectionState.REMOTE_DEPRECATED -> MainToolbarState.ProxyState.FAILED
else -> MainToolbarState.ProxyState.NONE
}
toolbarViewModel.setProxyState(proxyState = proxyState)
} else {
toolbarViewModel.setProxyState(proxyState = MainToolbarState.ProxyState.NONE)
}
}
override fun onMultiSelectStarted() {
toolbarViewModel.presentToolbarForMultiselect()
}
override fun onMultiSelectFinished() {
toolbarViewModel.presentToolbarForCurrentDestination()
}
private fun handleDeepLinkIntent(intent: Intent) {
handleConversationIntent(intent)
handleGroupLinkInIntent(intent)
handleProxyInIntent(intent)
handleSignalMeIntent(intent)
@@ -289,10 +651,6 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
handleDonateReturnIntent(intent)
}
private fun updateNavigationBarColor() {
WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_colorSurface2))
}
@SuppressLint("NewApi")
private fun presentVitalsState(state: VitalsViewModel.State) {
when (state) {
@@ -306,6 +664,12 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
}
private fun handleConversationIntent(intent: Intent) {
if (ConversationIntents.isConversationIntent(intent)) {
mainNavigationViewModel.goTo(MainNavigationDetailLocation.Conversation(intent))
}
}
private fun handleGroupLinkInIntent(intent: Intent) {
intent.data?.let { data ->
CommunicationActions.handlePotentialGroupLinkUrl(this, data.toString())
@@ -340,6 +704,44 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
}
private fun updateNotificationProfileStatus(notificationProfiles: List<NotificationProfile>) {
val activeProfile = NotificationProfiles.getActiveProfile(notificationProfiles)
if (activeProfile != null) {
if (activeProfile.id != SignalStore.notificationProfile.lastProfilePopup) {
val view = findViewById<ViewGroup>(android.R.id.content)
view.postDelayed({
try {
var fragmentView = view ?: return@postDelayed
SignalStore.notificationProfile.lastProfilePopup = activeProfile.id
SignalStore.notificationProfile.lastProfilePopupTime = System.currentTimeMillis()
if (previousTopToastPopup?.isShowing == true) {
previousTopToastPopup?.dismiss()
}
val fragment = supportFragmentManager.findFragmentByTag(BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
if (fragment != null && fragment.isAdded && fragment.view != null) {
fragmentView = fragment.requireView() as ViewGroup
}
previousTopToastPopup = TopToastPopup.show(fragmentView, R.drawable.ic_moon_16, getString(R.string.ConversationListFragment__s_on, activeProfile.name))
} catch (e: Exception) {
Log.w(TAG, "Unable to show toast popup", e)
}
}, 500L)
}
toolbarViewModel.setNotificationProfileEnabled(true)
} else {
toolbarViewModel.setNotificationProfileEnabled(false)
}
if (!SignalStore.notificationProfile.hasSeenTooltip && Util.hasItems(notificationProfiles)) {
toolbarViewModel.setShowNotificationProfilesTooltip(true)
}
}
inner class ToolbarCallback : MainToolbarCallback {
override fun onNewGroupClick() {
@@ -357,8 +759,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
override fun onInviteFriendsClick() {
val intent = Intent(this@MainActivity, InviteActivity::class.java)
startActivity(intent)
openSettings.launch(AppSettingsActivity.invite(this@MainActivity))
}
override fun onFilterUnreadChatsClick() {
@@ -418,4 +819,97 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
toolbarViewModel.setShowNotificationProfilesTooltip(false)
}
}
inner class BottomChromeCallback : MainBottomChromeCallback {
override fun onNewChatClick() {
startActivity(Intent(this@MainActivity, NewConversationActivity::class.java))
}
override fun onNewCallClick() {
startActivity(NewCallActivity.createIntent(this@MainActivity))
}
override fun onCameraClick(destination: MainNavigationListLocation) {
val onGranted = {
startActivity(
MediaSelectionActivity.camera(
context = this@MainActivity,
isStory = destination == MainNavigationListLocation.STORIES
)
)
}
if (CameraXUtil.isSupported()) {
onGranted()
} else {
Permissions.with(this@MainActivity)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24)
.withPermanentDenialDialog(
getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos),
null,
R.string.CameraXFragment_allow_access_camera,
R.string.CameraXFragment_to_capture_photos_videos,
supportFragmentManager
)
.onAllGranted(onGranted)
.onAnyDenied { Toast.makeText(this@MainActivity, R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() }
.execute()
}
}
override fun onMegaphoneVisible(megaphone: Megaphone) {
mainNavigationViewModel.onMegaphoneVisible(megaphone)
}
override fun onSnackbarDismissed() {
mainNavigationViewModel.setSnackbar(null)
}
}
inner class MainMegaphoneActionController : MegaphoneActionController {
override fun onMegaphoneNavigationRequested(intent: Intent) {
startActivity(intent)
}
override fun onMegaphoneNavigationRequested(intent: Intent, requestCode: Int) {
startActivityForResult(intent, requestCode)
}
override fun onMegaphoneToastRequested(string: String) {
mainNavigationViewModel.setSnackbar(
SnackbarState(
message = string
)
)
}
override fun getMegaphoneActivity(): Activity {
return this@MainActivity
}
override fun onMegaphoneSnooze(event: Megaphones.Event) {
mainNavigationViewModel.onMegaphoneSnoozed(event)
}
override fun onMegaphoneCompleted(event: Megaphones.Event) {
mainNavigationViewModel.onMegaphoneCompleted(event)
}
override fun onMegaphoneDialogFragmentRequested(dialogFragment: DialogFragment) {
dialogFragment.show(supportFragmentManager, "megaphone_dialog")
}
}
private inner class MainNavigationCallback : (MainNavigationListLocation) -> Unit {
override fun invoke(location: MainNavigationListLocation) {
when (location) {
MainNavigationListLocation.CHATS -> mainNavigationViewModel.onChatsSelected()
MainNavigationListLocation.CALLS -> mainNavigationViewModel.onCallsSelected()
MainNavigationListLocation.STORIES -> mainNavigationViewModel.onStoriesSelected()
MainNavigationListLocation.ARCHIVE -> mainNavigationViewModel.onArchiveSelected()
}
}
}
}

View File

@@ -3,13 +3,9 @@ package org.thoughtcrime.securesms;
import android.app.Activity;
import android.content.Intent;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.viewmodel.internal.ViewModelProviders;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
@@ -27,25 +23,16 @@ public class MainNavigator {
private final AppCompatActivity activity;
private final LifecycleDisposable lifecycleDisposable;
private final MainNavigationViewModel viewModel;
private MainNavigationViewModel viewModel;
public MainNavigator(@NonNull AppCompatActivity activity) {
public MainNavigator(@NonNull AppCompatActivity activity, @NonNull MainNavigationViewModel viewModel) {
this.activity = activity;
this.lifecycleDisposable = new LifecycleDisposable();
this.viewModel = viewModel;
lifecycleDisposable.bindTo(activity);
}
@MainThread
public @NonNull MainNavigationViewModel getViewModel() {
if (viewModel == null) {
viewModel = new ViewModelProvider(activity).get(MainNavigationViewModel.class);
}
return viewModel;
}
public static MainNavigator get(@NonNull Activity activity) {
if (!(activity instanceof MainActivity)) {
throw new IllegalArgumentException("Activity must be an instance of MainActivity!");
@@ -54,20 +41,6 @@ public class MainNavigator {
return ((NavigatorProvider) activity).getNavigator();
}
/**
* @return True if the back pressed was handled in our own custom way, false if it should be given
* to the system to do the default behavior.
*/
public boolean onBackPressed() {
Fragment fragment = getFragmentManager().findFragmentById(R.id.fragment_container);
if (fragment instanceof BackHandler) {
return ((BackHandler) fragment).onBackPressed();
}
return false;
}
public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, int startingPosition) {
Disposable disposable = ConversationIntents.createBuilder(activity, recipientId, threadId)
.map(builder -> builder.withDistributionType(distributionType)
@@ -86,11 +59,6 @@ public class MainNavigator {
activity.startActivity(CreateGroupActivity.newIntent(activity));
}
public void goToInvite() {
Intent intent = new Intent(activity, InviteActivity.class);
activity.startActivity(intent);
}
private @NonNull FragmentManager getFragmentManager() {
return activity.getSupportFragmentManager();
}

View File

@@ -42,6 +42,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.components.menu.SignalContextMenu;
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity;
import org.thoughtcrime.securesms.contacts.management.ContactsManagementRepository;
import org.thoughtcrime.securesms.contacts.management.ContactsManagementViewModel;
import org.thoughtcrime.securesms.contacts.paged.ChatType;
@@ -215,7 +216,7 @@ public class NewConversationActivity extends ContactSelectionActivity
}
private void handleInvite() {
startActivity(new Intent(this, InviteActivity.class));
startActivity(AppSettingsActivity.invite(this));
}
@Override
@@ -361,6 +362,8 @@ public class NewConversationActivity extends ContactSelectionActivity
handleManualRefresh();
displaySnackbar(R.string.NewConversationActivity__s_has_been_blocked, recipient.getDisplayName(this));
contactsFragment.reset();
}, (throwable) -> {
displaySnackbar(R.string.NewConversationActivity__block_failed);
}));
})
);

View File

@@ -195,7 +195,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private boolean userMustCreateSignalPin() {
return !SignalStore.registration().isRegistrationComplete() &&
!SignalStore.svr().hasOptedInWithAccess() &&
!SignalStore.svr().hasPin() &&
!SignalStore.svr().lastPinCreateFailed() &&
!SignalStore.svr().hasOptedOut();
}

View File

@@ -8,4 +8,5 @@ interface AudioRecordingHandler {
fun onRecordSaved()
fun onRecordMoved(offsetX: Float, absoluteX: Float)
fun onRecordPermissionRequired()
fun onRecorderAlreadyInUse()
}

View File

@@ -9,18 +9,30 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.shape.CircleShape
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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.viewinterop.AndroidView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.rx3.asFlow
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.NameUtil
@Composable
fun AvatarImage(
recipient: Recipient,
modifier: Modifier = Modifier,
useProfile: Boolean = true
useProfile: Boolean = true,
contentDescription: String? = null
) {
if (LocalInspectionMode.current) {
Spacer(
@@ -28,15 +40,38 @@ fun AvatarImage(
.background(color = Color.Red, shape = CircleShape)
)
} else {
val context = LocalContext.current
var state: AvatarImageState by remember {
mutableStateOf(AvatarImageState(null, recipient, ProfileAvatarFileDetails.NO_DETAILS))
}
LaunchedEffect(recipient.id) {
Recipient.observable(recipient.id).asFlow()
.collectLatest {
state = AvatarImageState(NameUtil.getAbbreviation(it.getDisplayName(context)), it, AvatarHelper.getAvatarFileDetails(context, it.id))
}
}
AndroidView(
factory = ::AvatarImageView,
factory = {
AvatarImageView(context).apply {
initialize(context, null)
this.contentDescription = contentDescription
}
},
modifier = modifier.background(color = Color.Transparent, shape = CircleShape)
) {
if (useProfile) {
it.setAvatarUsingProfile(recipient)
it.setAvatarUsingProfile(state.self)
} else {
it.setAvatar(recipient)
it.setAvatar(state.self)
}
}
}
}
private data class AvatarImageState(
val displayName: String?,
val self: Recipient,
val avatarFileDetails: ProfileAvatarFileDetails
)

View File

@@ -183,6 +183,14 @@ object ImportSkips {
return log(sentTimestamp, "Failed to find a threadId for the provided chatId. ChatId in backup: $chatId")
}
fun chatFolderIdNotFound(): String {
return log(0, "Failed to parse chatFolderId for the provided chat folder.")
}
fun notificationProfileIdNotFound(): String {
return log(0, "Failed to parse notificationProfileId for the provided notification profile.")
}
private fun log(sentTimestamp: Long, message: String): String {
return "[SKIP][$sentTimestamp] $message"
}

View File

@@ -55,6 +55,7 @@ import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
@@ -68,8 +69,10 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob
import org.thoughtcrime.securesms.jobs.CheckRestoreMediaLeftJob
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
import org.thoughtcrime.securesms.jobs.RestoreOptimizedMediaJob
import org.thoughtcrime.securesms.keyvalue.BackupValues.ArchiveServiceCredentials
import org.thoughtcrime.securesms.keyvalue.KeyValueStore
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -195,6 +198,12 @@ object BackupRepository {
}
}
@JvmStatic
fun resumeMediaRestore() {
SignalStore.backup.userManuallySkippedMediaRestore = false
RestoreOptimizedMediaJob.enqueue()
}
/**
* Cancels any relevant jobs for media restore
*/
@@ -205,6 +214,10 @@ object BackupRepository {
AppDependencies.jobManager.cancelAllInQueue(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.RESTORE_OFFLOADED))
AppDependencies.jobManager.cancelAllInQueue(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.INITIAL_RESTORE))
AppDependencies.jobManager.cancelAllInQueue(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.MANUAL))
AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.RESTORE_OFFLOADED)))
AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.INITIAL_RESTORE)))
AppDependencies.jobManager.add(CheckRestoreMediaLeftJob(RestoreAttachmentJob.constructQueueString(RestoreAttachmentJob.RestoreOperation.MANUAL)))
}
/**
@@ -350,12 +363,18 @@ object BackupRepository {
fun turnOffAndDisableBackups(): Boolean {
return try {
Log.d(TAG, "Attempting to disable backups.")
if (SignalStore.backup.backupTier == MessageBackupTier.PAID) {
val backupsSubscriber = InAppPaymentsRepository.getSubscriber(InAppPaymentSubscriberRecord.Type.BACKUP)
if (SignalStore.backup.backupTier == MessageBackupTier.PAID && backupsSubscriber != null) {
Log.d(TAG, "User is currently on a paid tier. Canceling.")
RecurringInAppPaymentRepository.cancelActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
Log.d(TAG, "Successfully canceled paid tier.")
}
if (backupsSubscriber == null) {
Log.w(TAG, "No backup subscriber in the database. Proceeding with disabling backups anyway.")
}
Log.d(TAG, "Disabling backups.")
SignalStore.backup.disableBackups()
SignalDatabase.attachments.clearAllArchiveData()

View File

@@ -6,10 +6,13 @@
package org.thoughtcrime.securesms.backup.v2.processor
import androidx.core.content.contentValuesOf
import okio.ByteString.Companion.toByteString
import org.signal.core.util.Base64
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.ImportSkips
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.ChatFolder
import org.thoughtcrime.securesms.backup.v2.proto.Frame
@@ -19,6 +22,8 @@ import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderMembership
import org.thoughtcrime.securesms.database.ChatFolderTables.ChatFolderTable
import org.thoughtcrime.securesms.database.ChatFolderTables.MembershipType
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.whispersystems.signalservice.api.util.UuidUtil
import org.thoughtcrime.securesms.backup.v2.proto.ChatFolder as ChatFolderProto
/**
@@ -31,7 +36,7 @@ object ChatFolderProcessor {
fun export(db: SignalDatabase, exportState: ExportState, emitter: BackupFrameEmitter) {
val folders = db
.chatFoldersTable
.getChatFolders()
.getCurrentChatFolders()
.sortedBy { it.position }
if (folders.isEmpty()) {
@@ -66,6 +71,12 @@ object ChatFolderProcessor {
}
fun import(chatFolder: ChatFolderProto, importState: ImportState) {
val chatFolderUuid = UuidUtil.parseOrNull(chatFolder.id)
if (chatFolderUuid == null) {
ImportSkips.chatFolderIdNotFound()
return
}
val chatFolderId = SignalDatabase
.writableDatabase
.insertInto(ChatFolderTable.TABLE_NAME)
@@ -76,7 +87,9 @@ object ChatFolderProcessor {
ChatFolderTable.SHOW_MUTED to chatFolder.showMutedChats,
ChatFolderTable.SHOW_INDIVIDUAL to chatFolder.includeAllIndividualChats,
ChatFolderTable.SHOW_GROUPS to chatFolder.includeAllGroupChats,
ChatFolderTable.FOLDER_TYPE to chatFolder.folderType.toLocal().value
ChatFolderTable.FOLDER_TYPE to chatFolder.folderType.toLocal().value,
ChatFolderTable.CHAT_FOLDER_ID to chatFolderUuid.toString(),
ChatFolderTable.STORAGE_SERVICE_ID to Base64.encodeWithPadding(StorageSyncHelper.generateKey())
)
.run()
@@ -110,7 +123,8 @@ private fun ChatFolderRecord.toBackupFrame(includedRecipientIds: List<Long>, exc
else -> throw IllegalStateException("Only ALL or CUSTOM should be in the db")
},
includedRecipientIds = includedRecipientIds,
excludedRecipientIds = excludedRecipientIds
excludedRecipientIds = excludedRecipientIds,
id = UuidUtil.toByteArray(this.chatFolderId.uuid).toByteString()
)
return Frame(chatFolder = chatFolder)

View File

@@ -5,10 +5,12 @@
package org.thoughtcrime.securesms.backup.v2.processor
import okio.ByteString.Companion.toByteString
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.ImportSkips
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter
@@ -20,7 +22,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.serialize
import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile
import org.thoughtcrime.securesms.recipients.RecipientId
import java.lang.IllegalStateException
import org.whispersystems.signalservice.api.util.UuidUtil
import java.time.DayOfWeek
import org.thoughtcrime.securesms.backup.v2.proto.NotificationProfile as NotificationProfileProto
@@ -41,6 +43,12 @@ object NotificationProfileProcessor {
}
fun import(profile: NotificationProfileProto, importState: ImportState) {
val notificationProfileUuid = UuidUtil.parseOrNull(profile.id)
if (notificationProfileUuid == null) {
ImportSkips.notificationProfileIdNotFound()
return
}
val profileId = SignalDatabase
.writableDatabase
.insertInto(NotificationProfileTable.TABLE_NAME)
@@ -50,7 +58,8 @@ object NotificationProfileProcessor {
NotificationProfileTable.COLOR to (AvatarColor.fromColor(profile.color) ?: AvatarColor.random()).serialize(),
NotificationProfileTable.CREATED_AT to profile.createdAtMs,
NotificationProfileTable.ALLOW_ALL_CALLS to profile.allowAllCalls.toInt(),
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt()
NotificationProfileTable.ALLOW_ALL_MENTIONS to profile.allowAllMentions.toInt(),
NotificationProfileTable.NOTIFICATION_PROFILE_ID to notificationProfileUuid.toString()
)
.run()
@@ -89,6 +98,7 @@ object NotificationProfileProcessor {
private fun NotificationProfile.toBackupFrame(includeRecipient: (RecipientId) -> Boolean): Frame {
val profile = NotificationProfileProto(
id = UuidUtil.toByteArray(this.notificationProfileId.uuid).toByteString(),
name = this.name,
emoji = this.emoji.takeIf { it.isNotBlank() },
color = this.color.colorInt(),

View File

@@ -28,16 +28,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
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.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
@@ -47,6 +42,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.parcelize.Parcelize
import org.signal.core.ui.compose.BottomSheets
@@ -56,25 +52,18 @@ import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription
import org.thoughtcrime.securesms.billing.upgrade.UpgradeToPaidTierBottomSheet
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.PlayStoreUtil
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import org.signal.core.ui.R as CoreUiR
/**
* Notifies the user of an issue with their backup.
*/
class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 0.75f
@@ -82,8 +71,12 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
private const val ARG_ALERT = "alert"
@JvmStatic
fun create(backupAlert: BackupAlert): BackupAlertBottomSheet {
return BackupAlertBottomSheet().apply {
fun create(backupAlert: BackupAlert): DialogFragment {
return if (backupAlert is BackupAlert.MediaBackupsAreOff) {
MediaBackupsAreOffBottomSheet()
} else {
BackupAlertBottomSheet()
}.apply {
arguments = bundleOf(ARG_ALERT to backupAlert)
}
}
@@ -94,34 +87,20 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
}
@Composable
override fun UpgradeSheetContent(
paidBackupType: MessageBackupsType.Paid,
freeBackupType: MessageBackupsType.Free,
isSubscribeEnabled: Boolean,
onSubscribeClick: () -> Unit
) {
var pricePerMonth by remember { mutableStateOf("-") }
val resources = LocalContext.current.resources
LaunchedEffect(paidBackupType.pricePerMonth) {
pricePerMonth = FiatMoneyUtil.format(resources, paidBackupType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
val performPrimaryAction = remember(onSubscribeClick) {
createPrimaryAction(onSubscribeClick)
override fun SheetContent() {
val performPrimaryAction = remember(backupAlert) {
createPrimaryAction()
}
BackupAlertSheetContent(
backupAlert = backupAlert,
isSubscribeEnabled = isSubscribeEnabled,
mediaTtl = paidBackupType.mediaTtl,
onPrimaryActionClick = performPrimaryAction,
onSecondaryActionClick = this::performSecondaryAction
)
}
@Stable
private fun createPrimaryAction(onSubscribeClick: () -> Unit): () -> Unit = {
private fun createPrimaryAction(): () -> Unit = {
when (backupAlert) {
is BackupAlert.CouldNotCompleteBackup -> {
BackupMessagesJob.enqueue()
@@ -129,9 +108,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
}
BackupAlert.FailedToRenew -> launchManageBackupsSubscription()
is BackupAlert.MediaBackupsAreOff -> {
onSubscribeClick()
}
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
BackupAlert.MediaWillBeDeletedToday -> {
performFullMediaDownload()
@@ -152,7 +129,7 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
when (backupAlert) {
is BackupAlert.CouldNotCompleteBackup -> Unit
BackupAlert.FailedToRenew -> Unit
is BackupAlert.MediaBackupsAreOff -> Unit
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
BackupAlert.MediaWillBeDeletedToday -> {
displayLastChanceDialog()
}
@@ -206,17 +183,13 @@ class BackupAlertBottomSheet : UpgradeToPaidTierBottomSheet() {
}
private fun performFullMediaDownload() {
// TODO [backups] -- We need to force this to download everything
AppDependencies.jobManager.add(BackupRestoreMediaJob())
BackupRepository.resumeMediaRestore()
}
}
@Composable
private fun BackupAlertSheetContent(
fun BackupAlertSheetContent(
backupAlert: BackupAlert,
pricePerMonth: String = "",
isSubscribeEnabled: Boolean = true,
mediaTtl: Duration,
onPrimaryActionClick: () -> Unit = {},
onSecondaryActionClick: () -> Unit = {}
) {
@@ -231,7 +204,8 @@ private fun BackupAlertSheetContent(
Spacer(modifier = Modifier.size(26.dp))
when (backupAlert) {
BackupAlert.FailedToRenew, is BackupAlert.MediaBackupsAreOff -> {
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
BackupAlert.FailedToRenew -> {
Box {
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.image_signal_backups),
@@ -276,29 +250,27 @@ private fun BackupAlertSheetContent(
)
BackupAlert.FailedToRenew -> PaymentProcessingBody()
is BackupAlert.MediaBackupsAreOff -> MediaBackupsAreOffBody(backupAlert.endOfPeriodSeconds, mediaTtl)
BackupAlert.MediaWillBeDeletedToday -> MediaWillBeDeletedTodayBody()
is BackupAlert.DiskFull -> DiskFullBody(requiredSpace = backupAlert.requiredSpace)
BackupAlert.BackupFailed -> BackupFailedBody()
BackupAlert.CouldNotRedeemBackup -> CouldNotRedeemBackup()
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
}
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert)
val padBottom = if (secondaryActionResource > 0) 16.dp else 56.dp
Buttons.LargeTonal(
enabled = isSubscribeEnabled,
onClick = onPrimaryActionClick,
modifier = Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(bottom = padBottom)
) {
Text(text = primaryActionString(backupAlert = backupAlert, pricePerMonth = pricePerMonth))
Text(text = primaryActionString(backupAlert = backupAlert))
}
if (secondaryActionResource > 0) {
TextButton(
enabled = isSubscribeEnabled,
onClick = onSecondaryActionClick,
modifier = Modifier.padding(bottom = 32.dp)
) {
@@ -381,28 +353,6 @@ private fun PaymentProcessingBody() {
)
}
@Composable
private fun MediaBackupsAreOffBody(
endOfPeriodSeconds: Long,
mediaTtl: Duration
) {
val daysUntilDeletion = remember { endOfPeriodSeconds.days + mediaTtl }.inWholeDays.toInt()
Text(
text = pluralStringResource(id = R.plurals.BackupAlertBottomSheet__your_backup_plan_has_expired, daysUntilDeletion, daysUntilDeletion),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 24.dp)
)
Text(
text = stringResource(id = R.string.BackupAlertBottomSheet__you_can_begin_paying_for_backups_again),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 36.dp)
)
}
@Composable
private fun MediaWillBeDeletedTodayBody() {
Text(
@@ -463,7 +413,7 @@ private fun titleString(backupAlert: BackupAlert): String {
return when (backupAlert) {
is BackupAlert.CouldNotCompleteBackup -> stringResource(R.string.BackupAlertBottomSheet__couldnt_complete_backup)
BackupAlert.FailedToRenew -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_failed_to_renew)
is BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__your_backups_subscription_expired)
is BackupAlert.MediaBackupsAreOff -> error("Use MediaBackupsAreOffBottomSheet instead.")
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__your_media_will_be_deleted_today)
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__free_up_s_on_this_device, backupAlert.requiredSpace)
BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__backup_failed)
@@ -473,13 +423,12 @@ private fun titleString(backupAlert: BackupAlert): String {
@Composable
private fun primaryActionString(
backupAlert: BackupAlert,
pricePerMonth: String
backupAlert: BackupAlert
): String {
return when (backupAlert) {
is BackupAlert.CouldNotCompleteBackup -> stringResource(R.string.BackupAlertBottomSheet__back_up_now)
BackupAlert.FailedToRenew -> stringResource(R.string.BackupAlertBottomSheet__manage_subscription)
is BackupAlert.MediaBackupsAreOff -> stringResource(R.string.BackupAlertBottomSheet__subscribe_for_s_month, pricePerMonth)
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
BackupAlert.MediaWillBeDeletedToday -> stringResource(R.string.BackupAlertBottomSheet__download_media_now)
is BackupAlert.DiskFull -> stringResource(R.string.BackupAlertBottomSheet__got_it)
is BackupAlert.BackupFailed -> stringResource(R.string.BackupAlertBottomSheet__check_for_update)
@@ -493,7 +442,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
when (backupAlert) {
is BackupAlert.CouldNotCompleteBackup -> R.string.BackupAlertBottomSheet__try_later
BackupAlert.FailedToRenew -> R.string.BackupAlertBottomSheet__not_now
is BackupAlert.MediaBackupsAreOff -> R.string.BackupAlertBottomSheet__not_now
is BackupAlert.MediaBackupsAreOff -> error("Not supported.")
BackupAlert.MediaWillBeDeletedToday -> R.string.BackupAlertBottomSheet__dont_download_media
is BackupAlert.DiskFull -> R.string.BackupAlertBottomSheet__skip_restore
is BackupAlert.BackupFailed -> R.string.BackupAlertBottomSheet__learn_more
@@ -507,8 +456,7 @@ private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
private fun BackupAlertSheetContentPreviewGeneric() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = 7),
mediaTtl = 60.days
backupAlert = BackupAlert.CouldNotCompleteBackup(daysSinceLastBackup = 7)
)
}
}
@@ -518,20 +466,7 @@ private fun BackupAlertSheetContentPreviewGeneric() {
private fun BackupAlertSheetContentPreviewPayment() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.FailedToRenew,
mediaTtl = 60.days
)
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewMedia() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.MediaBackupsAreOff(endOfPeriodSeconds = System.currentTimeMillis().milliseconds.inWholeSeconds),
pricePerMonth = "$2.99",
mediaTtl = 60.days
backupAlert = BackupAlert.FailedToRenew
)
}
}
@@ -541,8 +476,7 @@ private fun BackupAlertSheetContentPreviewMedia() {
private fun BackupAlertSheetContentPreviewDelete() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.MediaWillBeDeletedToday,
mediaTtl = 60.days
backupAlert = BackupAlert.MediaWillBeDeletedToday
)
}
}
@@ -552,8 +486,7 @@ private fun BackupAlertSheetContentPreviewDelete() {
private fun BackupAlertSheetContentPreviewDiskFull() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.DiskFull(requiredSpace = "12GB"),
mediaTtl = 60.days
backupAlert = BackupAlert.DiskFull(requiredSpace = "12GB")
)
}
}
@@ -563,8 +496,7 @@ private fun BackupAlertSheetContentPreviewDiskFull() {
private fun BackupAlertSheetContentPreviewBackupFailed() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.BackupFailed,
mediaTtl = 60.days
backupAlert = BackupAlert.BackupFailed
)
}
}
@@ -574,8 +506,7 @@ private fun BackupAlertSheetContentPreviewBackupFailed() {
private fun BackupAlertSheetContentPreviewCouldNotRedeemBackup() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.CouldNotRedeemBackup,
mediaTtl = 60.days
backupAlert = BackupAlert.CouldNotRedeemBackup
)
}
}

View File

@@ -0,0 +1,177 @@
/*
* Copyright 2025 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.os.BundleCompat
import org.signal.core.ui.R
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.util.gibiBytes
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.billing.upgrade.UpgradeToPaidTierBottomSheet
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import java.math.BigDecimal
import java.util.Currency
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
class MediaBackupsAreOffBottomSheet : UpgradeToPaidTierBottomSheet() {
companion object {
private const val ARG_ALERT = "alert"
}
private val backupAlert: BackupAlert by lazy(LazyThreadSafetyMode.NONE) {
BundleCompat.getParcelable(requireArguments(), ARG_ALERT, BackupAlert::class.java)!!
}
@Composable
override fun UpgradeSheetContent(
paidBackupType: MessageBackupsType.Paid,
freeBackupType: MessageBackupsType.Free,
isSubscribeEnabled: Boolean,
onSubscribeClick: () -> Unit
) {
SheetContent(
backupAlert as BackupAlert.MediaBackupsAreOff,
paidBackupType,
isSubscribeEnabled,
onSubscribeClick,
onNotNowClick = { dismissAllowingStateLoss() }
)
}
}
@Composable
private fun SheetContent(
mediaBackupsAreOff: BackupAlert.MediaBackupsAreOff,
paidBackupType: MessageBackupsType.Paid,
isSubscribeEnabled: Boolean,
onSubscribeClick: () -> Unit,
onNotNowClick: () -> Unit
) {
val resources = LocalContext.current.resources
val pricePerMonth = remember(paidBackupType) {
FiatMoneyUtil.format(resources, paidBackupType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(id = R.dimen.gutter))
) {
BottomSheets.Handle()
Spacer(modifier = Modifier.size(26.dp))
Box {
Image(
imageVector = ImageVector.vectorResource(id = org.thoughtcrime.securesms.R.drawable.image_signal_backups),
contentDescription = null,
modifier = Modifier
.size(80.dp)
.padding(2.dp)
)
Icon(
imageVector = ImageVector.vectorResource(org.thoughtcrime.securesms.R.drawable.symbol_error_circle_fill_24),
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
modifier = Modifier.align(Alignment.TopEnd)
)
}
val daysUntilDeletion = remember(mediaBackupsAreOff.endOfPeriodSeconds, paidBackupType.mediaTtl) {
((System.currentTimeMillis().milliseconds - mediaBackupsAreOff.endOfPeriodSeconds.seconds) + paidBackupType.mediaTtl).inWholeDays.toInt()
}
Text(
text = stringResource(org.thoughtcrime.securesms.R.string.BackupAlertBottomSheet__your_backups_subscription_expired),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp, bottom = 12.dp)
)
Text(
text = pluralStringResource(id = org.thoughtcrime.securesms.R.plurals.BackupAlertBottomSheet__your_backup_plan_has_expired, daysUntilDeletion, daysUntilDeletion),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 24.dp)
)
Text(
text = stringResource(id = org.thoughtcrime.securesms.R.string.BackupAlertBottomSheet__you_can_begin_paying_for_backups_again),
textAlign = TextAlign.Center,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(bottom = 36.dp)
)
Buttons.LargeTonal(
enabled = isSubscribeEnabled,
onClick = onSubscribeClick,
modifier = Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(bottom = 16.dp)
) {
Text(text = stringResource(org.thoughtcrime.securesms.R.string.BackupAlertBottomSheet__subscribe_for_s_month, pricePerMonth))
}
TextButton(
enabled = isSubscribeEnabled,
onClick = onNotNowClick,
modifier = Modifier.padding(bottom = 32.dp)
) {
Text(text = stringResource(id = org.thoughtcrime.securesms.R.string.BackupAlertBottomSheet__not_now))
}
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewMedia() {
Previews.BottomSheetPreview {
SheetContent(
mediaBackupsAreOff = BackupAlert.MediaBackupsAreOff(endOfPeriodSeconds = System.currentTimeMillis().milliseconds.inWholeSeconds),
paidBackupType = MessageBackupsType.Paid(
pricePerMonth = FiatMoney(BigDecimal.ONE, Currency.getInstance("USD")),
mediaTtl = 30.days,
storageAllowanceBytes = 1.gibiBytes.inWholeBytes
),
isSubscribeEnabled = true,
onSubscribeClick = {},
onNotNowClick = {}
)
}
}

View File

@@ -283,8 +283,18 @@ sealed interface BackupStatusData {
val restoreStatus: RestoreStatus = RestoreStatus.NORMAL
) : BackupStatusData {
override val iconRes: Int = if (restoreStatus == RestoreStatus.FINISHED) R.drawable.symbol_check_circle_24 else R.drawable.symbol_backup_light
override val iconColors: BackupsIconColors = if (restoreStatus == RestoreStatus.FINISHED) BackupsIconColors.Success else BackupsIconColors.Normal
override val iconColors: BackupsIconColors = when (restoreStatus) {
RestoreStatus.FINISHED -> BackupsIconColors.Success
RestoreStatus.NORMAL -> BackupsIconColors.Normal
RestoreStatus.LOW_BATTERY,
RestoreStatus.WAITING_FOR_INTERNET,
RestoreStatus.WAITING_FOR_WIFI -> BackupsIconColors.Warning
}
override val showDismissAction: Boolean = restoreStatus == RestoreStatus.FINISHED
override val actionRes: Int = when (restoreStatus) {
RestoreStatus.WAITING_FOR_WIFI -> R.string.BackupStatus__resume
else -> NONE
}
override val title: String
@Composable get() = stringResource(
@@ -311,7 +321,7 @@ sealed interface BackupStatusData {
RestoreStatus.FINISHED -> bytesTotal.toUnitString()
}
override val progress: Float = if (bytesTotal.bytes > 0 && restoreStatus != RestoreStatus.FINISHED) {
override val progress: Float = if (bytesTotal.bytes > 0 && restoreStatus == RestoreStatus.NORMAL) {
min(1f, max(0f, bytesDownloaded.bytes.toFloat() / bytesTotal.bytes.toFloat()))
} else {
NONE.toFloat()

View File

@@ -184,9 +184,10 @@ private fun getRestoringMediaString(backupStatusData: BackupStatusData.Restoring
@Composable
private fun progressColor(backupStatusData: BackupStatusData): Color {
return when (backupStatusData) {
is BackupStatusData.RestoringMedia -> MaterialTheme.colorScheme.primary
else -> backupStatusData.iconColors.foreground
return if (backupStatusData is BackupStatusData.RestoringMedia && backupStatusData.restoreStatus == BackupStatusData.RestoreStatus.NORMAL) {
MaterialTheme.colorScheme.primary
} else {
backupStatusData.iconColors.foreground
}
}

View File

@@ -32,6 +32,8 @@ import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.Scaffolds
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.compose.BetaHeader
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
import org.signal.core.ui.R as CoreUiR
/**
@@ -62,6 +64,10 @@ fun MessageBackupsEducationScreen(
.fillMaxWidth()
.weight(1f)
) {
item {
BetaHeader()
}
item {
Image(
painter = painterResource(id = R.drawable.image_signal_backups),
@@ -73,9 +79,9 @@ fun MessageBackupsEducationScreen(
}
item {
Text(
TextWithBetaLabel(
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__signal_backups),
style = MaterialTheme.typography.headlineMedium,
textStyle = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(top = 15.dp)
)
}

View File

@@ -43,6 +43,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
@VisibleForTesting
const val TIER = "tier"
const val CLIPBOARD_TIMEOUT_SECONDS = 60
fun create(messageBackupTier: MessageBackupTier?): MessageBackupsFlowFragment {
return MessageBackupsFlowFragment().apply {
@@ -115,7 +116,7 @@ class MessageBackupsFlowFragment : ComposeFragment(), InAppPaymentCheckoutDelega
onNavigationClick = viewModel::goToPreviousStage,
onNextClick = viewModel::goToNextStage,
onCopyToClipboardClick = {
Util.copyToClipboard(context, it)
Util.copyToClipboard(context, it, CLIPBOARD_TIMEOUT_SECONDS)
}
)
}

View File

@@ -24,7 +24,6 @@ import org.signal.core.util.billing.BillingPurchaseResult
import org.signal.core.util.concurrent.SignalDispatchers
import org.signal.core.util.logging.Log
import org.signal.donations.InAppPaymentType
import org.signal.donations.PaymentSourceType
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.components.settings.app.subscription.DonationSerializationHelper.toFiatValue
@@ -85,10 +84,15 @@ class MessageBackupsFlowViewModel(
}
viewModelScope.launch {
val availableBackupTypes = withContext(SignalDispatchers.IO) {
BackupRepository.getAvailableBackupsTypes(
if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
)
val availableBackupTypes = try {
withContext(SignalDispatchers.IO) {
BackupRepository.getAvailableBackupsTypes(
if (!RemoteConfig.messageBackups) emptyList() else listOf(MessageBackupTier.FREE, MessageBackupTier.PAID)
)
}
} catch (e: Exception) {
Log.w(TAG, "Failed to download available backup types.", e)
emptyList()
}
internalStateFlow.update {
@@ -133,12 +137,12 @@ class MessageBackupsFlowViewModel(
}
} catch (e: Exception) {
Log.d(TAG, "Failed to handle purchase.", e)
InAppPaymentsRepository.handlePipelineError(
inAppPaymentId = id,
donationErrorSource = DonationErrorSource.BACKUPS,
paymentSourceType = PaymentSourceType.GooglePlayBilling,
error = e
)
withContext(SignalDispatchers.IO) {
InAppPaymentsRepository.handlePipelineError(
inAppPaymentId = id,
error = e
)
}
internalStateFlow.update {
it.copy(

View File

@@ -39,7 +39,7 @@ fun MessageBackupsKeyEducationScreen(
) {
Scaffolds.Settings(
title = "",
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
onNavigationClick = onNavigationClick
) {
Column(

View File

@@ -55,7 +55,7 @@ fun MessageBackupsKeyRecordScreen(
Scaffolds.Settings(
title = "",
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
onNavigationClick = onNavigationClick
) { paddingValues ->
Column(

View File

@@ -87,7 +87,7 @@ fun MessageBackupsKeyVerifyScreen(
Scaffolds.Settings(
title = stringResource(R.string.MessageBackupsKeyVerifyScreen__confirm_your_backup_key),
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
onNavigationClick = onNavigationClick
) { paddingValues ->

View File

@@ -55,7 +55,7 @@ import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.fonts.SignalSymbols
import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol
import org.thoughtcrime.securesms.fonts.SignalSymbols.signalSymbolText
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.ByteUnit
import java.math.BigDecimal
@@ -82,7 +82,7 @@ fun MessageBackupsTypeSelectionScreen(
Scaffolds.Settings(
title = "",
onNavigationClick = onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24)
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_start_24)
) { paddingValues ->
Column(
modifier = Modifier
@@ -260,11 +260,10 @@ fun MessageBackupsTypeBlock(
) {
if (isCurrent) {
Text(
text = buildAnnotatedString {
SignalSymbol(weight = SignalSymbols.Weight.REGULAR, glyph = SignalSymbols.Glyph.CHECKMARK)
append(" ")
append(stringResource(R.string.MessageBackupsTypeSelectionScreen__current_plan))
},
text = signalSymbolText(
text = stringResource(R.string.MessageBackupsTypeSelectionScreen__current_plan),
glyphStart = SignalSymbols.Glyph.CHECK
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier
.padding(bottom = 12.dp)

View File

@@ -130,7 +130,7 @@ fun ChatStyle.toLocal(importState: ImportState): ChatColors? {
if (this.customColorId != null) {
return importState.remoteToLocalColorId[this.customColorId]?.let { localId ->
val colorId = ChatColors.Id.forLongValue(localId)
ChatColorsPalette.Bubbles.default.withId(colorId)
return SignalDatabase.chatColors.getById(colorId)
}
}

View File

@@ -15,7 +15,6 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.subjects.PublishSubject
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.signal.donations.InAppPaymentType
import org.thoughtcrime.securesms.MainActivity
@@ -58,10 +57,6 @@ class GiftFlowConfirmationFragment :
EmojiSearchFragment.Callback,
InAppPaymentCheckoutDelegate.Callback {
companion object {
private val TAG = Log.tag(GiftFlowConfirmationFragment::class.java)
}
private val viewModel: GiftFlowViewModel by viewModels(
ownerProducer = { requireActivity() }
)
@@ -118,7 +113,7 @@ class GiftFlowConfirmationFragment :
lifecycleDisposable += viewModel.insertInAppPayment().subscribe { inAppPayment ->
findNavController().safeNavigate(
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToGatewaySelectorBottomSheet(
inAppPayment
inAppPayment.id
)
)
}
@@ -266,8 +261,7 @@ class GiftFlowConfirmationFragment :
findNavController().safeNavigate(
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToStripePaymentInProgressFragment(
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
inAppPayment,
inAppPayment.type
inAppPayment.id
)
)
}
@@ -276,15 +270,14 @@ class GiftFlowConfirmationFragment :
findNavController().safeNavigate(
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToPaypalPaymentInProgressFragment(
InAppPaymentProcessorAction.PROCESS_NEW_IN_APP_PAYMENT,
inAppPayment,
inAppPayment.type
inAppPayment.id
)
)
}
override fun navigateToCreditCardForm(inAppPayment: InAppPaymentTable.InAppPayment) {
findNavController().safeNavigate(
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment)
GiftFlowConfirmationFragmentDirections.actionGiftFlowConfirmationFragmentToCreditCardFragment(inAppPayment.id)
)
}

View File

@@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.key
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp
@@ -45,17 +45,18 @@ class BannerManager @JvmOverloads constructor(
return@setContent
}
val bannerState by banner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
key(banner) {
val bannerState by banner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
bannerState?.let { model ->
SignalTheme {
Box {
banner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
bannerState?.let { model ->
SignalTheme {
Box {
banner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
}
}
}
onNewBannerShownListener()
} ?: onNoBannerShownListener()
onNewBannerShownListener()
} ?: onNoBannerShownListener()
}
}
}
}
@@ -65,12 +66,16 @@ class BannerManager @JvmOverloads constructor(
*/
@Composable
fun Banner() {
val banner by rememberUpdatedState(banners.firstOrNull { it.enabled } as Banner<Any>?)
val banner: Banner<Any>? = banners.firstOrNull { it.enabled } as Banner<Any>?
if (banner == null) {
return
}
banner?.let { nonNullBanner ->
val state by nonNullBanner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
state?.let { model ->
nonNullBanner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
key(banner) {
val bannerState by banner.dataFlow.collectAsStateWithLifecycle(initialValue = null)
bannerState?.let { model ->
banner.DisplayBanner(model, PaddingValues(horizontal = 12.dp, vertical = 8.dp))
}
}
}

View File

@@ -58,7 +58,7 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
totalRestoredSize > 0 -> {
flowOf(
BackupStatusData.RestoringMedia(
bytesTotal = totalRestoredSize.bytes.also { totalRestoredSize = 0 },
bytesTotal = totalRestoredSize.bytes,
restoreStatus = BackupStatusData.RestoreStatus.FINISHED
)
)
@@ -75,7 +75,10 @@ class MediaRestoreProgressBanner(private val listener: RestoreProgressBannerList
data = model,
onBannerClick = listener::onBannerClick,
onActionClick = listener::onActionClick,
onDismissClick = listener::onDismissComplete
onDismissClick = {
totalRestoredSize = 0
listener.onDismissComplete()
}
)
}

View File

@@ -21,6 +21,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ContactFilterView;
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode;
import org.thoughtcrime.securesms.contacts.paged.ChatType;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
@@ -38,6 +39,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private BlockedUsersViewModel viewModel;
private View container;
private final LifecycleDisposable lifecycleDisposable = new LifecycleDisposable();
@@ -57,7 +59,7 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
Toolbar toolbar = findViewById(R.id.toolbar);
ContactFilterView contactFilterView = findViewById(R.id.contact_filter_edit_text);
View container = findViewById(R.id.fragment_container);
container = findViewById(R.id.fragment_container);
toolbar.setNavigationOnClickListener(unused -> onBackPressed());
contactFilterView.setOnFilterChangedListener(query -> {
@@ -99,11 +101,41 @@ public class BlockedUsersActivity extends PassphraseRequiredActivity implements
@Override
public void onBeforeContactSelected(boolean isFromUnknownSearchKey, @NonNull Optional<RecipientId> recipientId, String number, @NonNull Optional<ChatType> chatType, @NonNull Consumer<Boolean> callback) {
final String displayName = recipientId.map(id -> Recipient.resolved(id).getDisplayName(this)).orElse(number);
Optional<Recipient> resolvedRecipient = recipientId.map(Recipient::resolved);
AlertDialog confirmationDialog = new MaterialAlertDialogBuilder(this)
.setTitle(R.string.BlockedUsersActivity__block_user)
.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName))
final String displayName = resolvedRecipient
.map(r -> r.getDisplayName(this))
.orElse(number);
boolean isSelf = resolvedRecipient
.map(Recipient::isSelf)
.orElseGet(() -> Optional.ofNullable(number)
.map(Recipient::external)
.map(Recipient::isSelf)
.orElse(false));
if (isSelf) {
Snackbar.make(container, getString(R.string.BlockedUsersActivity__cannot_block_yourself), Snackbar.LENGTH_SHORT).show();
return;
}
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
if (resolvedRecipient.isPresent() && resolvedRecipient.get().isGroup()) {
Recipient recipient = resolvedRecipient.get();
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
builder.setTitle(getString(R.string.BlockUnblockDialog_block_and_leave_s, displayName));
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
} else {
builder.setTitle(getString(R.string.BlockUnblockDialog_block_s, displayName));
builder.setMessage(R.string.BlockUnblockDialog_group_members_wont_be_able_to_add_you);
}
} else {
builder.setTitle(R.string.BlockedUsersActivity__block_user);
builder.setMessage(getString(R.string.BlockedUserActivity__s_will_not_be_able_to, displayName));
}
AlertDialog confirmationDialog = builder
.setPositiveButton(R.string.BlockedUsersActivity__block, (dialog, which) -> {
if (recipientId.isPresent()) {
viewModel.block(recipientId.get());

View File

@@ -34,7 +34,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.fragment.app.setFragmentResult
import androidx.navigation.fragment.navArgs
import org.signal.core.ui.compose.Buttons
import org.signal.core.ui.compose.Scaffolds
import org.signal.core.util.BreakIteratorCompat
@@ -45,11 +44,11 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
companion object {
const val RESULT_KEY = "edit_call_link_name"
private const val MAX_CHARACTER_COUNT = 32
const val ARG_NAME = "name"
}
private val args: EditCallLinkNameDialogFragmentArgs by navArgs()
private val argName: String
get() = requireArguments().getString(ARG_NAME)!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -69,8 +68,8 @@ class EditCallLinkNameDialogFragment : ComposeDialogFragment() {
var callName by remember {
mutableStateOf(
TextFieldValue(
text = args.name,
selection = TextRange(args.name.length)
text = argName,
selection = TextRange(argName.length)
)
)
}

View File

@@ -18,6 +18,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@@ -31,9 +33,9 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.fragment.findNavController
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.ui.compose.BottomSheets
import org.signal.core.ui.compose.Buttons
@@ -125,9 +127,9 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
private fun onAddACallNameClicked() {
val snapshot = viewModel.callLink.value
findNavController().navigate(
CreateCallLinkBottomSheetDialogFragmentDirections.actionCreateCallLinkBottomSheetToEditCallLinkNameDialogFragment(snapshot.state.name)
)
EditCallLinkNameDialogFragment().apply {
arguments = bundleOf(EditCallLinkNameDialogFragment.ARG_NAME to snapshot.state.name)
}.show(parentFragmentManager, null)
}
private fun onJoinClicked() {
@@ -242,6 +244,7 @@ private fun CreateCallLinkBottomSheetContent(
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
.verticalScroll(rememberScrollState())
) {
BottomSheets.Handle(modifier = Modifier.align(Alignment.CenterHorizontally))

View File

@@ -12,6 +12,8 @@ import android.view.View
import android.widget.Toast
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -287,7 +289,11 @@ private fun CallLinkDetails(
return@Settings
}
Column(modifier = Modifier.padding(paddingValues)) {
Column(
modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
SignalCallRow(
callLink = state.callLink,
callLinkPeekInfo = state.peekInfo,

View File

@@ -9,20 +9,14 @@ import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.compose.material3.SnackbarDuration
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.app.SharedElementCallback
import androidx.core.view.ViewCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.kotlin.Flowables
import io.reactivex.rxjava3.kotlin.subscribeBy
import org.signal.core.util.DimensionUnit
@@ -31,9 +25,8 @@ import org.signal.core.util.concurrent.addTo
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.MainNavigator
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.calls.links.create.CreateCallLinkBottomSheetDialogFragment
import org.thoughtcrime.securesms.calls.links.details.CallLinkDetailsActivity
import org.thoughtcrime.securesms.calls.new.NewCallActivity
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
import org.thoughtcrime.securesms.components.ViewBinderDelegate
@@ -50,19 +43,21 @@ import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterLerp
import org.thoughtcrime.securesms.conversationlist.chatfilter.FilterPullState
import org.thoughtcrime.securesms.databinding.CallLogFragmentBinding
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.main.MainNavigationDestination
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.main.MainNavigationViewModel
import org.thoughtcrime.securesms.main.MainToolbarMode
import org.thoughtcrime.securesms.main.MainToolbarViewModel
import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder
import org.thoughtcrime.securesms.main.SnackbarState
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.tabs.ConversationListTabsViewModel
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.doAfterNextLayout
import org.thoughtcrime.securesms.util.fragments.requireListener
import org.thoughtcrime.securesms.util.visible
import org.thoughtcrime.securesms.window.WindowSizeClass.Companion.getWindowSizeClass
import java.util.Objects
import java.util.concurrent.TimeUnit
/**
* Call Log tab.
@@ -89,12 +84,10 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
private lateinit var signalBottomActionBarController: SignalBottomActionBarController
private val viewModel: CallLogViewModel by activityViewModels()
private val tabsViewModel: ConversationListTabsViewModel by viewModels(ownerProducer = { requireActivity() })
private val mainToolbarViewModel: MainToolbarViewModel by activityViewModels()
private val mainNavigationViewModel: MainNavigationViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
initializeSharedElementTransition()
viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick)
viewLifecycleOwner.lifecycle.addObserver(viewModel.callLogPeekHelper)
@@ -150,9 +143,6 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
this.callLogAdapter = callLogAdapter
requireListener<Material3OnScrollHelperBinder>().bindScrollHelper(binding.recycler, viewLifecycleOwner)
binding.fab.setOnClickListener {
startActivity(NewCallActivity.createIntent(requireContext()))
}
binding.pullView.setPillText(R.string.CallLogFragment__filtered_by_missed)
@@ -180,12 +170,16 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (!closeSearchIfOpen()) {
tabsViewModel.onChatsSelected()
mainNavigationViewModel.onChatsSelected()
}
}
}
)
if (resources.getWindowSizeClass().isCompact()) {
ViewUtil.setBottomMargin(binding.bottomActionBar, ViewUtil.getNavigationBarHeight(binding.bottomActionBar))
}
signalBottomActionBarController = SignalBottomActionBarController(
binding.bottomActionBar,
binding.recycler,
@@ -204,29 +198,9 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
callLogAdapter?.onTimestampTick()
}
private fun initializeSharedElementTransition() {
ViewCompat.setTransitionName(binding.fab, "new_convo_fab")
ViewCompat.setTransitionName(binding.fabSharedElementTarget, "camera_fab")
sharedElementEnterTransition = TransitionInflater.from(requireContext()).inflateTransition(R.transition.change_transform_fabs)
setEnterSharedElementCallback(object : SharedElementCallback() {
override fun onSharedElementStart(sharedElementNames: MutableList<String>?, sharedElements: MutableList<View>?, sharedElementSnapshots: MutableList<View>?) {
if (sharedElementNames?.contains("camera_fab") == true) {
this@CallLogFragment.binding.fab.setImageResource(R.drawable.symbol_edit_24)
disposables += Single.timer(200, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
this@CallLogFragment.binding.fab.setImageResource(R.drawable.symbol_phone_plus_24)
this@CallLogFragment.binding.fabSharedElementTarget.alpha = 0f
}
}
}
})
}
private fun initializeTapToScrollToTop(scrollToPositionDelegate: ScrollToPositionDelegate) {
disposables += tabsViewModel.tabClickEvents
.filter { it == MainNavigationDestination.CALLS }
disposables += mainNavigationViewModel.tabClickEvents
.filter { it == MainNavigationListLocation.CALLS }
.subscribeBy(onNext = {
scrollToPositionDelegate.resetScrollPosition()
})
@@ -320,7 +294,7 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
}
override fun onCreateACallLinkClicked() {
findNavController().navigate(R.id.createCallLinkBottomSheet)
CreateCallLinkBottomSheetDialogFragment().show(parentFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
override fun onCallClicked(callLogRow: CallLogRow.Call) {
@@ -363,14 +337,22 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
override fun onStartAudioCallClicked(recipient: Recipient) {
CommunicationActions.startVoiceCall(this, recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
mainNavigationViewModel.setSnackbar(
SnackbarState(
getString(R.string.CommunicationActions__you_are_already_in_a_call)
)
)
}
}
override fun onStartVideoCallClicked(recipient: Recipient, canUserBeginCall: Boolean) {
if (canUserBeginCall) {
CommunicationActions.startVideoCall(this, recipient) {
YouAreAlreadyInACallSnackbar.show(requireView())
mainNavigationViewModel.setSnackbar(
SnackbarState(
getString(R.string.CommunicationActions__you_are_already_in_a_call)
)
)
}
} else {
ConversationDialogs.displayCannotStartGroupCallDueToPermissionsDialog(requireContext())
@@ -461,13 +443,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
}
CallLogDeletionResult.Success -> {
Snackbar
.make(
binding.root,
snackbarMessage,
Snackbar.LENGTH_SHORT
mainNavigationViewModel.setSnackbar(
SnackbarState(
message = snackbarMessage,
duration = SnackbarDuration.Short
)
.show()
)
}
is CallLogDeletionResult.UnknownFailure -> {
@@ -488,14 +469,12 @@ class CallLogFragment : Fragment(R.layout.call_log_fragment), CallLogAdapter.Cal
val actionMode = (requireActivity() as AppCompatActivity).startSupportActionMode(callback)
requireListener<Callback>().onMultiSelectStarted()
signalBottomActionBarController.setVisibility(true)
binding.fab.visible = false
return actionMode
}
override fun onActionModeWillEnd() {
requireListener<Callback>().onMultiSelectFinished()
signalBottomActionBarController.setVisibility(false)
binding.fab.visible = true
}
override fun getResources(): Resources = resources

View File

@@ -7,7 +7,8 @@ import org.signal.paging.PagedDataSource
class CallLogPagedDataSource(
private val query: String?,
private val filter: CallLogFilter,
private val repository: CallRepository
private val repository: CallRepository,
private val hasSelection: Boolean
) : PagedDataSource<CallLogRow.Id, CallLogRow> {
companion object {
@@ -46,7 +47,7 @@ class CallLogPagedDataSource(
val clearFilterStart = callEventStart + callEventsCount
var remaining = length
if (start < callLinkStart) {
if (start < callLinkStart && !hasSelection) {
callLogRows.add(CallLogRow.CreateCallLink)
remaining -= 1
}

View File

@@ -59,14 +59,21 @@ class CallLogViewModel(
init {
disposables.add(callLogStore)
disposables += distinctQueryFilterPairs.subscribe { (query, filter) ->
pagedData.onNext(
PagedData.createForObservable(
CallLogPagedDataSource(query, filter, callLogRepository),
pagingConfig
disposables += distinctQueryFilterPairs
.switchMap { (query, filter) ->
selected.map {
Triple(query, filter, it != CallLogSelectionState.empty())
}
}
.distinctUntilChanged()
.subscribe { (query, filter, hasSelection) ->
pagedData.onNext(
PagedData.createForObservable(
CallLogPagedDataSource(query, filter, callLogRepository, hasSelection),
pagingConfig
)
)
)
}
}
disposables += pagedData.map { it.controller }.subscribe {
controller.set(it)

View File

@@ -13,9 +13,9 @@ import org.signal.core.util.concurrent.SimpleTask
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.ContactSelectionActivity
import org.thoughtcrime.securesms.ContactSelectionListFragment
import org.thoughtcrime.securesms.InviteActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.contacts.ContactSelectionDisplayMode
import org.thoughtcrime.securesms.contacts.paged.ChatType
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -111,7 +111,7 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment
}
override fun onInvite() {
startActivity(Intent(this, InviteActivity::class.java))
startActivity(AppSettingsActivity.invite(this))
}
private fun handleManualRefresh() {
@@ -130,7 +130,7 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment
when (menuItem.itemId) {
android.R.id.home -> ActivityCompat.finishAfterTransition(this@NewCallActivity)
R.id.menu_refresh -> handleManualRefresh()
R.id.menu_invite -> startActivity(Intent(this@NewCallActivity, InviteActivity::class.java))
R.id.menu_invite -> startActivity(AppSettingsActivity.invite(this@NewCallActivity))
}
return true

View File

@@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.thoughtcrime.securesms.util.NameUtil;
import java.util.Collections;
import java.util.List;
@@ -62,12 +62,13 @@ public final class AvatarImageView extends AppCompatImageView {
private final RequestListener<Drawable> redownloadRequestListener = new RedownloadRequestListener();
private int size;
private boolean inverted;
private OnClickListener listener;
private boolean blurred;
private ChatColors chatColors;
private FixedSizeTarget fixedSizeTarget;
private int size;
private boolean inverted;
private OnClickListener listener;
private boolean blurred;
private ChatColors chatColors;
private String initials;
private FixedSizeTarget fixedSizeTarget;
private @Nullable RecipientContactPhoto recipientContactPhoto;
private @NonNull Drawable unknownRecipientDrawable;
@@ -100,6 +101,7 @@ public final class AvatarImageView extends AppCompatImageView {
unknownRecipientDrawable = new FallbackAvatarDrawable(context, new FallbackAvatar.Resource.Person(AvatarColor.UNKNOWN)).circleCrop();
blurred = false;
chatColors = null;
initials = null;
}
@Override
@@ -123,12 +125,7 @@ public final class AvatarImageView extends AppCompatImageView {
* Shows self as the actual profile picture.
*/
public void setRecipient(@NonNull Recipient recipient, boolean quickContactEnabled) {
if (recipient.isSelf()) {
setAvatar(Glide.with(this), null, quickContactEnabled);
AvatarUtil.loadIconIntoImageView(recipient, this);
} else {
setAvatar(Glide.with(this), recipient, quickContactEnabled);
}
setAvatar(Glide.with(this), recipient, quickContactEnabled, recipient.isSelf());
}
public AvatarOptions.Builder buildOptions() {
@@ -177,10 +174,12 @@ public final class AvatarImageView extends AppCompatImageView {
boolean shouldBlur = recipient.getShouldBlurAvatar();
ChatColors chatColors = recipient.getChatColors();
String initials = NameUtil.getAbbreviation(recipient.getDisplayName(getContext()));
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred || !Objects.equals(chatColors, this.chatColors)) {
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred || !Objects.equals(chatColors, this.chatColors) || !Objects.equals(initials, this.initials)) {
requestManager.clear(this);
this.chatColors = chatColors;
this.chatColors = chatColors;
this.initials = initials;
recipientContactPhoto = photo;
FallbackAvatarProvider activeFallbackPhotoProvider = this.fallbackAvatarProvider;
@@ -189,7 +188,7 @@ public final class AvatarImageView extends AppCompatImageView {
@Override
public @NonNull FallbackAvatar getFallbackAvatar(@NonNull Recipient recipient) {
if (recipient.isSelf()) {
return new FallbackAvatar.Resource.Person(recipient.getAvatarColor());
return FallbackAvatar.forTextOrDefault(recipient.getDisplayName(getContext()), recipient.getAvatarColor());
}
return FallbackAvatarProvider.super.getFallbackAvatar(recipient);

View File

@@ -253,7 +253,7 @@ public class ComposeText extends EmojiEditText {
}
public @NonNull List<Mention> getMentions() {
return MentionAnnotation.getMentionsFromAnnotations(getText());
return MentionAnnotation.getMentionsFromAnnotations(getTextTrimmed());
}
public boolean hasStyling() {

View File

@@ -15,12 +15,10 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.constraintlayout.widget.ConstraintSet;
import com.airbnb.lottie.LottieAnimationView;
import com.airbnb.lottie.LottieProperty;
@@ -283,9 +281,9 @@ public class ConversationItemFooter extends ConstraintLayout {
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale, @NonNull ConversationItemDisplayMode displayMode) {
dateView.forceLayout();
if (MessageRecordUtil.isScheduled(messageRecord)) {
if (MessageRecordUtil.isScheduled(messageRecord)) {
dateView.setText(DateUtils.getOnlyTimeString(getContext(), ((MmsMessageRecord) messageRecord).getScheduledDate()));
} else if (messageRecord.isMediaPending()) {
} else if (messageRecord.isMediaPending() && messageRecord.isOutgoing() && !messageRecord.isSent()) {
dateView.setText(null);
} else if (messageRecord.isFailed()) {
int errorMsg;

View File

@@ -187,7 +187,7 @@ public class DocumentView extends FrameLayout {
@Override
public void onClick(View v) {
if (!slide.isPendingDownload() && !slide.isInProgress() && viewListener != null) {
if (slide.hasDocument() && slide.getUri()!=null && viewListener != null) {
viewListener.onClick(v, slide);
}
}

View File

@@ -220,6 +220,9 @@ public class InputPanel extends ConstraintLayout
@NonNull QuoteModel.Type quoteType)
{
this.quoteView.setQuote(requestManager, id, author, body, false, attachments, null, quoteType);
if (listener != null) {
this.quoteView.setOnClickListener(v -> listener.onQuoteClicked(id, author.getId()));
}
int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight()
: 0;
@@ -565,6 +568,11 @@ public class InputPanel extends ConstraintLayout
if (listener != null) listener.onRecorderPermissionRequired();
}
@Override
public void onRecorderAlreadyInUse() {
if (listener != null) listener.onRecorderAlreadyInUse();
}
@Override
public void onRecordPressed() {
if (listener != null) listener.onRecorderStarted();
@@ -784,13 +792,17 @@ public class InputPanel extends ConstraintLayout
}
private void updateVisibility() {
if (hideForGroupState || hideForBlockedState || hideForSearch || hideForSelection || hideForMessageRequestState) {
if (isHidden()) {
setVisibility(GONE);
} else {
setVisibility(VISIBLE);
}
}
public boolean isHidden() {
return hideForGroupState || hideForBlockedState || hideForSearch || hideForSelection || hideForMessageRequestState;
}
public @Nullable MessageRecord getEditMessage() {
return messageToEdit;
}
@@ -808,11 +820,13 @@ public class InputPanel extends ConstraintLayout
void onRecorderFinished();
void onRecorderCanceled(boolean byUser);
void onRecorderPermissionRequired();
void onRecorderAlreadyInUse();
void onEmojiToggle();
void onLinkPreviewCanceled();
void onStickerSuggestionSelected(@NonNull StickerRecord sticker);
void onQuoteChanged(long id, @NonNull RecipientId author);
void onQuoteCleared();
void onQuoteClicked(long quoteId, RecipientId authorId);
void onEnterEditMode();
void onExitEditMode();
void onQuickCameraToggleClicked();

View File

@@ -5,6 +5,7 @@ import android.os.Build
import android.util.AttributeSet
import android.util.DisplayMetrics
import android.view.Surface
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.Guideline
import androidx.core.content.withStyledAttributes
@@ -63,25 +64,63 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
private val displayMetrics = DisplayMetrics()
private var overridingKeyboard: Boolean = false
private var previousKeyboardHeight: Int = 0
private var applyRootInsets: Boolean = false
private var insets: WindowInsetsCompat? = null
private var windowTypes: Int = InsetAwareConstraintLayout.windowTypes
private val windowInsetsListener = androidx.core.view.OnApplyWindowInsetsListener { _, insets ->
this.insets = insets
applyInsets(windowInsets = insets.getInsets(windowTypes), keyboardInsets = insets.getInsets(keyboardType))
insets
}
val isKeyboardShowing: Boolean
get() = previousKeyboardHeight > 0
init {
ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsetsCompat ->
applyInsets(windowInsets = windowInsetsCompat.getInsets(windowTypes), keyboardInsets = windowInsetsCompat.getInsets(keyboardType))
windowInsetsCompat
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), windowInsetsListener)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), null)
}
init {
if (attrs != null) {
context.withStyledAttributes(attrs, R.styleable.InsetAwareConstraintLayout) {
applyRootInsets = getBoolean(R.styleable.InsetAwareConstraintLayout_applyRootInsets, false)
if (getBoolean(R.styleable.InsetAwareConstraintLayout_animateKeyboardChanges, false)) {
ViewCompat.setWindowInsetsAnimationCallback(this@InsetAwareConstraintLayout, keyboardAnimator)
ViewCompat.setWindowInsetsAnimationCallback(insetTarget(), keyboardAnimator)
}
}
}
}
private fun insetTarget(): View = if (applyRootInsets) rootView else this
/**
* Specifies whether or not window insets should be accounted for when applying
* insets. This is useful when choosing whether to display the content in this
* constraint layout as a full-window view or as a framed view.
*/
fun setUseWindowTypes(useWindowTypes: Boolean) {
windowTypes = if (useWindowTypes) {
InsetAwareConstraintLayout.windowTypes
} else {
0
}
if (insets != null) {
applyInsets(insets!!.getInsets(windowTypes), insets!!.getInsets(keyboardType))
}
}
fun addKeyboardStateListener(listener: KeyboardStateListener) {
keyboardStateListeners += listener
}
@@ -115,10 +154,12 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
if (keyboardInsets.bottom > 0) {
setKeyboardHeight(keyboardInsets.bottom)
if (!keyboardAnimator.animating) {
keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom)
} else {
keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom
if (!overridingKeyboard) {
if (!keyboardAnimator.animating) {
keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom)
} else {
keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom
}
}
} else if (!overridingKeyboard) {
if (!keyboardAnimator.animating) {
@@ -153,6 +194,7 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
protected fun resetKeyboardGuideline() {
clearKeyboardGuidelineOverride()
keyboardGuideline?.setGuidelineEnd(navigationBarGuideline.guidelineEnd)
keyboardAnimator.endingGuidelineEnd = navigationBarGuideline.guidelineEnd
}
private fun getKeyboardHeight(): Int {

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
import android.Manifest;
import android.content.Context;
import android.graphics.PorterDuff;
import android.media.AudioManager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
@@ -19,6 +20,7 @@ import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.audio.AudioRecordingHandler;
@@ -40,12 +42,16 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
private @Nullable AudioRecordingHandler handler;
private @NonNull State state = State.NOT_RUNNING;
private final AudioManager audioManager;
public MicrophoneRecorderView(Context context) {
super(context);
this.audioManager = ContextCompat.getSystemService(context, AudioManager.class);
}
public MicrophoneRecorderView(Context context, AttributeSet attrs) {
super(context, attrs);
this.audioManager = ContextCompat.getSystemService(context, AudioManager.class);
}
@Override
@@ -110,10 +116,16 @@ public final class MicrophoneRecorderView extends FrameLayout implements View.On
@Override
public boolean onTouch(View v, final MotionEvent event) {
boolean isMicPossiblyInUse = false;
if (audioManager != null) {
isMicPossiblyInUse = audioManager.getMode() == AudioManager.MODE_IN_COMMUNICATION || audioManager.getMode() == AudioManager.MODE_IN_CALL;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO)) {
if (handler != null) handler.onRecordPermissionRequired();
} else if (isMicPossiblyInUse) {
if (handler != null) handler.onRecorderAlreadyInUse();
} else if (state == State.NOT_RUNNING) {
state = State.RUNNING_HELD;
floatingRecordButton.display(event.getX(), event.getY());

View File

@@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.components.transfercontrols.TransferControlVie
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideClickListener;
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
@@ -488,7 +489,12 @@ public class ThumbnailView extends FrameLayout {
transferControlViewStub.setVisibility(View.GONE);
RequestBuilder<Drawable> request = requestManager.load(new DecryptableUri(uri))
Object glideModel = uri;
if (PartAuthority.isLocalUri(uri)) {
glideModel = new DecryptableUri(uri);
}
RequestBuilder<Drawable> request = requestManager.load(glideModel)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.downsample(SignalDownsampleStrategy.CENTER_OUTSIDE_NO_UPSCALE)
.listener(listener);

View File

@@ -9,6 +9,7 @@ import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface;
import com.bumptech.glide.RequestManager;
@@ -80,6 +81,12 @@ public class ZoomingImageView extends FrameLayout {
this.subsamplingImageView.setOnClickListener(v -> ZoomingImageView.this.callOnClick());
}
@Override
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
this.gifView.setOnLongClickListener(l);
this.subsamplingImageView.setOnLongClickListener(l);
}
@SuppressLint("StaticFieldLeak")
public void setImageUri(@NonNull RequestManager requestManager, @NonNull Uri uri, @NonNull String contentType, @NonNull Runnable onMediaReady) {
if (MediaUtil.isGif(contentType)) {

View File

@@ -0,0 +1,92 @@
package org.thoughtcrime.securesms.components.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.Previews
import org.signal.core.ui.compose.SignalPreview
import org.thoughtcrime.securesms.R
/**
* Adds a 'Beta' label next to [text] to indicate a feature is in development
*/
@Composable
fun TextWithBetaLabel(
text: String,
textStyle: TextStyle = TextStyle.Default,
modifier: Modifier = Modifier
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
) {
Text(
text = text,
style = textStyle
)
Text(
text = stringResource(R.string.Beta__beta_title).uppercase(),
color = MaterialTheme.colorScheme.onPrimaryContainer,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier
.padding(horizontal = 4.dp)
.background(color = MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(28.dp))
.padding(horizontal = 12.dp, vertical = 4.dp)
)
}
}
/**
* 'Beta' header to indicate a feature is currently in development
*/
@Composable
fun BetaHeader(modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = RoundedCornerShape(12.dp)
)
.padding(16.dp)
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_info_24),
contentDescription = stringResource(id = R.string.Beta__info),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
Text(
text = stringResource(id = R.string.Beta__this_is_beta),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(start = 12.dp)
)
}
}
@SignalPreview
@Composable
fun BetaLabelPreview() {
Previews.Preview {
TextWithBetaLabel("Signal Backups")
}
}
@SignalPreview
@Composable
fun BetaHeaderPreview() {
Previews.Preview {
BetaHeader()
}
}

View File

@@ -1,4 +1,9 @@
package org.thoughtcrime.securesms.components
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components
package org.thoughtcrime.securesms.components.compose
import android.content.DialogInterface
import android.os.Bundle

View File

@@ -1,4 +1,9 @@
package org.thoughtcrime.securesms.components
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.compose
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Checkbox
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.thoughtcrime.securesms.R
/**
* A custom circular [Checkbox] that can be toggled between checked an unchecked states.
*
* @param checked Indicates whether the checkbox is checked or not.
* @param onCheckedChange A callback function invoked when this checkbox is clicked.
* @param modifier The [Modifier] to be applied to this checkbox.
*/
@Composable
fun RoundCheckbox(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
val contentDescription = if (checked) {
stringResource(R.string.SignalCheckbox_accessibility_checked_description)
} else {
stringResource(R.string.SignalCheckbox_accessibility_unchecked_description)
}
Box(
modifier = modifier
.padding(12.dp)
.size(24.dp)
.aspectRatio(1f)
.border(
width = 1.5.dp,
color = if (checked) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.outline
},
shape = CircleShape
)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = { onCheckedChange(!checked) },
onClickLabel = stringResource(R.string.SignalCheckbox_accessibility_on_click_label)
)
.semantics(mergeDescendants = true) {
this.role = Role.Checkbox
this.contentDescription = contentDescription
}
) {
AnimatedVisibility(
visible = checked,
enter = fadeIn(animationSpec = tween(durationMillis = 150)) + scaleIn(initialScale = 1.20f, animationSpec = tween(durationMillis = 500)),
exit = fadeOut(animationSpec = tween(durationMillis = 300)) + scaleOut(targetScale = 0.50f, animationSpec = tween(durationMillis = 600))
) {
Image(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_check_circle_solid_24),
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
}
}
@SignalPreview
@Composable
private fun RoundCheckboxCheckedPreview() = SignalTheme {
RoundCheckbox(checked = true, onCheckedChange = {})
}
@SignalPreview
@Composable
private fun RoundCheckboxUncheckedPreview() = SignalTheme {
RoundCheckbox(checked = false, onCheckedChange = {})
}

View File

@@ -112,6 +112,12 @@ public class EmojiPageView extends RecyclerView implements VariationSelectorList
addItemDecoration(new EmojiItemDecoration(allowVariations, drawable));
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
getParent().requestDisallowInterceptTouchEvent(true);
return super.dispatchTouchEvent(ev);
}
public void presentForEmojiKeyboard() {
setPadding(getPaddingLeft(),
getPaddingTop(),

View File

@@ -10,6 +10,23 @@ import android.view.animation.AnimationUtils
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.util.ViewUtil
@@ -20,7 +37,7 @@ import org.thoughtcrime.securesms.util.ViewUtil
*
* Overflow items are rendered in a [SignalContextMenu].
*/
class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : LinearLayout(context, attributeSet) {
class SignalBottomActionBar(context: Context, attributeSet: AttributeSet?) : LinearLayout(context, attributeSet) {
val items: MutableList<ActionItem> = mutableListOf()
@@ -118,3 +135,55 @@ class SignalBottomActionBar(context: Context, attributeSet: AttributeSet) : Line
view.setOnClickListener { item.action.run() }
}
}
@Composable
fun SignalBottomActionBar(
visible: Boolean = true,
items: List<ActionItem>,
modifier: Modifier = Modifier
) {
val slideAnimationOffset = with(LocalDensity.current) { 40.dp.roundToPx() }
val enterAnimation = slideInVertically(
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
initialOffsetY = { slideAnimationOffset }
) + fadeIn(
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
)
val exitAnimation = slideOutVertically(
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing),
targetOffsetY = { slideAnimationOffset }
) + fadeOut(
animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing)
)
AnimatedVisibility(
visible = visible,
enter = enterAnimation,
exit = exitAnimation,
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 20.dp, vertical = 16.dp)
.wrapContentHeight()
) {
AndroidView(
factory = { context ->
SignalBottomActionBar(context, null)
.apply {
elevation = 0f
setItems(items)
}
},
update = { view ->
view.setItems(items)
},
modifier = Modifier
.padding(4.dp) // prevent shadow clipping during visibility animations
.shadow(
elevation = 4.dp,
shape = RoundedCornerShape(18.dp)
)
)
}
}

View File

@@ -26,4 +26,12 @@ public enum Orientation {
return PORTRAIT_BOTTOM_EDGE;
}
public static @NonNull Orientation fromSurfaceRotation(int surfaceRotation) {
return switch (surfaceRotation) {
case 1 -> LANDSCAPE_LEFT_EDGE;
case 3 -> LANDSCAPE_RIGHT_EDGE;
default -> PORTRAIT_BOTTOM_EDGE;
};
}
}

View File

@@ -78,6 +78,7 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent {
)
StartLocation.BACKUPS_SETTINGS -> AppSettingsFragmentDirections.actionDirectToBackupsSettingsFragment()
StartLocation.INVITE -> AppSettingsFragmentDirections.actionDirectToInviteFragment()
}
}
@@ -223,6 +224,9 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent {
@JvmStatic
fun backupsSettings(context: Context): Intent = getIntentForStartLocation(context, StartLocation.BACKUPS_SETTINGS)
@JvmStatic
fun invite(context: Context): Intent = getIntentForStartLocation(context, StartLocation.INVITE)
private fun getIntentForStartLocation(context: Context, startLocation: StartLocation): Intent {
return Intent(context, AppSettingsActivity::class.java)
.putExtra(ARG_NAV_GRAPH, R.navigation.app_settings_with_change_number)
@@ -250,7 +254,8 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent {
REMOTE_BACKUPS(16),
CHAT_FOLDERS(17),
CREATE_CHAT_FOLDER(18),
BACKUPS_SETTINGS(19);
BACKUPS_SETTINGS(19),
INVITE(20);
companion object {
fun fromCode(code: Int?): StartLocation {

View File

@@ -29,12 +29,14 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
@@ -64,6 +66,7 @@ import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner
import org.thoughtcrime.securesms.banner.ui.compose.Action
import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
import org.thoughtcrime.securesms.banner.ui.compose.Importance
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
import org.thoughtcrime.securesms.components.emoji.Emojifier
import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageMedium
import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository
@@ -181,7 +184,7 @@ private fun AppSettingsContent(
Scaffolds.Settings(
title = stringResource(R.string.text_secure_normal__menu_settings),
navigationContentDescription = stringResource(R.string.CallScreenTopBar__go_back),
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
onNavigationClick = callbacks::onNavigationClick
) { contentPadding ->
Column(
@@ -371,8 +374,19 @@ private fun AppSettingsContent(
if (state.showBackups) {
item {
Rows.TextRow(
text = stringResource(R.string.preferences_chats__backups),
icon = painterResource(R.drawable.symbol_backup_24),
text = {
TextWithBetaLabel(
text = stringResource(R.string.preferences_chats__backups),
textStyle = MaterialTheme.typography.bodyLarge
)
},
icon = {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_backup_24),
contentDescription = stringResource(R.string.preferences_chats__backups),
tint = MaterialTheme.colorScheme.onSurface
)
},
onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_backupsSettingsFragment)
},
@@ -468,7 +482,7 @@ private fun AppSettingsContent(
text = stringResource(R.string.AppSettingsFragment__invite_your_friends),
icon = painterResource(R.drawable.symbol_invite_24),
onClick = {
callbacks.navigate(R.id.action_appSettingsFragment_to_inviteActivity)
callbacks.navigate(R.id.action_appSettingsFragment_to_inviteFragment)
}
)
}

View File

@@ -69,10 +69,10 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
@Suppress("DEPRECATION")
clickPref(
title = DSLSettingsText.from(if (state.hasOptedInWithAccess) R.string.preferences_app_protection__change_your_pin else R.string.preferences_app_protection__create_a_pin),
isEnabled = state.isDeprecatedOrUnregistered(),
title = DSLSettingsText.from(if (state.hasPin || state.hasRestoredAep) R.string.preferences_app_protection__change_your_pin else R.string.preferences_app_protection__create_a_pin),
isEnabled = state.isNotDeprecatedOrUnregistered(),
onClick = {
if (state.hasOptedInWithAccess) {
if (state.hasPin) {
startActivityForResult(CreateSvrPinActivity.getIntentForPinChangeFromSettings(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN)
} else {
startActivityForResult(CreateSvrPinActivity.getIntentForPinCreate(requireContext()), CreateSvrPinActivity.REQUEST_NEW_PIN)
@@ -84,7 +84,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
title = DSLSettingsText.from(R.string.preferences_app_protection__pin_reminders),
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__youll_be_asked_less_frequently),
isChecked = state.hasPin && state.pinRemindersEnabled,
isEnabled = state.hasPin && state.isDeprecatedOrUnregistered(),
isEnabled = state.hasPin && state.isNotDeprecatedOrUnregistered(),
onClick = {
setPinRemindersEnabled(!state.pinRemindersEnabled)
}
@@ -94,7 +94,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
title = DSLSettingsText.from(R.string.preferences_app_protection__registration_lock),
summary = DSLSettingsText.from(R.string.AccountSettingsFragment__require_your_signal_pin),
isChecked = state.registrationLockEnabled,
isEnabled = (state.hasOptedInWithAccess) && state.isDeprecatedOrUnregistered(),
isEnabled = state.hasPin && state.isNotDeprecatedOrUnregistered(),
onClick = {
setRegistrationLockEnabled(!state.registrationLockEnabled)
}
@@ -102,7 +102,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
clickPref(
title = DSLSettingsText.from(R.string.preferences__advanced_pin_settings),
isEnabled = state.isDeprecatedOrUnregistered(),
isEnabled = state.isNotDeprecatedOrUnregistered(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_advancedPinSettingsActivity)
}
@@ -115,7 +115,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
if (SignalStore.account.isRegistered) {
clickPref(
title = DSLSettingsText.from(R.string.AccountSettingsFragment__change_phone_number),
isEnabled = state.isDeprecatedOrUnregistered(),
isEnabled = state.isNotDeprecatedOrUnregistered(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_changePhoneNumberFragment)
}
@@ -125,6 +125,7 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__transfer_account),
summary = DSLSettingsText.from(R.string.preferences_chats__transfer_account_to_a_new_android_device),
isEnabled = state.isNotDeprecatedOrUnregistered(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_oldDeviceTransferActivity)
}
@@ -132,13 +133,13 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
clickPref(
title = DSLSettingsText.from(R.string.AccountSettingsFragment__request_account_data),
isEnabled = state.isDeprecatedOrUnregistered(),
isEnabled = state.isNotDeprecatedOrUnregistered(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_exportAccountFragment)
}
)
if (!state.isDeprecatedOrUnregistered()) {
if (!state.isNotDeprecatedOrUnregistered()) {
if (state.clientDeprecated) {
clickPref(
title = DSLSettingsText.from(R.string.preferences_account_update_signal),
@@ -173,8 +174,8 @@ class AccountSettingsFragment : DSLSettingsFragment(R.string.AccountSettingsFrag
}
clickPref(
title = DSLSettingsText.from(R.string.preferences__delete_account, ContextCompat.getColor(requireContext(), if (state.isDeprecatedOrUnregistered()) R.color.signal_alert_primary else R.color.signal_alert_primary_50)),
isEnabled = state.isDeprecatedOrUnregistered(),
title = DSLSettingsText.from(R.string.preferences__delete_account, ContextCompat.getColor(requireContext(), if (state.isNotDeprecatedOrUnregistered()) R.color.signal_alert_primary else R.color.signal_alert_primary_50)),
isEnabled = state.isNotDeprecatedOrUnregistered(),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_accountSettingsFragment_to_deleteAccountFragment)
}

View File

@@ -2,13 +2,13 @@ package org.thoughtcrime.securesms.components.settings.app.account
data class AccountSettingsState(
val hasPin: Boolean,
val hasOptedInWithAccess: Boolean,
val hasRestoredAep: Boolean,
val pinRemindersEnabled: Boolean,
val registrationLockEnabled: Boolean,
val userUnregistered: Boolean,
val clientDeprecated: Boolean
) {
fun isDeprecatedOrUnregistered(): Boolean {
fun isNotDeprecatedOrUnregistered(): Boolean {
return !(userUnregistered || clientDeprecated)
}
}

View File

@@ -19,7 +19,7 @@ class AccountSettingsViewModel : ViewModel() {
private fun getCurrentState(): AccountSettingsState {
return AccountSettingsState(
hasPin = SignalStore.svr.hasPin() && !SignalStore.svr.hasOptedOut(),
hasOptedInWithAccess = SignalStore.svr.hasOptedInWithAccess(),
hasRestoredAep = SignalStore.account.restoredAccountEntropyPool,
pinRemindersEnabled = SignalStore.pin.arePinRemindersEnabled() && SignalStore.svr.hasPin(),
registrationLockEnabled = SignalStore.svr.isRegistrationLockEnabled,
userUnregistered = TextSecurePreferences.isUnauthorizedReceived(AppDependencies.application),

View File

@@ -27,10 +27,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
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.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -46,6 +48,7 @@ import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
@@ -114,7 +117,7 @@ private fun BackupsSettingsContent(
) {
Scaffolds.Settings(
title = stringResource(R.string.preferences_chats__backups),
navigationIconPainter = painterResource(R.drawable.symbol_arrow_left_24),
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
onNavigationClick = onNavigationClick
) { paddingValues ->
LazyColumn(
@@ -228,9 +231,7 @@ private fun NeverEnabledBackupsRow(
},
text = {
Column {
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups)
)
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
Text(
text = stringResource(R.string.BackupsSettingsFragment_automatic_backups_with_signals),
@@ -268,9 +269,23 @@ private fun InactiveBackupsRow(
onBackupsRowClick: () -> Unit = {}
) {
Rows.TextRow(
text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups),
label = stringResource(R.string.preferences_off),
icon = painterResource(R.drawable.symbol_backup_24),
text = {
Column {
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
Text(
text = stringResource(R.string.preferences_off),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
icon = {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.symbol_backup_24),
contentDescription = stringResource(R.string.preferences_chats__backups),
tint = MaterialTheme.colorScheme.onSurface
)
},
onClick = onBackupsRowClick
)
}
@@ -297,9 +312,7 @@ private fun ActiveBackupsRow(
},
text = {
Column {
Text(
text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups)
)
TextWithBetaLabel(text = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups))
when (enabledState.type) {
is MessageBackupsType.Paid -> {

View File

@@ -5,9 +5,11 @@
package org.thoughtcrime.securesms.components.settings.app.backups
import androidx.annotation.WorkerThread
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -16,6 +18,7 @@ import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.asFlow
import org.signal.core.util.concurrent.SignalDispatchers
import org.signal.core.util.logging.Log
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.backup.v2.BackupRepository
@@ -41,37 +44,54 @@ class BackupsSettingsViewModel : ViewModel() {
val stateFlow: StateFlow<BackupsSettingsState> = internalStateFlow
private val loadRequests = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
init {
viewModelScope.launch(Dispatchers.Default) {
viewModelScope.launch(SignalDispatchers.Default) {
InternetConnectionObserver.observe().asFlow()
.distinctUntilChanged()
.filter { it }
.drop(1)
.collect {
refreshState()
Log.d(TAG, "Triggering refresh from internet reconnect.")
loadRequests.tryEmit(Unit)
}
}
viewModelScope.launch(SignalDispatchers.Default) {
loadRequests.collect {
Log.d(TAG, "-- Dispatching state load.")
loadEnabledState().join()
Log.d(TAG, "-- Completed state load.")
}
}
}
fun refreshState() {
Log.d(TAG, "Refreshing state.")
loadEnabledState()
override fun onCleared() {
Log.d(TAG, "ViewModel has been cleared.")
}
private fun loadEnabledState() {
viewModelScope.launch(Dispatchers.IO) {
fun refreshState() {
Log.d(TAG, "Refreshing state from manual call.")
loadRequests.tryEmit(Unit)
}
@WorkerThread
private fun loadEnabledState(): Job {
return viewModelScope.launch(SignalDispatchers.IO) {
if (!RemoteConfig.messageBackups || !AppDependencies.billingApi.isApiAvailable()) {
Log.w(TAG, "Paid backups are not available on this device.")
internalStateFlow.update { it.copy(enabledState = BackupsSettingsState.EnabledState.NotAvailable, showBackupTierInternalOverride = false) }
return@launch
}
} else {
val enabledState = when (SignalStore.backup.backupTier) {
MessageBackupTier.FREE -> getEnabledStateForFreeTier()
MessageBackupTier.PAID -> getEnabledStateForPaidTier()
null -> getEnabledStateForNoTier()
}
val enabledState = when (SignalStore.backup.backupTier) {
MessageBackupTier.FREE -> getEnabledStateForFreeTier()
MessageBackupTier.PAID -> getEnabledStateForPaidTier()
null -> getEnabledStateForNoTier()
Log.d(TAG, "Found enabled state $enabledState. Updating UI state.")
internalStateFlow.update { it.copy(enabledState = enabledState, showBackupTierInternalOverride = RemoteConfig.internalUser, backupTierInternalOverride = SignalStore.backup.backupTierInternalOverride) }
}
internalStateFlow.update { it.copy(enabledState = enabledState, showBackupTierInternalOverride = RemoteConfig.internalUser, backupTierInternalOverride = SignalStore.backup.backupTierInternalOverride) }
}
}
@@ -82,10 +102,14 @@ class BackupsSettingsViewModel : ViewModel() {
private suspend fun getEnabledStateForFreeTier(): BackupsSettingsState.EnabledState {
return try {
Log.d(TAG, "Attempting to grab enabled state for free tier.")
val backupType = BackupRepository.getBackupsType(MessageBackupTier.FREE)!!
Log.d(TAG, "Retrieved backup type. Returning active state...")
BackupsSettingsState.EnabledState.Active(
expiresAt = 0.seconds,
lastBackupAt = SignalStore.backup.lastBackupTime.milliseconds,
type = BackupRepository.getBackupsType(MessageBackupTier.FREE)!!
type = backupType
)
} catch (e: Exception) {
Log.w(TAG, "Failed to build enabled state.", e)
@@ -95,8 +119,13 @@ class BackupsSettingsViewModel : ViewModel() {
private suspend fun getEnabledStateForPaidTier(): BackupsSettingsState.EnabledState {
return try {
Log.d(TAG, "Attempting to grab enabled state for paid tier.")
val backupType = BackupRepository.getBackupsType(MessageBackupTier.PAID) as MessageBackupsType.Paid
Log.d(TAG, "Retrieved backup type. Grabbing active subscription...")
val activeSubscription = RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP).getOrThrow()
Log.d(TAG, "Retrieved subscription. Active? ${activeSubscription.isActive}")
if (activeSubscription.isActive) {
BackupsSettingsState.EnabledState.Active(
expiresAt = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds,
@@ -120,6 +149,7 @@ class BackupsSettingsViewModel : ViewModel() {
}
private fun getEnabledStateForNoTier(): BackupsSettingsState.EnabledState {
Log.d(TAG, "Grabbing enabled state for no tier.")
return if (SignalStore.uiHints.hasEverEnabledRemoteBackups) {
BackupsSettingsState.EnabledState.Inactive
} else {

View File

@@ -16,12 +16,17 @@ import org.thoughtcrime.securesms.util.Util
* Fragment which only displays the backup key to the user.
*/
class BackupKeyDisplayFragment : ComposeFragment() {
companion object {
const val CLIPBOARD_TIMEOUT_SECONDS = 60
}
@Composable
override fun FragmentContent() {
MessageBackupsKeyRecordScreen(
backupKey = SignalStore.account.accountEntropyPool.displayValue,
onNavigationClick = { findNavController().popBackStack() },
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it) },
onCopyToClipboardClick = { Util.copyToClipboard(requireContext(), it, CLIPBOARD_TIMEOUT_SECONDS) },
onNextClick = { findNavController().popBackStack() }
)
}

View File

@@ -31,8 +31,6 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
@@ -40,10 +38,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
@@ -54,6 +54,7 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
@@ -95,11 +96,13 @@ import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusRow
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.billing.launchManageBackupsSubscription
import org.thoughtcrime.securesms.components.compose.BetaHeader
import org.thoughtcrime.securesms.components.compose.TextWithBetaLabel
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.MessageBackupsCheckoutLauncher.createBackupsCheckoutLauncher
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.fonts.SignalSymbols
import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol
import org.thoughtcrime.securesms.fonts.SignalSymbols.signalSymbolText
import org.thoughtcrime.securesms.help.HelpFragment
import org.thoughtcrime.securesms.keyvalue.protos.ArchiveUploadProgressState
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
@@ -219,7 +222,7 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
}
override fun onCancelMediaRestore() {
viewModel.cancelMediaRestore()
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION)
}
override fun onDisplaySkipMediaRestoreProtectionDialog() {
@@ -247,8 +250,12 @@ class RemoteBackupsSettingsFragment : ComposeFragment() {
BackupAlertBottomSheet.create(BackupAlert.BackupFailed).show(parentFragmentManager, null)
}
override fun onRestoreUsingCellularClick(canUseCellular: Boolean) {
viewModel.setCanRestoreUsingCellular(canUseCellular)
override fun onRestoreUsingCellularConfirm() {
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.RESTORE_OVER_CELLULAR_PROTECTION)
}
override fun onRestoreUsingCellularClick() {
viewModel.setCanRestoreUsingCellular()
}
override fun onRedemptionErrorDetailsClick() {
@@ -336,10 +343,12 @@ private interface ContentCallbacks {
fun onLearnMoreAboutLostSubscription() = Unit
fun onContactSupport() = Unit
fun onLearnMoreAboutBackupFailure() = Unit
fun onRestoreUsingCellularClick(canUseCellular: Boolean) = Unit
fun onRestoreUsingCellularConfirm() = Unit
fun onRestoreUsingCellularClick() = Unit
fun onRedemptionErrorDetailsClick() = Unit
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun RemoteBackupsSettingsContent(
backupsEnabled: Boolean,
@@ -360,10 +369,22 @@ private fun RemoteBackupsSettingsContent(
SnackbarHostState()
}
Scaffolds.Settings(
title = stringResource(id = R.string.RemoteBackupsSettingsFragment__signal_backups),
onNavigationClick = contentCallbacks::onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24),
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
topBar = {
Scaffolds.DefaultTopAppBar(
title = stringResource(R.string.RemoteBackupsSettingsFragment__signal_backups),
titleContent = { _, title ->
TextWithBetaLabel(text = title, textStyle = MaterialTheme.typography.titleLarge)
},
onNavigationClick = contentCallbacks::onNavigationClick,
navigationIconPainter = painterResource(R.drawable.symbol_arrow_start_24),
navigationContentDescription = stringResource(R.string.DefaultTopAppBar__navigate_up_content_description),
scrollBehavior = scrollBehavior
)
},
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
snackbarHost = {
Snackbars.Host(snackbarHostState = snackbarHostState)
}
@@ -372,6 +393,10 @@ private fun RemoteBackupsSettingsContent(
modifier = Modifier
.padding(it)
) {
item {
BetaHeader(modifier = Modifier.padding(horizontal = 16.dp))
}
if (hasRedemptionError) {
item {
RedemptionErrorAlert(onDetailsClick = contentCallbacks::onRedemptionErrorDetailsClick)
@@ -427,18 +452,20 @@ private fun RemoteBackupsSettingsContent(
)
}
item {
Rows.ToggleRow(
checked = canRestoreUsingCellular,
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__restore_using_cellular),
onCheckChanged = contentCallbacks::onRestoreUsingCellularClick
)
if (!canRestoreUsingCellular) {
item {
Rows.TextRow(
text = stringResource(R.string.RemoteBackupsSettingsFragment__resume_download),
icon = painterResource(R.drawable.symbol_arrow_circle_down_24),
onClick = contentCallbacks::onRestoreUsingCellularConfirm
)
}
}
} else if (backupRestoreState is BackupRestoreState.Ready && backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
} else if (backupRestoreState is BackupRestoreState.Ready) {
item {
BackupReadyToDownloadRow(
ready = backupRestoreState,
endOfSubscription = backupState.renewalTime,
backupState = backupState,
onDownloadClick = contentCallbacks::onStartMediaRestore
)
}
@@ -538,6 +565,20 @@ private fun RemoteBackupsSettingsContent(
onSkipClick = contentCallbacks::onSkipMediaRestore
)
}
RemoteBackupsSettingsState.Dialog.CANCEL_MEDIA_RESTORE_PROTECTION -> {
CancelInitialRestoreDialog(
onDismiss = contentCallbacks::onDialogDismissed,
onSkipClick = contentCallbacks::onSkipMediaRestore
)
}
RemoteBackupsSettingsState.Dialog.RESTORE_OVER_CELLULAR_PROTECTION -> {
ResumeRestoreOverCellularDialog(
onDismiss = contentCallbacks::onDialogDismissed,
onResumeOverCellularClick = contentCallbacks::onRestoreUsingCellularClick
)
}
}
val snackbarMessageId = remember(requestedSnackbar) {
@@ -687,14 +728,10 @@ private fun BackupCard(
}
Text(
text = buildAnnotatedString {
if (backupState.isActive()) {
SignalSymbol(SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.CHECKMARK)
append(" ")
}
append(title)
},
text = signalSymbolText(
text = title,
glyphStart = if (backupState.isActive()) SignalSymbols.Glyph.CHECK else null
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodyMedium
)
@@ -1170,6 +1207,37 @@ private fun SkipDownloadDialog(
)
}
@Composable
private fun CancelInitialRestoreDialog(
onSkipClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.RemoteBackupsSettingsFragment__skip_restore_question),
body = stringResource(R.string.RemoteBackupsSettingsFragment__skip_restore_message),
confirm = stringResource(R.string.RemoteBackupsSettingsFragment__skip),
dismiss = stringResource(android.R.string.cancel),
confirmColor = MaterialTheme.colorScheme.error,
onConfirm = onSkipClick,
onDismiss = onDismiss
)
}
@Composable
private fun ResumeRestoreOverCellularDialog(
onResumeOverCellularClick: () -> Unit = {},
onDismiss: () -> Unit = {}
) {
Dialogs.SimpleAlertDialog(
title = stringResource(R.string.ResumeRestoreCellular_resume_using_cellular_title),
body = stringResource(R.string.ResumeRestoreCellular_resume_using_cellular_message),
confirm = stringResource(R.string.BackupStatus__resume),
dismiss = stringResource(android.R.string.cancel),
onConfirm = onResumeOverCellularClick,
onDismiss = onDismiss
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun CircularProgressDialog(
@@ -1183,8 +1251,8 @@ private fun CircularProgressDialog(
)
) {
Surface(
shape = AlertDialogDefaults.shape,
color = AlertDialogDefaults.containerColor
shape = Dialogs.Defaults.shape,
color = Dialogs.Defaults.containerColor
) {
Box(
contentAlignment = Alignment.Center,
@@ -1206,21 +1274,21 @@ private fun BackupFrequencyDialog(
onSelected: (BackupFrequency) -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
BasicAlertDialog(
onDismissRequest = onDismiss
) {
Surface {
Surface(
color = Dialogs.Defaults.containerColor,
shape = Dialogs.Defaults.shape,
shadowElevation = Dialogs.Defaults.TonalElevation
) {
Column(
modifier = Modifier
.background(
color = AlertDialogDefaults.containerColor,
shape = AlertDialogDefaults.shape
)
.fillMaxWidth()
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(id = R.string.RemoteBackupsSettingsFragment__backup_frequency),
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(24.dp)
)
@@ -1260,11 +1328,16 @@ private fun BackupFrequencyDialog(
@Composable
private fun BackupReadyToDownloadRow(
ready: BackupRestoreState.Ready,
endOfSubscription: Duration,
backupState: RemoteBackupsSettingsState.BackupState,
onDownloadClick: () -> Unit = {}
) {
val days = (endOfSubscription - System.currentTimeMillis().milliseconds).inWholeDays.toInt()
val string = pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, days, ready.bytes, days)
val string = if (backupState is RemoteBackupsSettingsState.BackupState.Canceled) {
val days = (backupState.renewalTime - System.currentTimeMillis().milliseconds).inWholeDays.toInt()
pluralStringResource(R.plurals.RemoteBackupsSettingsFragment__you_have_s_of_backup_data, days, ready.bytes, days)
} else {
stringResource(R.string.RemoteBackupsSettingsFragment__you_have_s_of_backup_data_not_on_device, ready.bytes)
}
val annotated = buildAnnotatedString {
append(string)
val startIndex = string.indexOf(ready.bytes)
@@ -1442,7 +1515,7 @@ private fun BackupReadyToDownloadPreview() {
Previews.Preview {
BackupReadyToDownloadRow(
ready = BackupRestoreState.Ready("12GB"),
endOfSubscription = System.currentTimeMillis().milliseconds + 30.days
backupState = RemoteBackupsSettingsState.BackupState.None
)
}
}

View File

@@ -115,7 +115,9 @@ data class RemoteBackupsSettingsState(
DOWNLOADING_YOUR_BACKUP,
TURN_OFF_FAILED,
SUBSCRIPTION_NOT_FOUND,
SKIP_MEDIA_RESTORE_PROTECTION
SKIP_MEDIA_RESTORE_PROTECTION,
CANCEL_MEDIA_RESTORE_PROTECTION,
RESTORE_OVER_CELLULAR_PROTECTION
}
enum class Snackbar {

View File

@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
@@ -54,7 +55,7 @@ import kotlin.time.Duration.Companion.seconds
class RemoteBackupsSettingsViewModel : ViewModel() {
companion object {
private val TAG = Log.tag(RemoteBackupsSettingsFragment::class)
private val TAG = Log.tag(RemoteBackupsSettingsViewModel::class)
}
private val _state = MutableStateFlow(
@@ -83,15 +84,25 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
}
viewModelScope.launch(Dispatchers.Default) {
viewModelScope.launch(Dispatchers.IO) {
val restoreProgress = MediaRestoreProgressBanner()
var optimizedRemainingBytes = 0L
while (isActive) {
if (restoreProgress.enabled) {
Log.d(TAG, "Backup is being restored. Collecting updates.")
restoreProgress.dataFlow.collectLatest { latest ->
_restoreState.update { BackupRestoreState.FromBackupStatusData(latest) }
}
restoreProgress
.dataFlow
.takeWhile { it !is BackupStatusData.RestoringMedia || it.restoreStatus != BackupStatusData.RestoreStatus.FINISHED }
.collectLatest { latest ->
_restoreState.update { BackupRestoreState.FromBackupStatusData(latest) }
}
} else if (
!SignalStore.backup.optimizeStorage &&
SignalStore.backup.userManuallySkippedMediaRestore &&
SignalDatabase.attachments.getOptimizedMediaAttachmentSize().also { optimizedRemainingBytes = it } > 0
) {
_restoreState.update { BackupRestoreState.Ready(optimizedRemainingBytes.bytes.toUnitString()) }
} else if (SignalStore.backup.totalRestorableAttachmentSize > 0L) {
_restoreState.update { BackupRestoreState.Ready(SignalStore.backup.totalRestorableAttachmentSize.bytes.toUnitString()) }
} else if (BackupRepository.shouldDisplayBackupFailedSettingsRow()) {
@@ -126,9 +137,9 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
_state.update { it.copy(canBackUpUsingCellular = canBackUpUsingCellular) }
}
fun setCanRestoreUsingCellular(canRestoreUsingCellular: Boolean) {
SignalStore.backup.restoreWithCellular = canRestoreUsingCellular
_state.update { it.copy(canRestoreUsingCellular = canRestoreUsingCellular) }
fun setCanRestoreUsingCellular() {
SignalStore.backup.restoreWithCellular = true
_state.update { it.copy(canRestoreUsingCellular = true) }
}
fun setBackupsFrequency(backupsFrequency: BackupFrequency) {
@@ -139,17 +150,13 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
fun beginMediaRestore() {
// TODO - [backups] Begin media restore.
BackupRepository.resumeMediaRestore()
}
fun skipMediaRestore() {
BackupRepository.skipMediaRestore()
}
fun cancelMediaRestore() {
// TODO - [backups] Cancel in-progress media restoration
}
fun requestDialog(dialog: RemoteBackupsSettingsState.Dialog) {
_state.update { it.copy(dialog = dialog) }
}
@@ -207,6 +214,15 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
}
private suspend fun refreshState(lastPurchase: InAppPaymentTable.InAppPayment?) {
try {
performStateRefresh(lastPurchase)
} catch (e: Exception) {
Log.w(TAG, "State refresh failed", e)
throw e
}
}
private suspend fun performStateRefresh(lastPurchase: InAppPaymentTable.InAppPayment?) {
val tier = SignalStore.backup.latestBackupTier
_state.update {
@@ -262,18 +278,26 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
it.copy(
backupState = RemoteBackupsSettingsState.BackupState.SubscriptionMismatchMissingGooglePlay(
messageBackupsType = type,
renewalTime = activeSubscription!!.activeSubscription.endOfCurrentPeriod.seconds
renewalTime = activeSubscription.activeSubscription.endOfCurrentPeriod.seconds
)
)
}
return
}
hasActiveSignalSubscription && hasActiveGooglePlayBillingSubscription -> {
Log.d(TAG, "Found erroneous mismatch. Clearing.")
Log.d(TAG, "Found active signal subscription and active google play subscription. Clearing mismatch.")
SignalStore.backup.subscriptionStateMismatchDetected = false
}
!hasActiveSignalSubscription && !hasActiveGooglePlayBillingSubscription -> {
Log.d(TAG, "Found inactive signal subscription and inactive google play subscription. Clearing mismatch.")
SignalStore.backup.subscriptionStateMismatchDetected = false
}
else -> {
Log.w(TAG, "Hit unexpected subscription mismatch state: signal:false, google:true")
return
}
}
@@ -287,6 +311,7 @@ class RemoteBackupsSettingsViewModel : ViewModel() {
BackupRepository.getBackupsType(tier) as MessageBackupsType.Paid
}
Log.d(TAG, "Attempting to retrieve current subscription...")
val activeSubscription = withContext(Dispatchers.IO) {
RecurringInAppPaymentRepository.getActiveSubscriptionSync(InAppPaymentSubscriberRecord.Type.BACKUP)
}

View File

@@ -46,11 +46,11 @@ import org.whispersystems.signalservice.api.push.ServiceIdType
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.push.SignedPreKeyEntity
import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity
import org.whispersystems.signalservice.internal.push.MismatchedDevices
import org.whispersystems.signalservice.internal.push.OutgoingPushMessage
import org.whispersystems.signalservice.internal.push.SyncMessage
import org.whispersystems.signalservice.internal.push.VerifyAccountResponse
import org.whispersystems.signalservice.internal.push.WhoAmIResponse
import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException
import java.io.IOException
import java.security.MessageDigest
import java.security.SecureRandom
@@ -267,12 +267,14 @@ class ChangeNumberRepository(
SignalStore.misc.setPendingChangeNumberMetadata(metadata)
withContext(Dispatchers.IO) {
result = accountManager.registrationApi.changeNumber(request)
result = SignalNetwork.account.changeNumber(request)
}
val possibleError = result.getCause() as? MismatchedDevicesException
if (possibleError != null) {
messageSender.handleChangeNumberMismatchDevices(possibleError.mismatchedDevices)
if (result is NetworkResult.StatusCodeError && result.code == 409) {
val mismatchedDevices: MismatchedDevices? = result.parseJsonBody()
if (mismatchedDevices != null) {
messageSender.handleChangeNumberMismatchDevices(mismatchedDevices)
}
attempts++
} else {
completed = true

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