Compare commits

...

126 Commits

Author SHA1 Message Date
Cody Henthorne
da43ff1e95 Bump version to 7.5.2 2024-04-23 11:42:24 -04:00
Cody Henthorne
f053ebbd51 Update baseline profile. 2024-04-23 11:36:15 -04:00
Cody Henthorne
87606af29c Update translations and other static files. 2024-04-23 11:30:56 -04:00
Cody Henthorne
c811bdcffa Fix benchmark test messages. 2024-04-23 11:26:36 -04:00
Cody Henthorne
0536628da3 Stagger app wake ups due to analyze database alarm. 2024-04-23 10:44:09 -04:00
Nicholas Tinsley
1fa53cfcb8 Prevent crash on attachment delete while voice note system tone is playing. 2024-04-23 10:22:01 -04:00
Cody Henthorne
a9ea3854d2 Bump version to 7.5.1 2024-04-22 17:06:18 -04:00
Cody Henthorne
dc35261e00 Update translations and other static files. 2024-04-22 16:56:39 -04:00
Cody Henthorne
716bc1f5e7 Cleanup dangling domain reference. 2024-04-22 16:52:02 -04:00
Cody Henthorne
db27204084 Validate pni signature message. 2024-04-22 16:33:03 -04:00
Cody Henthorne
42aeceffe2 Revert full usage of ActiveCallManager. 2024-04-22 16:32:27 -04:00
Greyson Parrelli
03845eabaf Bump version to 7.5.0 2024-04-18 16:44:32 -04:00
Greyson Parrelli
62af9dad50 Update translations and other static files. 2024-04-18 16:43:51 -04:00
Cody Henthorne
ee58d47926 Cycle rx message sending flag. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
d74260b536 Improve network reliability. 2024-04-18 16:24:13 -04:00
Alex Hart
15d8a698c5 Add new name collision state management. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
62cf3feeaa Restore a Local Backup v2 2024-04-18 16:24:13 -04:00
Alex Hart
947ab7d48b Implement skeleton for backup sheets. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
a82b9ee25f Add a job to backfill attachment uploads to the archive service. 2024-04-18 16:24:13 -04:00
mtang-signal
1e4d96b7c4 Add camera permission check to group stories. 2024-04-18 16:24:13 -04:00
Alex Hart
735a8e680c Add backupSubscription field to configuration object. 2024-04-18 16:24:13 -04:00
Alex Hart
d9e9fe1d6a Move backups selection code to its own package. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
4bcd1df4f8 Expand account consistency checks. 2024-04-18 16:24:13 -04:00
Greyson Parrelli
9762899272 Remove old thread remappings. 2024-04-18 16:24:13 -04:00
Alex Hart
ce1b73970c Implement BackupStatus widget. 2024-04-18 16:24:13 -04:00
Alex Hart
58282e589b Implement backups settings fragment. 2024-04-18 16:24:13 -04:00
mtang-signal
75bd113545 Fix missing send button for voice notes. 2024-04-18 16:24:13 -04:00
Cody Henthorne
7a6bd0e1f2 Revert "Remove vestigial call camera toggle button."
This reverts commit 7a9c01e6e5.
2024-04-18 16:24:13 -04:00
Greyson Parrelli
f673c4eb83 Remove sql language annotation (for now).
It's broken in newer versions of Android Studio. It doesn't seem to
allow partial-sql anymore, only fully-formed statements. Same with
roomsql.
2024-04-18 16:24:13 -04:00
Jim Gustafson
cbb04e8f0c Update to RingRTC v2.40.0 2024-04-18 16:24:13 -04:00
mtang-signal
cd03da54d5 Fix note to self message detail text. 2024-04-18 16:24:13 -04:00
Clark
5f31f5966c Update backup locator proto. 2024-04-18 16:24:13 -04:00
Clark
d8bbfe2678 Add archived media sync job. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
7a2d408ca2 Stop voice memo playback if the current item is deleted.
Fixes #13502.
2024-04-18 16:24:13 -04:00
Nicholas Tinsley
5e4dfcc65f Add translator notes for some strings. 2024-04-18 16:24:13 -04:00
Clark
7811e51b41 Add CDN number as parameter for read credential call. 2024-04-18 16:24:13 -04:00
Alex Konradi
9703a868e5 Request new ZKC-based auth credential. 2024-04-18 16:24:13 -04:00
Alex Hart
1b7784b01f Update call strings to align with new designs. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
a83abaca1d Order story viewer names alphabetically. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
29b3f09d8a Catch possible ISE at end of re-registration. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
d36b2a23f5 Hide irrelevant rows in self about sheet. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
8f1722c718 Update placeholder label for view once media. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
5416c3b8aa Improve play button display logic on video editor fragment. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
89eeae36c4 Fix signed int overflow in disappearing timer UI message. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
eec2685e67 Registration refactor initial scaffolding. 2024-04-18 16:24:13 -04:00
Clark
318b59a6b2 Do not fallback to REST for resumable upload spec on ratelimit. 2024-04-18 16:24:13 -04:00
Nicholas Tinsley
a2e0468cd9 Remove "lower hand" confirmation dialog. 2024-04-18 16:24:13 -04:00
Clark
689eacd618 Add initial support for backup and restore of message and media to staging.
Co-authored-by: Cody Henthorne <cody@signal.org>
2024-04-18 16:24:13 -04:00
tedgravlin
8617a074ad Update CLA link in PR template. 2024-04-18 16:24:12 -04:00
Greyson Parrelli
046b8da880 Add missing static IPs.
Fixes #13513
2024-04-18 16:24:12 -04:00
Clark Chen
34a36ddfea Bump version to 7.4.2 2024-04-15 16:32:09 -04:00
Clark Chen
9330448198 Update translations and other static files. 2024-04-15 16:24:09 -04:00
Clark Chen
b3336b4d84 Revert "Use existing libsignal proguard rules."
This reverts commit 2ce6ea9a2a.
2024-04-15 10:17:36 -04:00
Alex Hart
9553c94097 Bump version to 7.4.1 2024-04-12 16:38:43 -03:00
Alex Hart
c1845ae1c4 Update baseline profile. 2024-04-12 16:33:11 -03:00
Alex Hart
b6cc3852b0 Update translations and other static files. 2024-04-12 16:28:07 -03:00
Cody Henthorne
eefc86f27e Fix dangling call notification and remove active call manager flag. 2024-04-12 09:38:06 -04:00
Nicholas Tinsley
09404157aa Add processor information to debug log. 2024-04-11 16:09:33 -04:00
Alex Hart
abfd9f8f41 Add proper capitalization settings in nickname activity. 2024-04-11 10:35:47 -03:00
Bishal
e04381fd75 Add fix for missing play button when the audio is not sent in offline mode. 2024-04-11 10:31:12 -03:00
Alex Hart
30cc3ff9fc Bump version to 7.4.0 2024-04-10 16:31:53 -03:00
Alex Hart
6f5f299035 Update baseline profile. 2024-04-10 16:31:47 -03:00
Alex Hart
02eed02cb8 Update translations and other static files. 2024-04-10 16:29:24 -03:00
Greyson Parrelli
c1d29b5c39 Set internalUser=true for nightly builds. 2024-04-10 14:54:35 -04:00
Greyson Parrelli
db4442939d Remove Environment.IS_PNP 2024-04-10 14:52:59 -04:00
tedgravlin
6ece776382 Fix navbar color in multiple instances. 2024-04-10 14:29:58 -03:00
Alex Hart
0eda714755 Send recipients when sending group story sync. 2024-04-10 14:21:34 -03:00
Greyson Parrelli
831d099503 Inline the nicknames feature flag. 2024-04-10 13:18:01 -04:00
Alex Hart
fa23e4ca70 Convert members collection to set to avoid duplicate entries. 2024-04-10 13:45:46 -03:00
Greyson Parrelli
982f602178 Regularly analyze database tables to improve index usage. 2024-04-09 16:55:25 -04:00
Greyson Parrelli
713298109a Specify indexes for mention table queries. 2024-04-09 16:18:21 -04:00
Greyson Parrelli
8793981804 Add a log section for the database schema. 2024-04-09 16:18:21 -04:00
Greyson Parrelli
9bd4e9524c Convert MentionTable to kotlin. 2024-04-09 16:18:21 -04:00
Cody Henthorne
791dc2724f Attempt to fix bad notification for call service shutdown. 2024-04-09 16:18:21 -04:00
Cody Henthorne
ba3473c61a Fix scroll to message when bubble is under toolbar. 2024-04-09 16:18:21 -04:00
moiseev-signal
3ea194255d Add getUsername default method to CredentialsProvider 2024-04-09 16:18:21 -04:00
Cody Henthorne
ea081e981f Treat unregistered user during send as general failure. 2024-04-09 16:18:21 -04:00
Alex Konradi
2ce6ea9a2a Use existing libsignal proguard rules. 2024-04-09 16:18:20 -04:00
Alex Konradi
295c9310e9 Map libsignal CDSI errors to existing exceptions. 2024-04-09 16:18:20 -04:00
Greyson Parrelli
7447ed2eac Add the ability to jump to a specific date in search. 2024-04-09 16:18:20 -04:00
Cody Henthorne
d5bf16b91a Fix incorrect thread body adjustments containing media, mentions, and styling. 2024-04-09 16:18:06 -04:00
Cody Henthorne
76665c1f0d Prevent excessive video toggling in group calls due to server instability. 2024-04-09 16:18:06 -04:00
Cody Henthorne
dd28523b05 Transition full screen call UX to terminal state when call handled by linked device. 2024-04-09 16:18:06 -04:00
Cody Henthorne
16588c401e Reduce verbosity of WebRtcViewModel event logging during calls. 2024-04-09 16:18:06 -04:00
Greyson Parrelli
dbf8a7ca87 Rotate libsignal-net flag. 2024-04-09 16:18:06 -04:00
moiseev-signal
e92c76434e Upgrade to libsignal-client 0.44.0 2024-04-09 16:18:06 -04:00
Greyson Parrelli
7adb581271 Bump version to 7.3.1 2024-04-09 16:17:21 -04:00
Greyson Parrelli
869476a41b Update translations and other static files. 2024-04-09 16:16:47 -04:00
Greyson Parrelli
8daf1bca20 Improve handling of unknown groups. 2024-04-09 15:56:15 -04:00
Greyson Parrelli
d044b3c931 Remove most lazy properties from Recipient. 2024-04-09 15:02:36 -04:00
Cody Henthorne
0fcb19e1cc Fix group recipient resolve race that can cause unknown group recipients in live cache. 2024-04-09 14:59:47 -04:00
Nicholas Tinsley
2a6977da75 Nickname screen copy update. 2024-04-04 09:45:26 -04:00
Nicholas Tinsley
26bd435bf6 Update nickname delete dialog copy. 2024-04-03 16:48:26 -04:00
Greyson Parrelli
91f8d6075c Bump version to 7.3.0 2024-04-03 15:55:34 -04:00
Greyson Parrelli
9ed9a330f4 Update baseline profile. 2024-04-03 15:54:13 -04:00
Greyson Parrelli
8bbf6b790f Update translations and other static files. 2024-04-03 15:47:24 -04:00
Greyson Parrelli
a277e9b307 Fix compilation of benchmark build. 2024-04-03 15:47:24 -04:00
Cody Henthorne
f8e6bcf290 Add username_edit release note cta action. 2024-04-03 14:07:56 -04:00
Greyson Parrelli
3ba2b46bb0 Convert Recipient to kotlin. 2024-04-03 14:02:55 -04:00
Greyson Parrelli
b50eab230d Update strings for 'system contact' -> 'phone contact'. 2024-04-03 14:02:13 -04:00
Alex Hart
3f91824325 Fix bug preventing the review sheet from opening. 2024-04-03 14:02:13 -04:00
Alex Hart
879e05148b Fix database revocation for call links. 2024-04-03 14:02:13 -04:00
moiseev-signal
78e36b85d4 Make sure not more than one libsignal Network instance is ever created
Co-authored-by: Greyson Parrelli <greyson@signal.org>
2024-04-03 14:02:13 -04:00
Alex Hart
544cc06f13 Add chevron to conversation heading. 2024-04-03 14:02:13 -04:00
Cody Henthorne
133b7ef3f1 Fix multiple exception crash in rx message send flow. 2024-04-03 14:02:13 -04:00
Cody Henthorne
08a407dc23 Prevent thread starvation during message sending. 2024-04-03 14:02:13 -04:00
Greyson Parrelli
6c697fad8b Stop reading the PNP capability. 2024-04-03 14:02:13 -04:00
Greyson Parrelli
c904a7aa97 Delete LegacyAttachmentUploadJob.
It's been over 4 months since it was replaced. That's beyond the 90 day
build expiration + 1 day job lifespan. Should be safe to remove.
2024-04-03 14:02:13 -04:00
Greyson Parrelli
ad131d7c65 Enqueue AccountConsistency check when prekey syncs fail. 2024-04-03 14:02:13 -04:00
Alex Hart
e12d2d1e98 Fix local pip movement when in RTL language. 2024-04-03 14:02:12 -04:00
adel-signal
f01e044662 Update to new calling turn info endpoint, add support for turn server ips.
Co-authored-by: Adel Lahlou <adel@signal.com>
2024-04-03 14:02:12 -04:00
Jim Gustafson
03d3ae7043 Update to RingRTC v2.39.3 2024-04-03 14:02:12 -04:00
Greyson Parrelli
6b60a22879 Bump version to 7.2.4 2024-04-03 13:32:38 -04:00
Greyson Parrelli
bbded8caa8 Update translations and other static files. 2024-04-03 13:32:08 -04:00
Greyson Parrelli
3a6352d2a3 Don't show profile name in parens if it's the same as display name. 2024-04-03 13:19:37 -04:00
Greyson Parrelli
8293d6bc4c Allow last-name-only nicknames to be saved. 2024-04-03 11:54:07 -04:00
Greyson Parrelli
56bdb28c2f Fix bug around entering text in the middle of a full note.
There's likely other weirdness, but this at least addresses the most
commond variation, where entering text in the middle of a full note
would start chopping stuff off the end.
2024-04-03 11:20:35 -04:00
Greyson Parrelli
b081fb1e13 Improve recipient shortname selection. 2024-04-03 10:45:47 -04:00
Greyson Parrelli
58c1f64dfe Allow familyName-only nicknames in storage service. 2024-04-03 10:44:04 -04:00
Greyson Parrelli
92b7147dcd Always take the remote nickname. 2024-04-03 10:39:43 -04:00
Greyson Parrelli
fa3a85c948 Bump version to 7.2.3 2024-04-02 15:30:07 -04:00
Greyson Parrelli
9da4513694 Update translations and other static files. 2024-04-02 15:29:14 -04:00
Greyson Parrelli
de520036a9 Allow last-name-only nicknames. 2024-04-02 15:19:44 -04:00
Greyson Parrelli
97ca15a1c0 Allow multi-line entry in note field. 2024-04-02 14:50:13 -04:00
Greyson Parrelli
713a34a5e7 Ensure that conversation count check is on background thread. 2024-04-02 14:36:07 -04:00
Alex Hart
d688280a30 Fix search for users without thread. 2024-04-02 15:27:57 -03:00
447 changed files with 21992 additions and 9076 deletions

View File

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

View File

@@ -21,8 +21,8 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1403
val canonicalVersionName = "7.2.2"
val canonicalVersionCode = 1413
val canonicalVersionName = "7.5.2"
val postFixSize = 100
val abiPostFix: Map<String, Int> = mapOf(
@@ -178,7 +178,6 @@ android {
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3.signal.org\"")
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.signal.org\"")
buildConfigField("String", "SIGNAL_SERVICE_STATUS_URL", "\"uptime.signal.org\"")
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api.backup.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.signal.org\"")
buildConfigField("String", "SIGNAL_SFU_URL", "\"https://sfu.voip.signal.org\"")
buildConfigField("String", "SIGNAL_STAGING_SFU_URL", "\"https://sfu.staging.voip.signal.org\"")
@@ -377,7 +376,6 @@ android {
buildConfigField("String", "SIGNAL_CDN2_URL", "\"https://cdn2-staging.signal.org\"")
buildConfigField("String", "SIGNAL_CDN3_URL", "\"https://cdn3-staging.signal.org\"")
buildConfigField("String", "SIGNAL_CDSI_URL", "\"https://cdsi.staging.signal.org\"")
buildConfigField("String", "SIGNAL_KEY_BACKUP_URL", "\"https://api-staging.backup.signal.org\"")
buildConfigField("String", "SIGNAL_SVR2_URL", "\"https://svr2.staging.signal.org\"")
buildConfigField("String", "SVR2_MRENCLAVE", "\"acb1973aa0bbbd14b3b4e06f145497d948fd4a98efc500fcce363b3b743ec482\"")
buildConfigField("String", "UNIDENTIFIED_SENDER_TRUST_ROOT", "\"BbqY1DzohE4NUZoVF+L18oUPrK3kILllLEJh2UnPSsEx\"")

View File

@@ -9,6 +9,7 @@ import android.Manifest
import android.app.UiAutomation
import android.os.Environment
import androidx.test.platform.app.InstrumentationRegistry
import io.mockk.InternalPlatformDsl.toArray
import okio.ByteString.Companion.toByteString
import org.junit.Assert
import org.junit.Before
@@ -23,6 +24,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.Call
import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
@@ -32,6 +34,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.FilePointer
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.backup.v2.proto.GroupCallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Quote
@@ -668,12 +672,62 @@ class ImportExportTest {
)
}
var sentTime = 0L
val individualCallChatItems = individualCalls.map { call ->
ChatItem(
chatId = 1,
authorId = selfRecipient.id,
dateSent = sentTime++,
sms = false,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = sentTime + 1,
dateServerSent = sentTime,
read = true,
sealedSender = true
),
updateMessage = ChatUpdateMessage(
callingMessage = CallChatUpdate(
callMessage = IndividualCallChatUpdate(
type = IndividualCallChatUpdate.Type.INCOMING_AUDIO_CALL
)
)
)
)
}.toTypedArray()
val startedAci = TestRecipientUtils.nextAci().toByteString()
val groupCallChatItems = groupCalls.map { call ->
ChatItem(
chatId = 1,
authorId = selfRecipient.id,
dateSent = sentTime++,
sms = false,
incoming = ChatItem.IncomingMessageDetails(
dateReceived = sentTime + 1,
dateServerSent = sentTime,
read = true,
sealedSender = true
),
updateMessage = ChatUpdateMessage(
callingMessage = CallChatUpdate(
groupCall = GroupCallChatUpdate(
startedCallAci = startedAci,
startedCallTimestamp = 0,
endedCallTimestamp = 0,
localUserJoined = GroupCallChatUpdate.LocalUserJoined.JOINED,
inCallAcis = emptyList()
)
)
)
)
}.toTypedArray()
importExport(
*standardFrames,
Recipient(
id = 3,
contact = Contact(
aci = TestRecipientUtils.nextAci().toByteString(),
aci = startedAci,
pni = TestRecipientUtils.nextPni().toByteString(),
username = "cool.01",
e164 = 141255501234,
@@ -698,8 +752,21 @@ class ImportExportTest {
name = "Cool test group"
)
),
Chat(
id = 1,
recipientId = 3,
archived = true,
pinnedOrder = 1,
expirationTimerMs = 1.days.inWholeMilliseconds,
muteUntilMs = System.currentTimeMillis(),
markedUnread = true,
dontNotifyForMentionsIfMuted = true,
wallpaper = null
),
*individualCalls.toArray(),
*groupCalls.toArray()
*groupCalls.toArray(),
*individualCallChatItems,
*groupCallChatItems
)
}
@@ -1008,17 +1075,47 @@ class ImportExportTest {
attachmentLocator = FilePointer.AttachmentLocator(
cdnKey = "coolCdnKey",
cdnNumber = 2,
uploadTimestamp = System.currentTimeMillis()
uploadTimestamp = System.currentTimeMillis(),
key = (1..32).map { it.toByte() }.toByteArray().toByteString(),
size = 12345,
digest = (1..32).map { it.toByte() }.toByteArray().toByteString()
),
key = (1..32).map { it.toByte() }.toByteArray().toByteString(),
contentType = "image/png",
size = 12345,
fileName = "very_cool_picture.png",
width = 100,
height = 200,
caption = "Love this cool picture!",
incrementalMacChunkSize = 0
)
),
wasDownloaded = true
),
MessageAttachment(
pointer = FilePointer(
invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator(),
contentType = "image/png",
width = 100,
height = 200,
caption = "Love this cool picture! Too bad u cant download it",
incrementalMacChunkSize = 0
),
wasDownloaded = false
),
MessageAttachment(
pointer = FilePointer(
backupLocator = FilePointer.BackupLocator(
"digestherebutimlazy",
cdnNumber = 3,
key = (1..32).map { it.toByte() }.toByteArray().toByteString(),
digest = (1..64).map { it.toByte() }.toByteArray().toByteString(),
size = 12345
),
contentType = "image/png",
width = 100,
height = 200,
caption = "Love this cool picture! Too bad u cant download it",
incrementalMacChunkSize = 0
),
wasDownloaded = true
)
)
)

View File

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

View File

@@ -14,6 +14,7 @@ import org.junit.runner.RunWith
import org.signal.core.util.Base64
import org.signal.core.util.update
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
import org.thoughtcrime.securesms.keyvalue.SignalStore
@@ -742,7 +743,7 @@ class AttachmentTableTest_deduping {
assertArrayEquals(lhsAttachment.remoteDigest, rhsAttachment.remoteDigest)
assertArrayEquals(lhsAttachment.incrementalDigest, rhsAttachment.incrementalDigest)
assertEquals(lhsAttachment.incrementalMacChunkSize, rhsAttachment.incrementalMacChunkSize)
assertEquals(lhsAttachment.cdnNumber, rhsAttachment.cdnNumber)
assertEquals(lhsAttachment.cdn.cdnNumber, rhsAttachment.cdn.cdnNumber)
}
fun assertDoesNotHaveRemoteFields(attachmentId: AttachmentId) {
@@ -751,7 +752,7 @@ class AttachmentTableTest_deduping {
assertNull(databaseAttachment.remoteLocation)
assertNull(databaseAttachment.remoteDigest)
assertNull(databaseAttachment.remoteKey)
assertEquals(0, databaseAttachment.cdnNumber)
assertEquals(0, databaseAttachment.cdn.cdnNumber)
}
fun assertSkipTransform(attachmentId: AttachmentId, state: Boolean) {
@@ -776,7 +777,7 @@ class AttachmentTableTest_deduping {
AttachmentTable.TRANSFER_PROGRESS_DONE,
databaseAttachment.size, // size
null,
3, // cdnNumber
Cdn.CDN_3, // cdnNumber
location,
key,
digest,

View File

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

View File

@@ -141,7 +141,7 @@ class SignalActivityRule(private val othersCount: Int = 4, private val createGro
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()

View File

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

View File

@@ -100,7 +100,7 @@ object TestUsers {
val recipientId = RecipientId.from(SignalServiceAddress(aci, "+15555551%03d".format(i)))
SignalDatabase.recipients.setProfileName(recipientId, ProfileName.fromParts("Buddy", "#$i"))
SignalDatabase.recipients.setProfileKeyIfAbsent(recipientId, ProfileKeyUtil.createNew())
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true, true))
SignalDatabase.recipients.setCapabilities(recipientId, SignalServiceProfile.Capabilities(true, true))
SignalDatabase.recipients.setProfileSharing(recipientId, true)
SignalDatabase.recipients.markRegistered(recipientId, aci)
val otherIdentity = IdentityKeyUtil.generateIdentityKeyPair()

View File

@@ -67,7 +67,8 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
hasWallpaper = springboardViewModel.hasWallpaper.value,
colorizer = Colorizer(),
startExpirationTimeout = {},
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) }
chatColorsDataProvider = { ChatColorsDrawable.ChatColorsData(null, null) },
displayDialogFragment = {}
)
if (springboardViewModel.hasWallpaper.value) {

View File

@@ -749,7 +749,7 @@
android:exported="false"/>
<activity
android:name=".backup.v2.ui.MessageBackupsFlowActivity"
android:name=".backup.v2.ui.subscription.MessageBackupsFlowActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"
android:theme="@style/Signal.DayNight.NoActionBar"
@@ -837,6 +837,20 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".registration.v2.ui.RegistrationV2Activity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".restore.RestoreActivity"
android:launchMode="singleTask"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"/>
<activity android:name=".revealable.ViewOnceMessageActivity"
android:launchMode="singleTask"
android:theme="@style/TextSecure.FullScreenMedia"
@@ -962,7 +976,7 @@
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false"/>
<activity android:name=".backup.v2.ui.MessageBackupsTestRestoreActivity"
<activity android:name=".backup.v2.ui.subscription.MessageBackupsTestRestoreActivity"
android:theme="@style/TextSecure.LightRegistrationTheme"
android:exported="false"/>
@@ -1168,6 +1182,10 @@
android:name=".service.AttachmentProgressService"
android:exported="false"/>
<service
android:name=".service.BackupProgressService"
android:exported="false"/>
<service
android:name=".gcm.FcmFetchBackgroundService"
android:exported="false"/>
@@ -1288,6 +1306,12 @@
</intent-filter>
</receiver>
<receiver android:name=".service.AnalyzeDatabaseAlarmListener" android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="org.thoughtcrime.securesms.jobs.ForegroundServiceUtil$Receiver" android:exported="false" />
<receiver android:name=".service.PersistentConnectionBootListener" android:exported="false">
@@ -1352,7 +1376,11 @@
</intent-filter>
</receiver>
<service android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService" android:exported="false" />
<service
android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallForegroundService"
android:exported="false"
android:foregroundServiceType="camera|microphone" />
<receiver android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallManager$ActiveCallServiceReceiver" android:exported="false">
<intent-filter>
<action android:name="org.thoughtcrime.securesms.service.webrtc.ActiveCallAction.DENY"/>

File diff suppressed because it is too large Load Diff

View File

@@ -40,6 +40,7 @@ import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.libsignal.protocol.logging.SignalProtocolLoggerProvider;
import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
@@ -83,12 +84,13 @@ import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationUtil;
import org.thoughtcrime.securesms.ringrtc.RingRtcLogger;
import org.thoughtcrime.securesms.service.AnalyzeDatabaseAlarmListener;
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.service.LocalBackupListener;
import org.thoughtcrime.securesms.service.RotateSenderCertificateListener;
import org.thoughtcrime.securesms.service.RotateSignedPreKeyListener;
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
import org.thoughtcrime.securesms.service.webrtc.ActiveCallManager;
import org.thoughtcrime.securesms.service.webrtc.AndroidTelecomUtil;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
@@ -217,6 +219,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
.addPostRender(AccountConsistencyWorkerJob::enqueueIfNecessary)
.addPostRender(GroupRingCleanupJob::enqueue)
.addPostRender(LinkedDeviceInactiveCheckJob::enqueueIfNecessary)
.addPostRender(() -> ActiveCallManager.clearNotifications(this))
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
@@ -419,6 +422,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
LocalBackupListener.schedule(this);
RotateSenderCertificateListener.schedule(this);
RoutineMessageFetchReceiver.startOrUpdateAlarm(this);
AnalyzeDatabaseAlarmListener.schedule(this);
if (BuildConfig.MANAGES_APP_UPDATES) {
ApkUpdateRefreshListener.schedule(this);

View File

@@ -136,7 +136,7 @@ public class NewConversationActivity extends ContactSelectionActivity
if (result instanceof RecipientRepository.LookupResult.Success) {
Recipient resolved = Recipient.resolved(((RecipientRepository.LookupResult.Success) result).getRecipientId());
if (resolved.isRegistered() && resolved.hasServiceId()) {
if (resolved.isRegistered() && resolved.getHasServiceId()) {
launch(resolved);
}
} else if (result instanceof RecipientRepository.LookupResult.NotFound || result instanceof RecipientRepository.LookupResult.InvalidEntry) {

View File

@@ -19,6 +19,7 @@ import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNum
import org.thoughtcrime.securesms.crypto.MasterSecretUtil;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.devicetransfer.olddevice.OldDeviceTransferActivity;
import org.thoughtcrime.securesms.keyvalue.InternalValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity;
import org.thoughtcrime.securesms.migrations.ApplicationMigrationActivity;
@@ -28,8 +29,11 @@ import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.registration.v2.ui.RegistrationV2Activity;
import org.thoughtcrime.securesms.restore.RestoreActivity;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.util.Locale;
@@ -51,6 +55,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
private static final int STATE_TRANSFER_ONGOING = 8;
private static final int STATE_TRANSFER_LOCKED = 9;
private static final int STATE_CHANGE_NUMBER_LOCK = 10;
private static final int STATE_RESTORE_BACKUP = 11;
private SignalServiceNetworkAccess networkAccess;
private BroadcastReceiver clearKeyReceiver;
@@ -125,8 +130,10 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private void routeApplicationState(boolean locked) {
Intent intent = getIntentForState(getApplicationState(locked));
final int applicationState = getApplicationState(locked);
Intent intent = getIntentForState(applicationState);
if (intent != null) {
Log.d(TAG, "routeApplicationState(), intent: " + intent.getComponent());
startActivity(intent);
finish();
}
@@ -146,6 +153,7 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
case STATE_TRANSFER_ONGOING: return getOldDeviceTransferIntent();
case STATE_TRANSFER_LOCKED: return getOldDeviceTransferLockedIntent();
case STATE_CHANGE_NUMBER_LOCK: return getChangeNumberLockIntent();
case STATE_RESTORE_BACKUP: return getRestoreIntent();
default: return null;
}
}
@@ -159,6 +167,8 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return STATE_UI_BLOCKING_UPGRADE;
} else if (!TextSecurePreferences.hasPromptedPushRegistration(this)) {
return STATE_WELCOME_PUSH_SCREEN;
} else if (SignalStore.internalValues().enterRestoreV2Flow()) {
return STATE_RESTORE_BACKUP;
} else if (SignalStore.storageService().needsAccountRestore()) {
return STATE_ENTER_SIGNAL_PIN;
} else if (userHasSkippedOrForgottenPin()) {
@@ -208,7 +218,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
}
private Intent getPushRegistrationIntent() {
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
if (FeatureFlags.registrationV2()) {
return RegistrationV2Activity.newIntentForNewRegistration(this, getIntent());
} else {
return RegistrationNavigationActivity.newIntentForNewRegistration(this, getIntent());
}
}
private Intent getEnterSignalPinIntent() {
@@ -227,6 +241,11 @@ public abstract class PassphraseRequiredActivity extends BaseActivity implements
return getRoutedIntent(CreateSvrPinActivity.class, intent);
}
private Intent getRestoreIntent() {
Intent intent = RestoreActivity.getIntentForRestore(this);
return getRoutedIntent(intent, getIntent());
}
private Intent getCreateProfileNameIntent() {
Intent intent = CreateProfileActivity.getIntentForUserProfile(this);
return getRoutedIntent(intent, getIntent());

View File

@@ -162,6 +162,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private ControlsAndInfoController controlsAndInfo;
private boolean enterPipOnResume;
private long lastProcessedIntentTimestamp;
private WebRtcViewModel previousEvent = null;
private Disposable ephemeralStateDisposable = Disposable.empty();
@@ -885,7 +886,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(@NonNull WebRtcViewModel event) {
Log.i(TAG, "Got message from service: " + event);
Log.i(TAG, "Got message from service: " + event.describeDifference(previousEvent));
previousEvent = event;
viewModel.setRecipient(event.getRecipient());
callScreen.setRecipient(event.getRecipient());

View File

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

View File

@@ -29,7 +29,7 @@ abstract class Attachment(
@JvmField
val fileName: String?,
@JvmField
val cdnNumber: Int,
val cdn: Cdn,
@JvmField
val remoteLocation: String?,
@JvmField
@@ -76,7 +76,7 @@ abstract class Attachment(
transferState = parcel.readInt(),
size = parcel.readLong(),
fileName = parcel.readString(),
cdnNumber = parcel.readInt(),
cdn = Cdn.deserialize(parcel.readInt()),
remoteLocation = parcel.readString(),
remoteKey = parcel.readString(),
remoteDigest = ParcelUtil.readByteArray(parcel),
@@ -103,7 +103,7 @@ abstract class Attachment(
dest.writeInt(transferState)
dest.writeLong(size)
dest.writeString(fileName)
dest.writeInt(cdnNumber)
dest.writeInt(cdn.serialize())
dest.writeString(remoteLocation)
dest.writeString(remoteKey)
ParcelUtil.writeByteArray(dest, remoteDigest)

View File

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

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.attachments
import android.content.Context
import android.graphics.Bitmap
import android.os.Build
import org.signal.core.util.logging.Log
import org.signal.protos.resumableuploads.ResumableUpload
import org.thoughtcrime.securesms.blurhash.BlurHashEncoder
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.util.MediaUtil
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream
import org.whispersystems.signalservice.internal.push.http.ResumableUploadSpec
import java.io.IOException
import java.util.Objects
/**
* A place collect common attachment upload operations to allow for code reuse.
*/
object AttachmentUploadUtil {
private val TAG = Log.tag(AttachmentUploadUtil::class.java)
/**
* Builds a [SignalServiceAttachmentStream] from the provided data, which can then be provided to various upload methods.
*/
@Throws(IOException::class)
fun buildSignalServiceAttachmentStream(
context: Context,
attachment: Attachment,
uploadSpec: ResumableUpload,
cancellationSignal: (() -> Boolean)? = null,
progressListener: ProgressListener? = null
): SignalServiceAttachmentStream {
val inputStream = PartAuthority.getAttachmentStream(context, attachment.uri!!)
val builder = SignalServiceAttachment.newStreamBuilder()
.withStream(inputStream)
.withContentType(attachment.contentType)
.withLength(attachment.size)
.withFileName(attachment.fileName)
.withVoiceNote(attachment.voiceNote)
.withBorderless(attachment.borderless)
.withGif(attachment.videoGif)
.withFaststart(attachment.transformProperties?.mp4FastStart ?: false)
.withWidth(attachment.width)
.withHeight(attachment.height)
.withUploadTimestamp(System.currentTimeMillis())
.withCaption(attachment.caption)
.withResumableUploadSpec(ResumableUploadSpec.from(uploadSpec))
.withCancelationSignal(cancellationSignal)
.withListener(progressListener)
if (MediaUtil.isImageType(attachment.contentType)) {
builder.withBlurHash(getImageBlurHash(context, attachment))
} else if (MediaUtil.isVideoType(attachment.contentType)) {
builder.withBlurHash(getVideoBlurHash(context, attachment))
}
return builder.build()
}
@Throws(IOException::class)
private fun getImageBlurHash(context: Context, attachment: Attachment): String? {
if (attachment.blurHash != null) {
return attachment.blurHash!!.hash
}
if (attachment.uri == null) {
return null
}
return PartAuthority.getAttachmentStream(context, attachment.uri!!).use { inputStream ->
BlurHashEncoder.encode(inputStream)
}
}
@Throws(IOException::class)
private fun getVideoBlurHash(context: Context, attachment: Attachment): String? {
if (attachment.blurHash != null) {
return attachment.blurHash.hash
}
if (Build.VERSION.SDK_INT < 23) {
Log.w(TAG, "Video thumbnails not supported...")
return null
}
return MediaUtil.getVideoThumbnail(context, Objects.requireNonNull(attachment.uri), 1000)?.let { bitmap ->
val thumb = Bitmap.createScaledBitmap(bitmap, 100, 100, false)
bitmap.recycle()
Log.i(TAG, "Generated video thumbnail...")
val hash = BlurHashEncoder.encode(thumb)
thumb.recycle()
hash
}
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.attachments
import org.signal.core.util.IntSerializer
/**
* Attachments/media can come from and go to multiple CDN locations depending on when and where
* they were uploaded. This class represents the CDNs where attachments/media can live.
*/
enum class Cdn(private val value: Int) {
S3(-1),
CDN_0(0),
CDN_2(2),
CDN_3(3);
val cdnNumber: Int
get() {
return when (this) {
S3 -> -1
CDN_0 -> 0
CDN_2 -> 2
CDN_3 -> 3
}
}
fun serialize(): Int {
return Serializer.serialize(this)
}
companion object Serializer : IntSerializer<Cdn> {
override fun serialize(data: Cdn): Int {
return data.value
}
override fun deserialize(data: Int): Cdn {
return values().first { it.value == data }
}
fun fromCdnNumber(cdnNumber: Int): Cdn {
return when (cdnNumber) {
-1 -> S3
0 -> CDN_0
2 -> CDN_2
3 -> CDN_3
else -> throw UnsupportedOperationException()
}
}
}
}

View File

@@ -25,6 +25,15 @@ class DatabaseAttachment : Attachment {
@JvmField
val dataHash: String?
@JvmField
val archiveCdn: Int
@JvmField
val archiveMediaName: String?
@JvmField
val archiveMediaId: String?
private val hasThumbnail: Boolean
val displayOrder: Int
@@ -37,7 +46,7 @@ class DatabaseAttachment : Attachment {
transferProgress: Int,
size: Long,
fileName: String?,
cdnNumber: Int,
cdn: Cdn,
location: String?,
key: String?,
digest: ByteArray?,
@@ -57,13 +66,16 @@ class DatabaseAttachment : Attachment {
transformProperties: TransformProperties?,
displayOrder: Int,
uploadTimestamp: Long,
dataHash: String?
dataHash: String?,
archiveCdn: Int,
archiveMediaName: String?,
archiveMediaId: String?
) : super(
contentType = contentType!!,
transferState = transferProgress,
size = size,
fileName = fileName,
cdnNumber = cdnNumber,
cdn = cdn,
remoteLocation = location,
remoteKey = key,
remoteDigest = digest,
@@ -88,6 +100,9 @@ class DatabaseAttachment : Attachment {
this.dataHash = dataHash
this.hasThumbnail = hasThumbnail
this.displayOrder = displayOrder
this.archiveCdn = archiveCdn
this.archiveMediaName = archiveMediaName
this.archiveMediaId = archiveMediaId
}
constructor(parcel: Parcel) : super(parcel) {
@@ -97,6 +112,9 @@ class DatabaseAttachment : Attachment {
hasThumbnail = ParcelUtil.readBoolean(parcel)
mmsId = parcel.readLong()
displayOrder = parcel.readInt()
archiveCdn = parcel.readInt()
archiveMediaName = parcel.readString()
archiveMediaId = parcel.readString()
}
override fun writeToParcel(dest: Parcel, flags: Int) {
@@ -107,6 +125,9 @@ class DatabaseAttachment : Attachment {
ParcelUtil.writeBoolean(dest, hasThumbnail)
dest.writeLong(mmsId)
dest.writeInt(displayOrder)
dest.writeInt(archiveCdn)
dest.writeString(archiveMediaName)
dest.writeString(archiveMediaId)
}
override val uri: Uri?

View File

@@ -9,7 +9,6 @@ import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.whispersystems.signalservice.api.InvalidMessageStructureException
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.util.AttachmentPointerUtil
import org.whispersystems.signalservice.internal.push.DataMessage
import java.util.Optional
@@ -21,7 +20,7 @@ class PointerAttachment : Attachment {
transferState: Int,
size: Long,
fileName: String?,
cdnNumber: Int,
cdn: Cdn,
location: String,
key: String?,
digest: ByteArray?,
@@ -42,7 +41,7 @@ class PointerAttachment : Attachment {
transferState = transferState,
size = size,
fileName = fileName,
cdnNumber = cdnNumber,
cdn = cdn,
remoteLocation = location,
remoteKey = key,
remoteDigest = digest,
@@ -83,7 +82,7 @@ class PointerAttachment : Attachment {
@JvmStatic
@JvmOverloads
fun forPointer(pointer: Optional<SignalServiceAttachment>, stickerLocator: StickerLocator? = null, fastPreflightId: String? = null): Optional<Attachment> {
fun forPointer(pointer: Optional<SignalServiceAttachment>, stickerLocator: StickerLocator? = null, fastPreflightId: String? = null, transferState: Int = AttachmentTable.TRANSFER_PROGRESS_PENDING): Optional<Attachment> {
if (!pointer.isPresent || !pointer.get().isPointer) {
return Optional.empty()
}
@@ -97,10 +96,10 @@ class PointerAttachment : Attachment {
return Optional.of(
PointerAttachment(
contentType = pointer.get().contentType,
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
transferState = transferState,
size = pointer.get().asPointer().size.orElse(0).toLong(),
fileName = pointer.get().asPointer().fileName.orElse(null),
cdnNumber = pointer.get().asPointer().cdnNumber,
cdn = Cdn.fromCdnNumber(pointer.get().asPointer().cdnNumber),
location = pointer.get().asPointer().remoteId.toString(),
key = encodedKey,
digest = pointer.get().asPointer().digest.orElse(null),
@@ -120,35 +119,6 @@ class PointerAttachment : Attachment {
)
}
fun forPointer(pointer: SignalServiceDataMessage.Quote.QuotedAttachment): Optional<Attachment> {
val thumbnail = pointer.thumbnail
return Optional.of(
PointerAttachment(
contentType = pointer.contentType,
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(),
fileName = pointer.fileName,
cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0,
location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0",
key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null,
digest = thumbnail?.asPointer()?.digest?.orElse(null),
incrementalDigest = thumbnail?.asPointer()?.incrementalDigest?.orElse(null),
incrementalMacChunkSize = thumbnail?.asPointer()?.incrementalMacChunkSize ?: 0,
fastPreflightId = null,
voiceNote = false,
borderless = false,
videoGif = false,
width = thumbnail?.asPointer()?.width ?: 0,
height = thumbnail?.asPointer()?.height ?: 0,
uploadTimestamp = thumbnail?.asPointer()?.uploadTimestamp ?: 0,
caption = thumbnail?.asPointer()?.caption?.orElse(null),
stickerLocator = null,
blurHash = null
)
)
}
fun forPointer(quotedAttachment: DataMessage.Quote.QuotedAttachment): Optional<Attachment> {
val thumbnail: SignalServiceAttachment? = try {
if (quotedAttachment.thumbnail != null) {
@@ -166,7 +136,7 @@ class PointerAttachment : Attachment {
transferState = AttachmentTable.TRANSFER_PROGRESS_PENDING,
size = (if (thumbnail != null) thumbnail.asPointer().size.orElse(0) else 0).toLong(),
fileName = quotedAttachment.fileName,
cdnNumber = thumbnail?.asPointer()?.cdnNumber ?: 0,
cdn = Cdn.fromCdnNumber(thumbnail?.asPointer()?.cdnNumber ?: 0),
location = thumbnail?.asPointer()?.remoteId?.toString() ?: "0",
key = if (thumbnail != null && thumbnail.asPointer().key != null) encodeWithPadding(thumbnail.asPointer().key) else null,
digest = thumbnail?.asPointer()?.digest?.orElse(null),

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.attachments
import android.net.Uri
import android.os.Parcel
import org.thoughtcrime.securesms.blurhash.BlurHash
import org.thoughtcrime.securesms.database.AttachmentTable
/**
@@ -17,7 +18,7 @@ class TombstoneAttachment : Attachment {
transferState = AttachmentTable.TRANSFER_PROGRESS_DONE,
size = 0,
fileName = null,
cdnNumber = 0,
cdn = Cdn.CDN_0,
remoteLocation = null,
remoteKey = null,
remoteDigest = null,
@@ -37,6 +38,44 @@ class TombstoneAttachment : Attachment {
transformProperties = null
)
constructor(
contentType: String?,
incrementalMac: ByteArray?,
incrementalMacChunkSize: Int?,
width: Int?,
height: Int?,
caption: String?,
blurHash: String?,
voiceNote: Boolean = false,
borderless: Boolean = false,
gif: Boolean = false,
quote: Boolean
) : super(
contentType = contentType ?: "",
quote = quote,
transferState = AttachmentTable.TRANSFER_PROGRESS_PERMANENT_FAILURE,
size = 0,
fileName = null,
cdn = Cdn.CDN_0,
remoteLocation = null,
remoteKey = null,
remoteDigest = null,
incrementalDigest = incrementalMac,
fastPreflightId = null,
voiceNote = voiceNote,
borderless = borderless,
videoGif = gif,
width = width ?: 0,
height = height ?: 0,
incrementalMacChunkSize = incrementalMacChunkSize ?: 0,
uploadTimestamp = 0,
caption = caption,
stickerLocator = null,
blurHash = BlurHash.parseOrNull(blurHash),
audioHash = null,
transformProperties = null
)
constructor(parcel: Parcel) : super(parcel)
override val uri: Uri? = null

View File

@@ -69,7 +69,7 @@ class UriAttachment : Attachment {
transferState = transferState,
size = size,
fileName = fileName,
cdnNumber = 0,
cdn = Cdn.CDN_0,
remoteLocation = null,
remoteKey = null,
remoteDigest = null,

View File

@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.OneTimePreKeyTable;
import org.thoughtcrime.securesms.database.PendingRetryReceiptTable;
import org.thoughtcrime.securesms.database.ReactionTable;
import org.thoughtcrime.securesms.database.RemappedRecordTables;
import org.thoughtcrime.securesms.database.SearchTable;
import org.thoughtcrime.securesms.database.SenderKeyTable;
import org.thoughtcrime.securesms.database.SenderKeySharedTable;
@@ -92,7 +93,9 @@ public class FullBackupExporter extends FullBackupBase {
SenderKeyTable.TABLE_NAME,
SenderKeySharedTable.TABLE_NAME,
PendingRetryReceiptTable.TABLE_NAME,
AvatarPickerDatabase.TABLE_NAME
AvatarPickerDatabase.TABLE_NAME,
RemappedRecordTables.Recipients.TABLE_NAME,
RemappedRecordTables.Threads.TABLE_NAME
);
public static BackupEvent export(@NonNull Context context,

View File

@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.Util;
import java.io.ByteArrayOutputStream;
@@ -270,6 +271,13 @@ public class FullBackupImporter extends FullBackupBase {
return;
}
if (FeatureFlags.registrationV2()) {
if (SignalStore.account().getKeysToIncludeInBackup().contains(keyValue.key)) {
Log.i(TAG, "[regv2] skipping restore of " + keyValue.key);
return;
}
}
if (keyValue.blobValue != null) {
dataSet.putBlob(keyValue.key, keyValue.blobValue.toByteArray());
} else if (keyValue.booleanValue != null) {

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup
import org.signal.core.util.LongSerializer
enum class RestoreState(val id: Int, val inProgress: Boolean) {
FAILED(-1, false),
NONE(0, false),
PENDING(1, true),
RESTORING_DB(2, true),
RESTORING_MEDIA(3, true);
companion object {
val serializer: LongSerializer<RestoreState> = Serializer()
}
class Serializer : LongSerializer<RestoreState> {
override fun serialize(data: RestoreState): Long {
return data.id.toLong()
}
override fun deserialize(data: Long): RestoreState {
return when (data.toInt()) {
FAILED.id -> FAILED
PENDING.id -> PENDING
RESTORING_DB.id -> RESTORING_DB
RESTORING_MEDIA.id -> RESTORING_MEDIA
else -> NONE
}
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2023 Signal Messenger, LLC
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -14,6 +14,8 @@ import org.signal.libsignal.messagebackup.MessageBackup.ValidationResult
import org.signal.libsignal.messagebackup.MessageBackupKey
import org.signal.libsignal.protocol.ServiceId.Aci
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.database.ChatItemImportInserter
import org.thoughtcrime.securesms.backup.v2.database.clearAllDataForBackupRestore
@@ -37,17 +39,20 @@ import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.archive.ArchiveGetMediaItemsResponse
import org.whispersystems.signalservice.api.archive.ArchiveMediaRequest
import org.whispersystems.signalservice.api.archive.ArchiveMediaResponse
import org.whispersystems.signalservice.api.archive.ArchiveServiceCredential
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse
import org.whispersystems.signalservice.api.archive.DeleteArchivedMediaRequest
import org.whispersystems.signalservice.api.archive.GetArchiveCdnCredentialsResponse
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment.ProgressListener
import org.whispersystems.signalservice.api.push.ServiceId.ACI
import org.whispersystems.signalservice.api.push.ServiceId.PNI
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import kotlin.time.Duration.Companion.milliseconds
object BackupRepository {
@@ -55,10 +60,8 @@ object BackupRepository {
private val TAG = Log.tag(BackupRepository::class.java)
private const val VERSION = 1L
fun export(plaintext: Boolean = false): ByteArray {
fun export(outputStream: OutputStream, append: (ByteArray) -> Unit, plaintext: Boolean = false) {
val eventTimer = EventTimer()
val outputStream = ByteArrayOutputStream()
val writer: BackupExportWriter = if (plaintext) {
PlainTextBackupWriter(outputStream)
} else {
@@ -66,11 +69,11 @@ object BackupRepository {
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
aci = SignalStore.account().aci!!,
outputStream = outputStream,
append = { mac -> outputStream.write(mac) }
append = append
)
}
val exportState = ExportState(System.currentTimeMillis())
val exportState = ExportState(backupTime = System.currentTimeMillis(), allowMediaBackup = true)
writer.use {
writer.write(
@@ -110,7 +113,11 @@ object BackupRepository {
}
Log.d(TAG, "export() ${eventTimer.stop().summary}")
}
fun export(plaintext: Boolean = false): ByteArray {
val outputStream = ByteArrayOutputStream()
export(outputStream = outputStream, append = { mac -> outputStream.write(mac) }, plaintext = plaintext)
return outputStream.toByteArray()
}
@@ -124,11 +131,13 @@ object BackupRepository {
fun import(length: Long, inputStreamFactory: () -> InputStream, selfData: SelfData, plaintext: Boolean = false) {
val eventTimer = EventTimer()
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val frameReader = if (plaintext) {
PlainTextBackupReader(inputStreamFactory())
} else {
EncryptedBackupReader(
key = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey(),
key = backupKey,
aci = selfData.aci,
streamLength = length,
dataStream = inputStreamFactory
@@ -160,7 +169,7 @@ object BackupRepository {
SignalDatabase.recipients.setProfileSharing(selfId, true)
eventTimer.emit("setup")
val backupState = BackupState()
val backupState = BackupState(backupKey)
val chatItemInserter: ChatItemImportInserter = ChatItemBackupProcessor.beginImport(backupState)
for (frame in frameReader) {
@@ -215,6 +224,22 @@ object BackupRepository {
Log.d(TAG, "import() ${eventTimer.stop().summary}")
}
fun listRemoteMediaObjects(limit: Int, cursor: String? = null): NetworkResult<ArchiveGetMediaItemsResponse> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.setPublicKey(backupKey, credential)
.map { credential }
}
.then { credential ->
api.getArchiveMediaItemsPage(backupKey, credential, limit, cursor)
}
}
/**
* Returns an object with details about the remote backup state.
*/
@@ -281,6 +306,24 @@ object BackupRepository {
.also { Log.i(TAG, "OverallResult: $it") } is NetworkResult.Success
}
fun downloadBackupFile(destination: File, listener: ProgressListener? = null): Boolean {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return api
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.getBackupInfo(backupKey, credential)
}
.then { info -> getCdnReadCredentials(info.cdn ?: Cdn.CDN_3.cdnNumber).map { it.headers to info } }
.map { pair ->
val (cdnCredentials, info) = pair
val messageReceiver = ApplicationDependencies.getSignalServiceMessageReceiver()
messageReceiver.retrieveBackup(info.cdn!!, cdnCredentials, "backups/${info.backupDir}/${info.backupName}", destination, listener)
} is NetworkResult.Success
}
/**
* Returns an object with details about the remote backup state.
*/
@@ -296,7 +339,7 @@ object BackupRepository {
}
}
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<ArchiveMediaResponse> {
fun archiveMedia(attachment: DatabaseAttachment): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
@@ -304,16 +347,27 @@ object BackupRepository {
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
item = attachment.toArchiveMediaRequest(backupKey)
)
api.setPublicKey(backupKey, credential)
.map { credential }
}
.also { Log.i(TAG, "backupMediaResult: $it") }
.then { credential ->
val mediaName = attachment.getMediaName()
val request = attachment.toArchiveMediaRequest(mediaName, backupKey)
api
.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
item = request
)
.map { Triple(mediaName, request.mediaId, it) }
}
.map { (mediaName, mediaId, response) ->
SignalDatabase.attachments.setArchiveData(attachmentId = attachment.attachmentId, archiveCdn = response.cdn, archiveMediaName = mediaName.name, archiveMediaId = mediaId)
}
.also { Log.i(TAG, "archiveMediaResult: $it") }
}
fun archiveMedia(attachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResponse> {
fun archiveMedia(databaseAttachments: List<DatabaseAttachment>): NetworkResult<BatchArchiveMediaResult> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
@@ -321,24 +375,55 @@ object BackupRepository {
.triggerBackupIdReservation(backupKey)
.then { getAuthCredential() }
.then { credential ->
api.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
items = attachments.map { it.toArchiveMediaRequest(backupKey) }
)
val requests = mutableListOf<ArchiveMediaRequest>()
val mediaIdToAttachmentId = mutableMapOf<String, AttachmentId>()
val attachmentIdToMediaName = mutableMapOf<AttachmentId, String>()
databaseAttachments.forEach {
val mediaName = it.getMediaName()
val request = it.toArchiveMediaRequest(mediaName, backupKey)
requests += request
mediaIdToAttachmentId[request.mediaId] = it.attachmentId
attachmentIdToMediaName[it.attachmentId] = mediaName.name
}
api
.archiveAttachmentMedia(
backupKey = backupKey,
serviceCredential = credential,
items = requests
)
.map { BatchArchiveMediaResult(it, mediaIdToAttachmentId, attachmentIdToMediaName) }
}
.also { Log.i(TAG, "backupMediaResult: $it") }
.map { result ->
result
.successfulResponses
.forEach {
val attachmentId = result.mediaIdToAttachmentId(it.mediaId)
val mediaName = result.attachmentIdToMediaName(attachmentId)
SignalDatabase.attachments.setArchiveData(attachmentId = attachmentId, archiveCdn = it.cdn!!, archiveMediaName = mediaName, archiveMediaId = it.mediaId)
}
result
}
.also { Log.i(TAG, "archiveMediaResult: $it") }
}
fun deleteArchivedMedia(attachments: List<DatabaseAttachment>): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val mediaToDelete = attachments.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = 3, // TODO [cody] store and reuse backup cdn returned from copy/move call
mediaId = backupKey.deriveMediaId(Base64.decode(it.dataHash!!)).toString()
)
val mediaToDelete = attachments
.filter { it.archiveMediaId != null }
.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = it.archiveCdn,
mediaId = it.archiveMediaId!!
)
}
if (mediaToDelete.isEmpty()) {
Log.i(TAG, "No media to delete, quick success")
return NetworkResult.Success(Unit)
}
return getAuthCredential()
@@ -349,7 +434,135 @@ object BackupRepository {
mediaToDelete = mediaToDelete
)
}
.also { Log.i(TAG, "deleteBackupMediaResult: $it") }
.map {
SignalDatabase.attachments.clearArchiveData(attachments.map { it.attachmentId })
}
.also { Log.i(TAG, "deleteArchivedMediaResult: $it") }
}
fun deleteAbandonedMediaObjects(mediaObjects: Collection<ArchivedMediaObject>): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
val mediaToDelete = mediaObjects
.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = it.cdn,
mediaId = it.mediaId
)
}
if (mediaToDelete.isEmpty()) {
Log.i(TAG, "No media to delete, quick success")
return NetworkResult.Success(Unit)
}
return getAuthCredential()
.then { credential ->
api.deleteArchivedMedia(
backupKey = backupKey,
serviceCredential = credential,
mediaToDelete = mediaToDelete
)
}
.also { Log.i(TAG, "deleteAbandonedMediaObjectsResult: $it") }
}
fun debugDeleteAllArchivedMedia(): NetworkResult<Unit> {
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return debugGetArchivedMediaState()
.then { archivedMedia ->
val mediaToDelete = archivedMedia
.map {
DeleteArchivedMediaRequest.ArchivedMediaObject(
cdn = it.cdn,
mediaId = it.mediaId
)
}
if (mediaToDelete.isEmpty()) {
Log.i(TAG, "No media to delete, quick success")
NetworkResult.Success(Unit)
} else {
getAuthCredential()
.then { credential ->
api.deleteArchivedMedia(
backupKey = backupKey,
serviceCredential = credential,
mediaToDelete = mediaToDelete
)
}
}
}
.map {
SignalDatabase.attachments.clearAllArchiveData()
}
.also { Log.i(TAG, "debugDeleteAllArchivedMediaResult: $it") }
}
/**
* Retrieve credentials for reading from the backup cdn.
*/
fun getCdnReadCredentials(cdnNumber: Int): NetworkResult<GetArchiveCdnCredentialsResponse> {
val cached = SignalStore.backup().cdnReadCredentials
if (cached != null) {
return NetworkResult.Success(cached)
}
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return getAuthCredential()
.then { credential ->
api.getCdnReadCredentials(
cdnNumber = cdnNumber,
backupKey = backupKey,
serviceCredential = credential
)
}
.also {
if (it is NetworkResult.Success) {
SignalStore.backup().cdnReadCredentials = it.result
}
}
.also { Log.i(TAG, "getCdnReadCredentialsResult: $it") }
}
/**
* Retrieves backupDir and mediaDir, preferring cached value if available.
*
* These will only ever change if the backup expires.
*/
fun getCdnBackupDirectories(): NetworkResult<BackupDirectories> {
val cachedBackupDirectory = SignalStore.backup().cachedBackupDirectory
val cachedBackupMediaDirectory = SignalStore.backup().cachedBackupMediaDirectory
if (cachedBackupDirectory != null && cachedBackupMediaDirectory != null) {
return NetworkResult.Success(
BackupDirectories(
backupDir = cachedBackupDirectory,
mediaDir = cachedBackupMediaDirectory
)
)
}
val api = ApplicationDependencies.getSignalServiceAccountManager().archiveApi
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
return getAuthCredential()
.then { credential ->
api.getBackupInfo(backupKey, credential).map {
BackupDirectories(it.backupDir!!, it.mediaDir!!)
}
}
.also {
if (it is NetworkResult.Success) {
SignalStore.backup().cachedBackupDirectory = it.result.backupDir
SignalStore.backup().cachedBackupMediaDirectory = it.result.mediaDir
}
}
}
/**
@@ -380,15 +593,20 @@ object BackupRepository {
val profileKey: ProfileKey
)
private fun DatabaseAttachment.toArchiveMediaRequest(backupKey: BackupKey): ArchiveMediaRequest {
val mediaSecrets = backupKey.deriveMediaSecrets(Base64.decode(dataHash!!))
fun DatabaseAttachment.getMediaName(): MediaName {
return MediaName.fromDigest(remoteDigest!!)
}
private fun DatabaseAttachment.toArchiveMediaRequest(mediaName: MediaName, backupKey: BackupKey): ArchiveMediaRequest {
val mediaSecrets = backupKey.deriveMediaSecrets(mediaName)
return ArchiveMediaRequest(
sourceAttachment = ArchiveMediaRequest.SourceAttachment(
cdn = cdnNumber,
cdn = cdn.cdnNumber,
key = remoteLocation!!
),
objectLength = AttachmentCipherStreamUtil.getCiphertextLength(PaddingInputStream.getPaddedSize(size)).toInt(),
mediaId = mediaSecrets.id.toString(),
mediaId = mediaSecrets.id.encode(),
hmacKey = Base64.encodeWithPadding(mediaSecrets.macKey),
encryptionKey = Base64.encodeWithPadding(mediaSecrets.cipherKey),
iv = Base64.encodeWithPadding(mediaSecrets.iv)
@@ -396,12 +614,16 @@ object BackupRepository {
}
}
class ExportState(val backupTime: Long) {
data class ArchivedMediaObject(val mediaId: String, val cdn: Int)
data class BackupDirectories(val backupDir: String, val mediaDir: String)
class ExportState(val backupTime: Long, val allowMediaBackup: Boolean) {
val recipientIds = HashSet<Long>()
val threadIds = HashSet<Long>()
}
class BackupState {
class BackupState(val backupKey: BackupKey) {
val backupToLocalRecipientId = HashMap<Long, RecipientId>()
val chatIdToLocalThreadId = HashMap<Long, Long>()
val chatIdToLocalRecipientId = HashMap<Long, RecipientId>()

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.jobs.RestoreAttachmentJob
/**
* Responsible for managing logic around restore prioritization
*/
object BackupRestoreManager {
private val reprioritizedAttachments: HashSet<AttachmentId> = HashSet()
/**
* Raise priority of all attachments for the included message records.
*
* This is so we can make certain attachments get downloaded more quickly
*/
fun prioritizeAttachmentsIfNeeded(messageRecords: List<MessageRecord>) {
SignalExecutors.BOUNDED.execute {
synchronized(this) {
val restoringAttachments: List<AttachmentId> = messageRecords
.mapNotNull { (it as? MmsMessageRecord?)?.slideDeck?.slides }
.flatten()
.mapNotNull { it.asAttachment() as? DatabaseAttachment }
.filter { it.transferState == AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS && !reprioritizedAttachments.contains(it.attachmentId) }
.map { it.attachmentId }
reprioritizedAttachments += restoringAttachments
if (restoringAttachments.isNotEmpty()) {
RestoreAttachmentJob.modifyPriorities(restoringAttachments.toSet(), 1)
}
}
}
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.whispersystems.signalservice.api.archive.BatchArchiveMediaResponse
/**
* Result of attempting to batch copy multiple attachments at once with helpers for
* processing the collection of mini-responses.
*/
data class BatchArchiveMediaResult(
private val response: BatchArchiveMediaResponse,
private val mediaIdToAttachmentId: Map<String, AttachmentId>,
private val attachmentIdToMediaName: Map<AttachmentId, String>
) {
val successfulResponses: Sequence<BatchArchiveMediaResponse.BatchArchiveMediaItemResponse>
get() = response
.responses
.asSequence()
.filter { it.status == 200 }
val sourceNotFoundResponses: Sequence<BatchArchiveMediaResponse.BatchArchiveMediaItemResponse>
get() = response
.responses
.asSequence()
.filter { it.status == 410 }
fun mediaIdToAttachmentId(mediaId: String): AttachmentId {
return mediaIdToAttachmentId[mediaId]!!
}
fun attachmentIdToMediaName(attachmentId: AttachmentId): String {
return attachmentIdToMediaName[attachmentId]!!
}
}

View File

@@ -17,7 +17,9 @@ import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.BackupRepository.getMediaName
import org.thoughtcrime.securesms.backup.v2.proto.CallChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
@@ -36,6 +38,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.backup.v2.proto.Text
import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.MessageTable
import org.thoughtcrime.securesms.database.MessageTypes
@@ -73,7 +76,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
*
* All of this complexity is hidden from the user -- they just get a normal iterator interface.
*/
class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int) : Iterator<ChatItem>, Closeable {
class ChatItemExportIterator(private val cursor: Cursor, private val batchSize: Int, private val archiveMedia: Boolean) : Iterator<ChatItem>, Closeable {
companion object {
private val TAG = Log.tag(ChatItemExportIterator::class.java)
@@ -139,6 +142,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
builder.expiresInMs = null
}
MessageTypes.isProfileChange(record.type) -> {
if (record.body == null) continue
builder.updateMessage = ChatUpdateMessage(
profileChange = try {
val decoded: ByteArray = Base64.decode(record.body!!)
@@ -196,6 +200,7 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
}
MessageTypes.isCallLog(record.type) -> {
builder.sms = false
val call = calls.getCallByMessageId(record.id)
if (call != null) {
builder.updateMessage = ChatUpdateMessage(callingMessage = CallChatUpdate(callId = call.callId))
@@ -228,12 +233,23 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
.withoutNulls()
.map { obj: UUID? -> ACI.from(obj!!).toByteString() }
.toList()
val localUserJoined: GroupCallChatUpdate.LocalUserJoined = if (groupCallUpdateDetails.localUserJoined) {
GroupCallChatUpdate.LocalUserJoined.JOINED
} else if (groupCallUpdateDetails.endedCallTimestamp == 0L) {
GroupCallChatUpdate.LocalUserJoined.UNKNOWN
} else {
GroupCallChatUpdate.LocalUserJoined.DID_NOT_JOIN
}
builder.updateMessage = ChatUpdateMessage(
callingMessage = CallChatUpdate(
groupCall = GroupCallChatUpdate(
startedCallAci = ACI.from(UuidUtil.parseOrThrow(groupCallUpdateDetails.startedCallUuid)).toByteString(),
startedCallTimestamp = groupCallUpdateDetails.startedCallTimestamp,
inCallAcis = joinedMembers
inCallAcis = joinedMembers,
localUserJoined = localUserJoined,
endedCallTimestamp = groupCallUpdateDetails.endedCallTimestamp
)
)
)
@@ -354,24 +370,46 @@ class ChatItemExportIterator(private val cursor: Cursor, private val batchSize:
}
private fun DatabaseAttachment.toBackupAttachment(): MessageAttachment {
val builder = FilePointer.Builder()
builder.contentType = contentType
builder.incrementalMac = incrementalDigest?.toByteString()
builder.incrementalMacChunkSize = incrementalMacChunkSize
builder.fileName = fileName
builder.width = width
builder.height = height
builder.caption = caption
builder.blurHash = blurHash?.hash
if (remoteKey.isNullOrBlank() || remoteDigest == null || size == 0L) {
builder.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
} else {
if (archiveMedia) {
builder.backupLocator = FilePointer.BackupLocator(
mediaName = archiveMediaName ?: this.getMediaName().toString(),
cdnNumber = if (archiveMediaName != null) archiveCdn else Cdn.CDN_3.cdnNumber, // TODO (clark): Update when new proto with optional cdn is landed
key = decode(remoteKey).toByteString(),
size = this.size.toInt(),
digest = remoteDigest.toByteString()
)
} else {
if (remoteLocation.isNullOrBlank()) {
builder.invalidAttachmentLocator = FilePointer.InvalidAttachmentLocator()
} else {
builder.attachmentLocator = FilePointer.AttachmentLocator(
cdnKey = this.remoteLocation,
cdnNumber = this.cdn.cdnNumber,
uploadTimestamp = this.uploadTimestamp,
key = decode(remoteKey).toByteString(),
size = this.size.toInt(),
digest = remoteDigest.toByteString()
)
}
}
}
return MessageAttachment(
pointer = FilePointer(
attachmentLocator = FilePointer.AttachmentLocator(
cdnKey = this.remoteLocation ?: "",
cdnNumber = this.cdnNumber,
uploadTimestamp = this.uploadTimestamp
),
key = if (remoteKey != null) decode(remoteKey).toByteString() else null,
contentType = this.contentType,
size = this.size.toInt(),
incrementalMac = this.incrementalDigest?.toByteString(),
incrementalMacChunkSize = this.incrementalMacChunkSize,
fileName = this.fileName,
width = this.width,
height = this.height,
caption = this.caption,
blurHash = this.blurHash?.hash
)
pointer = builder.build(),
wasDownloaded = this.transferState == AttachmentTable.TRANSFER_PROGRESS_DONE || this.transferState == AttachmentTable.TRANSFER_NEEDS_RESTORE,
flag = if (voiceNote) MessageAttachment.Flag.VOICE_MESSAGE else if (videoGif) MessageAttachment.Flag.GIF else if (borderless) MessageAttachment.Flag.BORDERLESS else MessageAttachment.Flag.NONE
)
}

View File

@@ -13,8 +13,11 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.orNull
import org.signal.core.util.requireLong
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.attachments.ArchivedAttachment
import org.thoughtcrime.securesms.attachments.Attachment
import org.thoughtcrime.securesms.attachments.Cdn
import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
import org.thoughtcrime.securesms.backup.v2.BackupState
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
@@ -26,6 +29,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.Reaction
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.GroupReceiptTable
import org.thoughtcrime.securesms.database.MessageTable
@@ -37,6 +41,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet
import org.thoughtcrime.securesms.database.documents.NetworkFailure
import org.thoughtcrime.securesms.database.documents.NetworkFailureSet
import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil
import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
@@ -48,11 +53,12 @@ import org.thoughtcrime.securesms.mms.QuoteModel
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.JsonUtils
import org.whispersystems.signalservice.api.backup.MediaName
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.util.UuidUtil
import org.whispersystems.signalservice.internal.push.DataMessage
import java.util.Optional
/**
@@ -455,6 +461,10 @@ class ChatItemImportInserter(
IndividualCallChatUpdate.Type.UNKNOWN -> typeFlags
}
}
updateMessage.callingMessage.groupCall != null -> {
typeFlags = MessageTypes.GROUP_CALL_TYPE
this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.callingMessage.groupCall))
}
}
// Calls don't use the incoming/outgoing flags, so we overwrite the flags here
this.put(MessageTable.TYPE, typeFlags)
@@ -570,12 +580,12 @@ class ChatItemImportInserter(
pointer.attachmentLocator.cdnNumber,
SignalServiceAttachmentRemoteId.from(pointer.attachmentLocator.cdnKey),
contentType,
pointer.key?.toByteArray(),
Optional.ofNullable(pointer.size),
pointer.attachmentLocator.key.toByteArray(),
Optional.ofNullable(pointer.attachmentLocator.size),
Optional.empty(),
pointer.width ?: 0,
pointer.height ?: 0,
Optional.empty(),
Optional.ofNullable(pointer.attachmentLocator.digest.toByteArray()),
Optional.ofNullable(pointer.incrementalMac?.toByteArray()),
pointer.incrementalMacChunkSize ?: 0,
Optional.ofNullable(fileName),
@@ -586,14 +596,53 @@ class ChatItemImportInserter(
Optional.ofNullable(pointer.blurHash),
pointer.attachmentLocator.uploadTimestamp
)
return PointerAttachment.forPointer(Optional.of(signalAttachmentPointer)).orNull()
return PointerAttachment.forPointer(
pointer = Optional.of(signalAttachmentPointer),
transferState = if (wasDownloaded) AttachmentTable.TRANSFER_NEEDS_RESTORE else AttachmentTable.TRANSFER_PROGRESS_PENDING
).orNull()
} else if (pointer.invalidAttachmentLocator != null) {
return TombstoneAttachment(
contentType = contentType,
incrementalMac = pointer.incrementalMac?.toByteArray(),
incrementalMacChunkSize = pointer.incrementalMacChunkSize,
width = pointer.width,
height = pointer.height,
caption = pointer.caption,
blurHash = pointer.blurHash,
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
gif = flag == MessageAttachment.Flag.GIF,
quote = false
)
} else if (pointer.backupLocator != null) {
return ArchivedAttachment(
contentType = contentType,
size = pointer.backupLocator.size.toLong(),
cdn = pointer.backupLocator.transitCdnNumber ?: Cdn.CDN_0.cdnNumber,
key = pointer.backupLocator.key.toByteArray(),
cdnKey = pointer.backupLocator.transitCdnKey,
archiveCdn = pointer.backupLocator.cdnNumber,
archiveMediaName = pointer.backupLocator.mediaName,
archiveMediaId = backupState.backupKey.deriveMediaId(MediaName(pointer.backupLocator.mediaName)).encode(),
digest = pointer.backupLocator.digest.toByteArray(),
incrementalMac = pointer.incrementalMac?.toByteArray(),
incrementalMacChunkSize = pointer.incrementalMacChunkSize,
width = pointer.width,
height = pointer.height,
caption = pointer.caption,
blurHash = pointer.blurHash,
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
gif = flag == MessageAttachment.Flag.GIF,
quote = false
)
}
return null
}
private fun Quote.QuotedAttachment.toLocalAttachment(): Attachment? {
return thumbnail?.toLocalAttachment(this.contentType, this.fileName)
?: if (this.contentType == null) null else PointerAttachment.forPointer(SignalServiceDataMessage.Quote.QuotedAttachment(contentType = this.contentType!!, fileName = this.fileName, thumbnail = null)).orNull()
?: if (this.contentType == null) null else PointerAttachment.forPointer(quotedAttachment = DataMessage.Quote.QuotedAttachment(contentType = this.contentType, fileName = this.fileName, thumbnail = null)).orNull()
}
private class MessageInsert(val contentValues: ContentValues, val followUp: ((Long) -> Unit)?)

View File

@@ -16,7 +16,7 @@ import java.util.concurrent.TimeUnit
private val TAG = Log.tag(MessageTable::class.java)
private const val BASE_TYPE = "base_type"
fun MessageTable.getMessagesForBackup(backupTime: Long): ChatItemExportIterator {
fun MessageTable.getMessagesForBackup(backupTime: Long, archiveMedia: Boolean): ChatItemExportIterator {
val cursor = readableDatabase
.select(
MessageTable.ID,
@@ -64,7 +64,7 @@ fun MessageTable.getMessagesForBackup(backupTime: Long): ChatItemExportIterator
.orderBy("${MessageTable.DATE_RECEIVED} ASC")
.run()
return ChatItemExportIterator(cursor, 100)
return ChatItemExportIterator(cursor, 100, archiveMedia)
}
fun MessageTable.createChatItemInserter(backupState: BackupState): ChatItemImportInserter {

View File

@@ -19,7 +19,7 @@ object ChatItemBackupProcessor {
val TAG = Log.tag(ChatItemBackupProcessor::class.java)
fun export(exportState: ExportState, emitter: BackupFrameEmitter) {
SignalDatabase.messages.getMessagesForBackup(exportState.backupTime).use { chatItems ->
SignalDatabase.messages.getMessagesForBackup(exportState.backupTime, exportState.allowMediaBackup).use { chatItems ->
for (chatItem in chatItems) {
if (exportState.threadIds.contains(chatItem.chatId)) {
emitter.emit(Frame(chatItem = chatItem))

View File

@@ -0,0 +1,289 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import android.os.Parcelable
import androidx.annotation.StringRes
import androidx.compose.foundation.background
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.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import kotlinx.parcelize.Parcelize
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Buttons
import org.signal.core.ui.Icons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
/**
* Notifies the user of an issue with their backup.
*/
class BackupAlertBottomSheet : ComposeBottomSheetDialogFragment() {
companion object {
private const val ARG_ALERT = "alert"
fun create(backupAlert: BackupAlert): BackupAlertBottomSheet {
return BackupAlertBottomSheet().apply {
arguments = bundleOf(ARG_ALERT to backupAlert)
}
}
}
private val backupAlert: BackupAlert by lazy(LazyThreadSafetyMode.NONE) {
BundleCompat.getParcelable(requireArguments(), ARG_ALERT, BackupAlert::class.java)!!
}
@Composable
override fun SheetContent() {
BackupAlertSheetContent(
backupAlert = backupAlert,
onPrimaryActionClick = this::performPrimaryAction,
onSecondaryActionClick = this::performSecondaryAction
)
}
@Stable
private fun performPrimaryAction() {
when (backupAlert) {
BackupAlert.GENERIC -> {
// TODO [message-backups] -- Back up now
}
BackupAlert.PAYMENT_PROCESSING -> {
// TODO [message-backups] -- Silence
}
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> {
// TODO [message-backups] -- Download media now
}
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> {
// TODO [message-backups] -- Download media now
}
}
dismissAllowingStateLoss()
}
@Stable
private fun performSecondaryAction() {
when (backupAlert) {
BackupAlert.GENERIC -> {
// TODO [message-backups] - Dismiss and notify later
}
BackupAlert.PAYMENT_PROCESSING -> error("PAYMENT_PROCESSING state does not support a secondary action.")
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> {
// TODO [message-backups] - Silence and remind on last day
}
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> {
// TODO [message-backups] - Silence forever
}
}
dismissAllowingStateLoss()
}
}
@Composable
private fun BackupAlertSheetContent(
backupAlert: BackupAlert,
onPrimaryActionClick: () -> Unit,
onSecondaryActionClick: () -> Unit
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
) {
BottomSheets.Handle()
Spacer(modifier = Modifier.size(26.dp))
val iconColors = rememberBackupsIconColors(backupAlert = backupAlert)
Icons.BrushedForeground(
painter = painterResource(id = R.drawable.symbol_backup_light), // TODO [message-backups] final asset
contentDescription = null,
foregroundBrush = iconColors.foreground,
modifier = Modifier
.size(88.dp)
.background(color = iconColors.background, shape = CircleShape)
.padding(20.dp)
)
Text(
text = stringResource(id = rememberTitleResource(backupAlert = backupAlert)),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
when (backupAlert) {
BackupAlert.GENERIC -> GenericBody()
BackupAlert.PAYMENT_PROCESSING -> PaymentProcessingBody()
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> MediaBackupsAreOffBody()
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> MediaWillBeDeletedTodayBody()
}
val secondaryActionResource = rememberSecondaryActionResource(backupAlert = backupAlert)
val padBottom = if (secondaryActionResource > 0) 16.dp else 56.dp
Buttons.LargeTonal(
onClick = onPrimaryActionClick,
modifier = Modifier
.defaultMinSize(minWidth = 220.dp)
.padding(top = 60.dp, bottom = padBottom)
) {
Text(text = stringResource(id = rememberPrimaryActionResource(backupAlert = backupAlert)))
}
if (secondaryActionResource > 0) {
TextButton(onClick = onSecondaryActionClick, modifier = Modifier.padding(bottom = 32.dp)) {
Text(text = stringResource(id = secondaryActionResource))
}
}
}
}
@Composable
private fun GenericBody() {
Text(text = "TODO")
}
@Composable
private fun PaymentProcessingBody() {
Text(text = "TODO")
}
@Composable
private fun MediaBackupsAreOffBody() {
Text(text = "TODO")
}
@Composable
private fun MediaWillBeDeletedTodayBody() {
Text(text = "TODO")
}
@Composable
private fun rememberBackupsIconColors(backupAlert: BackupAlert): BackupsIconColors {
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.GENERIC, BackupAlert.PAYMENT_PROCESSING -> BackupsIconColors.Warning
BackupAlert.MEDIA_BACKUPS_ARE_OFF, BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> BackupsIconColors.Error
}
}
}
@Composable
@StringRes
private fun rememberTitleResource(backupAlert: BackupAlert): Int {
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.GENERIC -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy
BackupAlert.PAYMENT_PROCESSING -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> R.string.default_error_msg // TODO [message-backups] -- Finalized copy
}
}
}
@Composable
private fun rememberPrimaryActionResource(backupAlert: BackupAlert): Int {
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.GENERIC -> android.R.string.ok // TODO [message-backups] -- Finalized copy
BackupAlert.PAYMENT_PROCESSING -> android.R.string.ok // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> android.R.string.ok // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> android.R.string.ok // TODO [message-backups] -- Finalized copy
}
}
}
@Composable
private fun rememberSecondaryActionResource(backupAlert: BackupAlert): Int {
return remember(backupAlert) {
when (backupAlert) {
BackupAlert.GENERIC -> android.R.string.cancel // TODO [message-backups] -- Finalized copy
BackupAlert.PAYMENT_PROCESSING -> -1
BackupAlert.MEDIA_BACKUPS_ARE_OFF -> android.R.string.cancel // TODO [message-backups] -- Finalized copy
BackupAlert.MEDIA_WILL_BE_DELETED_TODAY -> android.R.string.cancel // TODO [message-backups] -- Finalized copy
}
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewGeneric() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.GENERIC,
onPrimaryActionClick = {},
onSecondaryActionClick = {}
)
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewPayment() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.PAYMENT_PROCESSING,
onPrimaryActionClick = {},
onSecondaryActionClick = {}
)
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewMedia() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.MEDIA_BACKUPS_ARE_OFF,
onPrimaryActionClick = {},
onSecondaryActionClick = {}
)
}
}
@SignalPreview
@Composable
private fun BackupAlertSheetContentPreviewDelete() {
Previews.BottomSheetPreview {
BackupAlertSheetContent(
backupAlert = BackupAlert.MEDIA_WILL_BE_DELETED_TODAY,
onPrimaryActionClick = {},
onSecondaryActionClick = {}
)
}
}
@Parcelize
enum class BackupAlert : Parcelable {
GENERIC,
PAYMENT_PROCESSING,
MEDIA_BACKUPS_ARE_OFF,
MEDIA_WILL_BE_DELETED_TODAY
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
sealed interface BackupsIconColors {
@get:Composable
val foreground: Brush
@get:Composable
val background: Color
object Normal : BackupsIconColors {
override val foreground: Brush
@Composable get() = remember {
Brush.linearGradient(
colors = listOf(Color(0xFF316ED0), Color(0xFF558BE2)),
start = Offset(x = 0f, y = Float.POSITIVE_INFINITY),
end = Offset(x = Float.POSITIVE_INFINITY, y = 0f)
)
}
override val background: Color @Composable get() = MaterialTheme.colorScheme.primaryContainer
}
object Warning : BackupsIconColors {
override val foreground: Brush @Composable get() = SolidColor(Color(0xFFC86600))
override val background: Color @Composable get() = Color(0xFFF9E4B6)
}
object Error : BackupsIconColors {
override val foreground: Brush @Composable get() = SolidColor(MaterialTheme.colorScheme.error)
override val background: Color @Composable get() = Color(0xFFFFD9D9)
}
}

View File

@@ -32,8 +32,8 @@ import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.theme.SignalTheme
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeature
import org.thoughtcrime.securesms.backup.v2.ui.MessageBackupsTypeFeatureRow
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeature
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsTypeFeatureRow
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.devicetransfer.moreoptions.MoreTransferOrRestoreOptionsMode
import org.thoughtcrime.securesms.util.navigation.safeNavigate

View File

@@ -0,0 +1,217 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui.status
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LinearProgressIndicator
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.Brush
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Buttons
import org.signal.core.ui.Icons
import org.signal.core.ui.Previews
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.BackupsIconColors
import kotlin.math.max
import kotlin.math.min
private const val NONE = -1
/**
* Displays a "heads up" widget containing information about the current
* status of the user's backup.
*/
@Composable
fun BackupStatus(
data: BackupStatusData,
onActionClick: () -> Unit = {}
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.border(1.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.38f), shape = RoundedCornerShape(12.dp))
.fillMaxWidth()
.padding(14.dp)
) {
val foreground: Brush = data.iconColors.foreground
Icons.BrushedForeground(
painter = painterResource(id = data.iconRes),
contentDescription = null,
foregroundBrush = foreground,
modifier = Modifier
.background(color = data.iconColors.background, shape = CircleShape)
.padding(8.dp)
)
Column(
modifier = Modifier
.padding(start = 12.dp)
.weight(1f)
) {
Text(
text = stringResource(id = data.titleRes),
style = MaterialTheme.typography.bodyMedium
)
if (data.progress >= 0f) {
LinearProgressIndicator(
progress = data.progress,
strokeCap = StrokeCap.Round,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp)
)
}
if (data.statusRes != NONE) {
Text(
text = stringResource(id = data.statusRes),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
if (data.actionRes != NONE) {
Buttons.Small(
onClick = onActionClick,
modifier = Modifier.padding(start = 8.dp)
) {
Text(text = stringResource(id = data.actionRes))
}
}
}
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun BackupStatusPreview() {
Previews.Preview {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
BackupStatus(
data = BackupStatusData.CouldNotCompleteBackup
)
BackupStatus(
data = BackupStatusData.NotEnoughFreeSpace
)
BackupStatus(
data = BackupStatusData.RestoringMedia(50, 100)
)
}
}
}
/**
* Sealed interface describing status data to display in BackupStatus widget.
*
* TODO [message-requests] - Finalize assets and text
*/
sealed interface BackupStatusData {
@get:DrawableRes
val iconRes: Int
@get:StringRes
val titleRes: Int
val iconColors: BackupsIconColors
@get:StringRes
val actionRes: Int get() = NONE
@get:StringRes
val statusRes: Int get() = NONE
val progress: Float get() = NONE.toFloat()
/**
* Generic failure
*/
object CouldNotCompleteBackup : BackupStatusData {
override val iconRes: Int = R.drawable.symbol_backup_light
override val titleRes: Int = R.string.default_error_msg
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
}
/**
* User does not have enough space on their device to complete backup restoration
*/
object NotEnoughFreeSpace : BackupStatusData {
override val iconRes: Int = R.drawable.symbol_backup_light
override val titleRes: Int = R.string.default_error_msg
override val iconColors: BackupsIconColors = BackupsIconColors.Warning
override val actionRes: Int = R.string.registration_activity__skip
}
/**
* Restoring media, finished, and paused states.
*/
data class RestoringMedia(
val bytesDownloaded: Long,
val bytesTotal: Long,
val status: Status = Status.NONE
) : BackupStatusData {
override val iconRes: Int = R.drawable.symbol_backup_light
override val iconColors: BackupsIconColors = BackupsIconColors.Normal
override val titleRes: Int = when (status) {
Status.NONE -> R.string.default_error_msg
Status.LOW_BATTERY -> R.string.default_error_msg
Status.WAITING_FOR_INTERNET -> R.string.default_error_msg
Status.WAITING_FOR_WIFI -> R.string.default_error_msg
Status.FINISHED -> R.string.default_error_msg
}
override val statusRes: Int = when (status) {
Status.NONE -> R.string.default_error_msg
Status.LOW_BATTERY -> R.string.default_error_msg
Status.WAITING_FOR_INTERNET -> R.string.default_error_msg
Status.WAITING_FOR_WIFI -> R.string.default_error_msg
Status.FINISHED -> R.string.default_error_msg
}
override val progress: Float = if (bytesTotal > 0) {
min(1f, max(0f, bytesDownloaded.toFloat() / bytesTotal))
} else {
0f
}
}
/**
* Describes the status of an in-progress media download session.
*/
enum class Status {
NONE,
LOW_BATTERY,
WAITING_FOR_INTERNET,
WAITING_FOR_WIFI,
FINISHED
}
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.view.LayoutInflater
import android.view.ViewGroup

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.Image
import androidx.compose.foundation.background

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.os.Bundle
import androidx.activity.compose.setContent

View File

@@ -3,6 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
class MessageBackupsFlowRepository

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import org.thoughtcrime.securesms.components.settings.app.subscription.donate.gateway.GatewayResponse
import org.thoughtcrime.securesms.keyvalue.SignalStore

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui.subscription
/**
* Describes how often a users messages are backed up.
*/
enum class MessageBackupsFrequency {
DAILY,
WEEKLY,
MONTHLY,
NEVER
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
enum class MessageBackupsScreen {
EDUCATION,

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import android.content.Context
import android.content.Intent
@@ -26,6 +26,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -80,6 +81,15 @@ class MessageBackupsTestRestoreActivity : BaseActivity() {
.fillMaxSize()
.padding(16.dp)
) {
Buttons.LargePrimary(
onClick = this@MessageBackupsTestRestoreActivity::restoreFromServer,
enabled = !state.importState.inProgress
) {
Text("Restore")
}
Spacer(modifier = Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
@@ -120,9 +130,20 @@ class MessageBackupsTestRestoreActivity : BaseActivity() {
}
}
}
if (state.importState == MessageBackupsTestRestoreViewModel.ImportState.RESTORED) {
SideEffect {
RegistrationUtil.maybeMarkRegistrationComplete()
ApplicationDependencies.getJobManager().add(ProfileUploadJob())
startActivity(MainActivity.clearTop(this))
}
}
}
}
private fun restoreFromServer() {
viewModel.restore()
}
private fun continueRegistration() {
if (Recipient.self().profileName.isEmpty || !AvatarHelper.hasAvatar(this, Recipient.self().id)) {
val main = MainActivity.clearTop(this)

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
@@ -17,8 +17,13 @@ import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.BackupRestoreJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.jobs.SyncArchivedMediaJob
import org.thoughtcrime.securesms.recipients.Recipient
import java.io.InputStream
import kotlin.time.Duration.Companion.seconds
class MessageBackupsTestRestoreViewModel : ViewModel() {
val disposables = CompositeDisposable()
@@ -40,6 +45,23 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
}
}
fun restore() {
_state.value = _state.value.copy(importState = ImportState.IN_PROGRESS)
disposables += Single.fromCallable {
ApplicationDependencies
.getJobManager()
.startChain(BackupRestoreJob())
.then(SyncArchivedMediaJob())
.then(BackupRestoreMediaJob())
.enqueueAndBlockUntilCompletion(120.seconds.inWholeMilliseconds)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
_state.value = _state.value.copy(importState = ImportState.RESTORED)
}
}
fun onPlaintextToggled() {
_state.value = _state.value.copy(plaintext = !_state.value.plaintext)
}
@@ -54,6 +76,6 @@ class MessageBackupsTestRestoreViewModel : ViewModel() {
)
enum class ImportState(val inProgress: Boolean = false) {
NONE, IN_PROGRESS(true)
NONE, IN_PROGRESS(true), RESTORED
}
}

View File

@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -18,6 +18,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.R
/**
* Represents a "Feature" included for a specify tier of message backups
@@ -53,3 +56,16 @@ fun MessageBackupsTypeFeatureRow(
)
}
}
@SignalPreview
@Composable
private fun MessageBackupsTypeFeatureRowPreview() {
Previews.Preview {
MessageBackupsTypeFeatureRow(
messageBackupsTypeFeature = MessageBackupsTypeFeature(
iconResourceId = R.drawable.symbol_edit_24,
label = "Content Label"
)
)
}
}

View File

@@ -2,7 +2,7 @@
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2.ui
package org.thoughtcrime.securesms.backup.v2.ui.subscription
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
@@ -21,6 +21,7 @@ import androidx.compose.foundation.text.ClickableText
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -269,6 +270,7 @@ private fun formatCostPerMonth(pricePerMonth: FiatMoney): String {
}
}
@Stable
data class MessageBackupsType(
val pricePerMonth: FiatMoney,
val title: String,

View File

@@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadgeAmounts
import org.thoughtcrime.securesms.components.settings.app.subscription.getGiftBadges
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.whispersystems.signalservice.internal.push.DonationsConfiguration
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.util.Currency
import java.util.Locale
@@ -28,7 +28,7 @@ class GiftFlowRepository {
.getDonationsConfiguration(Locale.getDefault())
}
.flatMap { it.flattenResult() }
.map { DonationsConfiguration.GIFT_LEVEL to it.getGiftBadges().first() }
.map { SubscriptionsConfiguration.GIFT_LEVEL to it.getGiftBadges().first() }
.subscribeOn(Schedulers.io())
}

View File

@@ -24,10 +24,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.app.ShareCompat
@@ -128,19 +127,19 @@ class CreateCallLinkBottomSheetDialogFragment : ComposeBottomSheetDialogFragment
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal),
icon = ImageVector.vectorResource(id = R.drawable.symbol_forward_24),
icon = painterResource(id = R.drawable.symbol_forward_24),
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareViaSignalClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_copy_android_24),
icon = painterResource(id = R.drawable.symbol_copy_android_24),
onClick = this@CreateCallLinkBottomSheetDialogFragment::onCopyLinkClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_share_android_24),
icon = painterResource(id = R.drawable.symbol_share_android_24),
onClick = this@CreateCallLinkBottomSheetDialogFragment::onShareLinkClicked
)

View File

@@ -17,10 +17,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.app.ActivityCompat
@@ -287,25 +285,25 @@ private fun CallLinkDetails(
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__share_link_via_signal),
icon = ImageVector.vectorResource(id = R.drawable.symbol_forward_24),
icon = painterResource(id = R.drawable.symbol_forward_24),
onClick = callback::onShareLinkViaSignalClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CreateCallLinkBottomSheetDialogFragment__copy_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_copy_android_24),
icon = painterResource(id = R.drawable.symbol_copy_android_24),
onClick = callback::onCopyClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_link_24),
icon = painterResource(id = R.drawable.symbol_link_24),
onClick = callback::onShareClicked
)
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__delete_call_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_trash_24),
icon = painterResource(id = R.drawable.symbol_trash_24),
foregroundTint = MaterialTheme.colorScheme.error,
onClick = callback::onDeleteClicked
)

View File

@@ -305,7 +305,7 @@ class CallLogAdapter(
val color = ContextCompat.getColor(
context,
if (call.record.event.isMissedCall()) {
if (call.record.isDisplayedAsMissedCallInUi) {
R.color.signal_colorError
} else {
R.color.signal_colorOnSurfaceVariant
@@ -371,11 +371,11 @@ class CallLogAdapter(
private fun getCallStateDrawableRes(call: CallTable.Call): Int {
return when (call.messageType) {
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_compact_16
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_compact_16
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) R.drawable.symbol_missed_incoming_compact_16 else R.drawable.symbol_arrow_downleft_compact_16
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_compact_16
MessageTypes.GROUP_CALL_TYPE -> when {
call.type == CallTable.Type.AD_HOC_CALL -> R.drawable.symbol_link_compact_16
call.event.isMissedCall() -> R.drawable.symbol_missed_incoming_compact_16
call.isDisplayedAsMissedCallInUi -> R.drawable.symbol_missed_incoming_compact_16
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_compact_16
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_compact_16
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_compact_16
@@ -389,23 +389,19 @@ class CallLogAdapter(
@StringRes
private fun getCallStateStringRes(call: CallTable.Call): Int {
return when (call.messageType) {
MessageTypes.MISSED_VIDEO_CALL_TYPE,
MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__missed_notification_profile
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__incoming
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__incoming
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__missed_notification_profile
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.CallLogAdapter__outgoing
MessageTypes.GROUP_CALL_TYPE -> when {
call.type == CallTable.Type.AD_HOC_CALL -> R.string.CallLogAdapter__call_link
call.event == CallTable.Event.MISSED -> R.string.CallLogAdapter__missed
call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallLogAdapter__missed_notification_profile
call.isDisplayedAsMissedCallInUi -> R.string.CallLogAdapter__missed
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
call.direction == CallTable.Direction.INCOMING -> R.string.CallLogAdapter__incoming
call.direction == CallTable.Direction.OUTGOING -> R.string.CallLogAdapter__outgoing
else -> throw AssertionError()
}
else -> error("Unexpected type ${call.messageType}")
else -> if (call.isDisplayedAsMissedCallInUi) R.string.CallLogAdapter__missed else R.string.CallLogAdapter__incoming
}
}
}

View File

@@ -53,7 +53,7 @@ class NewCallActivity : ContactSelectionActivity(), ContactSelectionListFragment
when (result) {
is RecipientRepository.LookupResult.Success -> {
val resolved = Recipient.resolved(result.recipientId)
if (resolved.isRegistered && resolved.hasServiceId()) {
if (resolved.isRegistered && resolved.hasServiceId) {
launch(resolved)
}
}

View File

@@ -35,12 +35,6 @@ public class AlertView extends AppCompatImageView {
setVisibility(View.GONE);
}
public void setPendingApproval() {
setVisibility(View.VISIBLE);
setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurfaceVariant));
setContentDescription(getContext().getString(R.string.conversation_item_sent__pending_approval_description));
}
public void setFailed() {
setVisibility(View.VISIBLE);
setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_colorError));

View File

@@ -202,6 +202,7 @@ public final class AudioView extends FrameLayout {
} else if (showControls && audio.getTransferState() == AttachmentTable.TRANSFER_PROGRESS_STARTED) {
controlToggle.displayQuick(progressAndPlay);
seekBar.setEnabled(false);
showPlayButton();
if (circleProgress != null) {
circleProgress.setVisibility(View.VISIBLE);
circleProgress.spin();

View File

@@ -176,7 +176,7 @@ public final class AvatarImageView extends AppCompatImageView {
new ProfileContactPhoto(Recipient.self()))
: new RecipientContactPhoto(recipient);
boolean shouldBlur = recipient.shouldBlurAvatar();
boolean shouldBlur = recipient.getShouldBlurAvatar();
ChatColors chatColors = recipient.getChatColors();
if (!photo.equals(recipientContactPhoto) || shouldBlur != blurred || !Objects.equals(chatColors, this.chatColors)) {

View File

@@ -315,8 +315,6 @@ public class ConversationItemFooter extends ConstraintLayout {
}
dateView.setText(errorMsg);
} else if (messageRecord.isPendingInsecureSmsFallback()) {
dateView.setText(R.string.ConversationItem_click_to_approve_unencrypted);
} else if (messageRecord.isRateLimited()) {
dateView.setText(R.string.ConversationItem_send_paused);
} else if (MessageRecordUtil.isScheduled(messageRecord)) {
@@ -410,7 +408,7 @@ public class ConversationItemFooter extends ConstraintLayout {
previousMessageId = newMessageId;
if (messageRecord.isFailed() || messageRecord.isPendingInsecureSmsFallback() || MessageRecordUtil.isScheduled(messageRecord)) {
if (messageRecord.isFailed() || MessageRecordUtil.isScheduled(messageRecord)) {
deliveryStatusView.setNone();
return;
}

View File

@@ -22,6 +22,7 @@ public class ConversationSearchBottomBar extends ConstraintLayout {
private View searchUp;
private TextView searchPositionText;
private View progressWheel;
private View jumpToDateButton;
private EventListener eventListener;
@@ -42,6 +43,7 @@ public class ConversationSearchBottomBar extends ConstraintLayout {
this.searchDown = findViewById(R.id.conversation_search_down);
this.searchPositionText = findViewById(R.id.conversation_search_position);
this.progressWheel = findViewById(R.id.conversation_search_progress_wheel);
this.jumpToDateButton = findViewById(R.id.conversation_jump_to_date_button);
}
public void setData(int position, int count) {
@@ -65,6 +67,12 @@ public class ConversationSearchBottomBar extends ConstraintLayout {
searchPositionText.setText(R.string.ConversationActivity_no_results);
}
jumpToDateButton.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onDatePickerSelected();
}
});
setViewEnabled(searchUp, position < (count - 1));
setViewEnabled(searchDown, position > 0);
}
@@ -85,5 +93,6 @@ public class ConversationSearchBottomBar extends ConstraintLayout {
public interface EventListener {
void onSearchMoveUpPressed();
void onSearchMoveDownPressed();
void onDatePickerSelected();
}
}

View File

@@ -40,9 +40,15 @@ public class FromTextView extends SimpleEmojiTextView {
}
public void setText(Recipient recipient, @Nullable CharSequence fromString, @Nullable CharSequence suffix, boolean asThread) {
setText(recipient, fromString, suffix, asThread, false);
}
public void setText(Recipient recipient, @Nullable CharSequence fromString, @Nullable CharSequence suffix, boolean asThread, boolean showSelfAsYou) {
SpannableStringBuilder builder = new SpannableStringBuilder();
if (asThread && recipient.isSelf()) {
if (asThread && recipient.isSelf() && showSelfAsYou) {
builder.append(getContext().getString(R.string.Recipient_you));
} else if (asThread && recipient.isSelf()) {
builder.append(getContext().getString(R.string.note_to_self));
} else {
builder.append(fromString);
@@ -52,7 +58,7 @@ public class FromTextView extends SimpleEmojiTextView {
builder.append(suffix);
}
if (asThread && recipient.showVerified()) {
if (asThread && recipient.getShowVerified()) {
Drawable official = ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_20);
official.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20));

View File

@@ -191,10 +191,10 @@ class ScrollToPositionDelegate private constructor(
if (abs(layoutManager.findFirstVisibleItemPosition() - position) < SCROLL_ANIMATION_THRESHOLD) {
val child: View? = layoutManager.findViewByPosition(position)
if (child == null || !layoutManager.isViewPartiallyVisible(child, true, false)) {
layoutManager.scrollToPositionWithOffset(position, recyclerView.height / 4)
layoutManager.scrollToPositionWithOffset(position, recyclerView.height / 3)
}
} else {
layoutManager.scrollToPositionWithOffset(position, recyclerView.height / 4)
layoutManager.scrollToPositionWithOffset(position, recyclerView.height / 3)
}
}
}

View File

@@ -59,7 +59,7 @@ class VerificationCodeView @JvmOverloads constructor(context: Context, attrs: At
}
}
interface OnCodeEnteredListener {
fun interface OnCodeEnteredListener {
fun onCodeComplete(code: String)
}

View File

@@ -215,15 +215,15 @@ class ChangeNumberRepository(
@WorkerThread
fun changeLocalNumber(e164: String, pni: PNI): Single<Unit> {
val oldStorageId: ByteArray? = Recipient.self().storageServiceId
val oldStorageId: ByteArray? = Recipient.self().storageId
SignalDatabase.recipients.updateSelfE164(e164, pni)
val newStorageId: ByteArray? = Recipient.self().storageServiceId
val newStorageId: ByteArray? = Recipient.self().storageId
if (e164 != SignalStore.account().requireE164() && MessageDigest.isEqual(oldStorageId, newStorageId)) {
Log.w(TAG, "Self storage id was not rotated, attempting to rotate again")
SignalDatabase.recipients.rotateStorageId(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
val secondAttemptStorageId: ByteArray? = Recipient.self().storageServiceId
val secondAttemptStorageId: ByteArray? = Recipient.self().storageId
if (MessageDigest.isEqual(oldStorageId, secondAttemptStorageId)) {
Log.w(TAG, "Second attempt also failed to rotate storage id")
}

View File

@@ -1,12 +1,15 @@
package org.thoughtcrime.securesms.components.settings.app.chats
import android.content.Intent
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.Navigation
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowActivity
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
@@ -81,9 +84,23 @@ class ChatsSettingsFragment : DSLSettingsFragment(R.string.preferences_chats__ch
sectionHeaderPref(R.string.preferences_chats__backups)
if (FeatureFlags.messageBackups() || state.remoteBackupsEnabled) {
clickPref(
title = DSLSettingsText.from("Signal Backups"), // TODO [message-backups] -- Finalized copy
summary = DSLSettingsText.from(if (state.remoteBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
onClick = {
if (state.remoteBackupsEnabled) {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_remoteBackupsSettingsFragment)
} else {
startActivity(Intent(requireContext(), MessageBackupsFlowActivity::class.java))
}
}
)
}
clickPref(
title = DSLSettingsText.from(R.string.preferences_chats__chat_backups),
summary = DSLSettingsText.from(if (state.chatBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
summary = DSLSettingsText.from(if (state.localBackupsEnabled) R.string.arrays__enabled else R.string.arrays__disabled),
onClick = {
Navigation.findNavController(requireView()).safeNavigate(R.id.action_chatsSettingsFragment_to_backupsPreferenceFragment)
}

View File

@@ -6,5 +6,6 @@ data class ChatsSettingsState(
val keepMutedChatsArchived: Boolean,
val useSystemEmoji: Boolean,
val enterKeySends: Boolean,
val chatBackupsEnabled: Boolean
val localBackupsEnabled: Boolean,
val remoteBackupsEnabled: Boolean
)

View File

@@ -22,7 +22,8 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
keepMutedChatsArchived = SignalStore.settings().shouldKeepMutedChatsArchived(),
useSystemEmoji = SignalStore.settings().isPreferSystemEmoji,
enterKeySends = SignalStore.settings().isEnterKeySends,
chatBackupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication())
localBackupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication()),
remoteBackupsEnabled = SignalStore.backup().areBackupsEnabled
)
)
@@ -59,8 +60,8 @@ class ChatsSettingsViewModel @JvmOverloads constructor(
fun refresh() {
val backupsEnabled = SignalStore.settings().isBackupEnabled && BackupUtil.canUserAccessBackupDirectory(ApplicationDependencies.getApplication())
if (store.state.chatBackupsEnabled != backupsEnabled) {
store.update { it.copy(chatBackupsEnabled = backupsEnabled) }
if (store.state.localBackupsEnabled != backupsEnabled) {
store.update { it.copy(localBackupsEnabled = backupsEnabled) }
}
}
}

View File

@@ -0,0 +1,570 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.chats.backups
import android.content.Intent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
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.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.fragment.findNavController
import kotlinx.collections.immutable.persistentListOf
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dialogs
import org.signal.core.ui.Dividers
import org.signal.core.ui.Previews
import org.signal.core.ui.Rows
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.Snackbars
import org.signal.core.ui.Texts
import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFlowActivity
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFrequency
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.viewModel
import java.math.BigDecimal
import java.util.Currency
import java.util.Locale
/**
* Remote backups settings fragment.
*
* TODO [message-backups] -- All copy in this file is non-final
*/
class RemoteBackupsSettingsFragment : ComposeFragment() {
private val viewModel by viewModel {
RemoteBackupsSettingsViewModel()
}
@Composable
override fun FragmentContent() {
val state by viewModel.state
val callbacks = remember { Callbacks() }
RemoteBackupsSettingsContent(
messageBackupsType = state.messageBackupsType,
lastBackupTimestamp = state.lastBackupTimestamp,
canBackUpUsingCellular = state.canBackUpUsingCellular,
backupsFrequency = state.backupsFrequency,
requestedDialog = state.dialog,
requestedSnackbar = state.snackbar,
contentCallbacks = callbacks
)
}
@Stable
private inner class Callbacks : ContentCallbacks {
override fun onNavigationClick() {
findNavController().popBackStack()
}
override fun onEnableBackupsClick() {
startActivity(Intent(requireContext(), MessageBackupsFlowActivity::class.java))
}
override fun onBackUpUsingCellularClick(canUseCellular: Boolean) {
viewModel.setCanBackUpUsingCellular(canUseCellular)
}
override fun onViewPaymentHistory() {
// TODO [message-backups] Navigate to payment history
}
override fun onBackupNowClick() {
// TODO [message-backups] Enqueue immediate backup
}
override fun onTurnOffAndDeleteBackupsClick() {
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.TURN_OFF_AND_DELETE_BACKUPS)
}
override fun onChangeBackupFrequencyClick() {
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.BACKUP_FREQUENCY)
}
override fun onDialogDismissed() {
viewModel.requestDialog(RemoteBackupsSettingsState.Dialog.NONE)
}
override fun onSnackbarDismissed() {
viewModel.requestSnackbar(RemoteBackupsSettingsState.Snackbar.NONE)
}
override fun onSelectBackupsFrequencyChange(newFrequency: MessageBackupsFrequency) {
viewModel.setBackupsFrequency(newFrequency)
}
override fun onTurnOffAndDeleteBackupsConfirm() {
viewModel.turnOffAndDeleteBackups()
}
override fun onChangeBackupsTypeClick() {
// TODO - launch flow at appropriate point
}
}
}
/**
* Callback interface for RemoteBackupsSettingsContent composable.
*/
private interface ContentCallbacks {
fun onNavigationClick() = Unit
fun onEnableBackupsClick() = Unit
fun onChangeBackupsTypeClick() = Unit
fun onBackUpUsingCellularClick(canUseCellular: Boolean) = Unit
fun onViewPaymentHistory() = Unit
fun onBackupNowClick() = Unit
fun onTurnOffAndDeleteBackupsClick() = Unit
fun onChangeBackupFrequencyClick() = Unit
fun onDialogDismissed() = Unit
fun onSnackbarDismissed() = Unit
fun onSelectBackupsFrequencyChange(newFrequency: MessageBackupsFrequency) = Unit
fun onTurnOffAndDeleteBackupsConfirm() = Unit
}
@Composable
private fun RemoteBackupsSettingsContent(
messageBackupsType: MessageBackupsType?,
lastBackupTimestamp: Long,
canBackUpUsingCellular: Boolean,
backupsFrequency: MessageBackupsFrequency,
requestedDialog: RemoteBackupsSettingsState.Dialog,
requestedSnackbar: RemoteBackupsSettingsState.Snackbar,
contentCallbacks: ContentCallbacks
) {
val snackbarHostState = remember {
SnackbarHostState()
}
Scaffolds.Settings(
title = "Signal Backups",
onNavigationClick = contentCallbacks::onNavigationClick,
navigationIconPainter = painterResource(id = R.drawable.symbol_arrow_left_24),
snackbarHost = {
Snackbars.Host(snackbarHostState = snackbarHostState)
}
) {
LazyColumn(
modifier = Modifier
.padding(it)
) {
item {
BackupTypeRow(
messageBackupsType = messageBackupsType,
onEnableBackupsClick = contentCallbacks::onEnableBackupsClick,
onChangeBackupsTypeClick = contentCallbacks::onChangeBackupsTypeClick
)
}
if (messageBackupsType == null) {
item {
Rows.TextRow(
text = "Payment history",
onClick = contentCallbacks::onViewPaymentHistory
)
}
} else {
item {
Dividers.Default()
}
item {
Texts.SectionHeader(text = "Backup Details")
}
item {
LastBackupRow(
lastBackupTimestamp = lastBackupTimestamp,
onBackupNowClick = {}
)
}
item {
Rows.TextRow(text = {
Column {
Text(
text = "Backup size",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = "2.3GB",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
})
}
item {
Rows.TextRow(
text = {
Column {
Text(
text = "Backup frequency",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
Text(
text = getTextForFrequency(backupsFrequency = backupsFrequency),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
},
onClick = contentCallbacks::onChangeBackupFrequencyClick
)
}
item {
Rows.ToggleRow(
checked = canBackUpUsingCellular,
text = "Back up using cellular",
onCheckChanged = contentCallbacks::onBackUpUsingCellularClick
)
}
item {
Dividers.Default()
}
item {
Rows.TextRow(
text = "Turn off and delete backup",
foregroundTint = MaterialTheme.colorScheme.error,
onClick = contentCallbacks::onTurnOffAndDeleteBackupsClick
)
}
}
}
}
when (requestedDialog) {
RemoteBackupsSettingsState.Dialog.NONE -> {}
RemoteBackupsSettingsState.Dialog.TURN_OFF_AND_DELETE_BACKUPS -> {
TurnOffAndDeleteBackupsDialog(
onConfirm = contentCallbacks::onTurnOffAndDeleteBackupsConfirm,
onDismiss = contentCallbacks::onDialogDismissed
)
}
RemoteBackupsSettingsState.Dialog.BACKUP_FREQUENCY -> {
BackupFrequencyDialog(
selected = backupsFrequency,
onSelected = contentCallbacks::onSelectBackupsFrequencyChange,
onDismiss = contentCallbacks::onDialogDismissed
)
}
}
LaunchedEffect(requestedSnackbar) {
when (requestedSnackbar) {
RemoteBackupsSettingsState.Snackbar.NONE -> {
snackbarHostState.currentSnackbarData?.dismiss()
}
RemoteBackupsSettingsState.Snackbar.BACKUP_DELETED_AND_TURNED_OFF -> {
snackbarHostState.showSnackbar(
"Backup deleted and turned off"
)
}
RemoteBackupsSettingsState.Snackbar.BACKUP_TYPE_CHANGED_AND_SUBSCRIPTION_CANCELLED -> {
snackbarHostState.showSnackbar(
"Backup type changed and subscription cancelled"
)
}
RemoteBackupsSettingsState.Snackbar.SUBSCRIPTION_CANCELLED -> {
snackbarHostState.showSnackbar(
"Subscription cancelled"
)
}
RemoteBackupsSettingsState.Snackbar.DOWNLOAD_COMPLETE -> {
snackbarHostState.showSnackbar(
"Download complete"
)
}
}
contentCallbacks.onSnackbarDismissed()
}
}
@Composable
private fun BackupTypeRow(
messageBackupsType: MessageBackupsType?,
onEnableBackupsClick: () -> Unit,
onChangeBackupsTypeClick: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = messageBackupsType != null, onClick = onChangeBackupsTypeClick)
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(top = 16.dp, bottom = 14.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Backup type",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
if (messageBackupsType == null) {
Text(
text = "Backups disabled",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
val localResources = LocalContext.current.resources
val formattedCurrency = remember(messageBackupsType.pricePerMonth) {
FiatMoneyUtil.format(localResources, messageBackupsType.pricePerMonth, FiatMoneyUtil.formatOptions().trimZerosAfterDecimal())
}
Text(
text = "${messageBackupsType.title} · $formattedCurrency/month"
)
}
}
if (messageBackupsType == null) {
Buttons.Small(onClick = onEnableBackupsClick) {
Text(text = "Enable backups")
}
}
}
}
@Composable
private fun LastBackupRow(
lastBackupTimestamp: Long,
onBackupNowClick: () -> Unit
) {
Row(
modifier = Modifier
.padding(horizontal = dimensionResource(id = R.dimen.core_ui__gutter))
.padding(top = 16.dp, bottom = 14.dp)
) {
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = "Last backup",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface
)
if (lastBackupTimestamp > 0) {
val context = LocalContext.current
val day = remember(lastBackupTimestamp) {
DateUtils.getDayPrecisionTimeString(context, Locale.getDefault(), lastBackupTimestamp)
}
val time = remember(lastBackupTimestamp) {
DateUtils.getOnlyTimeString(context, lastBackupTimestamp)
}
Text(
text = "$day at $time",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
} else {
Text(
text = "Never",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
Buttons.Small(onClick = onBackupNowClick) {
Text(text = "Back up now")
}
}
}
@Composable
private fun TurnOffAndDeleteBackupsDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit
) {
Dialogs.SimpleAlertDialog(
title = "Turn off and delete backups?",
body = "You will not be charged again. Your backup will be deleted and no new backups will be created.",
confirm = "Turn off and delete",
dismiss = stringResource(id = android.R.string.cancel),
confirmColor = MaterialTheme.colorScheme.error,
onConfirm = onConfirm,
onDismiss = onDismiss
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun BackupFrequencyDialog(
selected: MessageBackupsFrequency,
onSelected: (MessageBackupsFrequency) -> Unit,
onDismiss: () -> Unit
) {
AlertDialog(
onDismissRequest = onDismiss
) {
Column(
modifier = Modifier
.background(
color = AlertDialogDefaults.containerColor,
shape = AlertDialogDefaults.shape
)
.fillMaxWidth()
) {
Text(
text = "Backup frequency",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(24.dp)
)
MessageBackupsFrequency.values().forEach {
Rows.RadioRow(
selected = selected == it,
text = getTextForFrequency(backupsFrequency = it),
label = when (it) {
MessageBackupsFrequency.NEVER -> "By tapping \"Back up now\""
else -> null
},
modifier = Modifier
.padding(end = 24.dp)
.clickable(onClick = {
onSelected(it)
onDismiss()
})
)
}
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(bottom = 24.dp)
) {
TextButton(onClick = onDismiss) {
Text(text = stringResource(id = android.R.string.cancel))
}
}
}
}
}
@Composable
private fun getTextForFrequency(backupsFrequency: MessageBackupsFrequency): String {
return when (backupsFrequency) {
MessageBackupsFrequency.DAILY -> "Daily"
MessageBackupsFrequency.WEEKLY -> "Weekly"
MessageBackupsFrequency.MONTHLY -> "Monthly"
MessageBackupsFrequency.NEVER -> "Manually back up"
}
}
@SignalPreview
@Composable
private fun RemoteBackupsSettingsContentPreview() {
Previews.Preview {
RemoteBackupsSettingsContent(
messageBackupsType = null,
lastBackupTimestamp = -1,
canBackUpUsingCellular = false,
backupsFrequency = MessageBackupsFrequency.NEVER,
requestedDialog = RemoteBackupsSettingsState.Dialog.NONE,
requestedSnackbar = RemoteBackupsSettingsState.Snackbar.NONE,
contentCallbacks = object : ContentCallbacks {}
)
}
}
@SignalPreview
@Composable
private fun BackupTypeRowPreview() {
Previews.Preview {
BackupTypeRow(
messageBackupsType = MessageBackupsType(
title = "Text + all media",
pricePerMonth = FiatMoney(BigDecimal.valueOf(3L), Currency.getInstance(Locale.US)),
features = persistentListOf()
),
onChangeBackupsTypeClick = {},
onEnableBackupsClick = {}
)
}
}
@SignalPreview
@Composable
private fun LastBackupRowPreview() {
Previews.Preview {
LastBackupRow(
lastBackupTimestamp = -1,
onBackupNowClick = {}
)
}
}
@SignalPreview
@Composable
private fun TurnOffAndDeleteBackupsDialogPreview() {
Previews.Preview {
TurnOffAndDeleteBackupsDialog(
onConfirm = {},
onDismiss = {}
)
}
}
@SignalPreview
@Composable
private fun BackupFrequencyDialogPreview() {
Previews.Preview {
BackupFrequencyDialog(
selected = MessageBackupsFrequency.DAILY,
onSelected = {},
onDismiss = {}
)
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.chats.backups
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFrequency
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
data class RemoteBackupsSettingsState(
val messageBackupsType: MessageBackupsType? = null,
val canBackUpUsingCellular: Boolean = false,
val backupSize: Long = 0,
val backupsFrequency: MessageBackupsFrequency = MessageBackupsFrequency.DAILY,
val lastBackupTimestamp: Long = 0,
val dialog: Dialog = Dialog.NONE,
val snackbar: Snackbar = Snackbar.NONE
) {
enum class Dialog {
NONE,
TURN_OFF_AND_DELETE_BACKUPS,
BACKUP_FREQUENCY
}
enum class Snackbar {
NONE,
BACKUP_DELETED_AND_TURNED_OFF,
BACKUP_TYPE_CHANGED_AND_SUBSCRIPTION_CANCELLED,
SUBSCRIPTION_CANCELLED,
DOWNLOAD_COMPLETE
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.chats.backups
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsFrequency
/**
* ViewModel for state management of RemoteBackupsSettingsFragment
*/
class RemoteBackupsSettingsViewModel : ViewModel() {
private val internalState = mutableStateOf(RemoteBackupsSettingsState())
val state: State<RemoteBackupsSettingsState> = internalState
fun setCanBackUpUsingCellular(canBackUpUsingCellular: Boolean) {
// TODO [message-backups] -- Update via repository?
internalState.value = state.value.copy(canBackUpUsingCellular = canBackUpUsingCellular)
}
fun setBackupsFrequency(backupsFrequency: MessageBackupsFrequency) {
// TODO [message-backups] -- Update via repository?
internalState.value = state.value.copy(backupsFrequency = backupsFrequency)
}
fun requestDialog(dialog: RemoteBackupsSettingsState.Dialog) {
internalState.value = state.value.copy(dialog = dialog)
}
fun requestSnackbar(snackbar: RemoteBackupsSettingsState.Snackbar) {
internalState.value = state.value.copy(snackbar = snackbar)
}
fun turnOffAndDeleteBackups() {
// TODO [message-backups] -- Delete.
internalState.value = state.value.copy(snackbar = RemoteBackupsSettingsState.Snackbar.BACKUP_DELETED_AND_TURNED_OFF)
}
}

View File

@@ -29,6 +29,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHostState
@@ -37,6 +42,8 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -46,10 +53,12 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dividers
import org.signal.core.ui.Snackbars
@@ -57,10 +66,13 @@ import org.signal.core.ui.theme.SignalTheme
import org.signal.core.util.bytes
import org.signal.core.util.getLength
import org.signal.core.util.roundedString
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupUploadState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.keyvalue.SignalStore
class InternalBackupPlaygroundFragment : ComposeFragment() {
@@ -114,6 +126,8 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
}
Tabs(
onBack = { findNavController().popBackStack() },
onDeleteAllArchivedMedia = { viewModel.deleteAllArchivedMedia() },
mainContent = {
Screen(
state = state,
@@ -149,25 +163,32 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
}
validateFileLauncher.launch(intent)
}
},
onTriggerBackupJobClicked = { viewModel.triggerBackupJob() },
onRestoreFromRemoteClicked = { viewModel.restoreFromRemote() }
)
},
mediaContent = { snackbarHostState ->
MediaList(
enabled = SignalStore.backup().canReadWriteToArchiveCdn,
state = mediaState,
snackbarHostState = snackbarHostState,
backupAttachmentMedia = { viewModel.backupAttachmentMedia(it) },
deleteBackupAttachmentMedia = { viewModel.deleteBackupAttachmentMedia(it) },
batchBackupAttachmentMedia = { viewModel.backupAttachmentMedia(it) },
batchDeleteBackupAttachmentMedia = { viewModel.deleteBackupAttachmentMedia(it) }
archiveAttachmentMedia = { viewModel.archiveAttachmentMedia(it) },
deleteArchivedMedia = { viewModel.deleteArchivedMedia(it) },
batchArchiveAttachmentMedia = { viewModel.archiveAttachmentMedia(it) },
batchDeleteBackupAttachmentMedia = { viewModel.deleteArchivedMedia(it) },
restoreArchivedMedia = { viewModel.restoreArchivedMedia(it) }
)
}
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Tabs(
onBack: () -> Unit,
onDeleteAllArchivedMedia: () -> Unit,
mainContent: @Composable () -> Unit,
mediaContent: @Composable (snackbarHostState: SnackbarHostState) -> Unit
) {
@@ -179,13 +200,36 @@ fun Tabs(
Scaffold(
snackbarHost = { Snackbars.Host(snackbarHostState) },
topBar = {
TabRow(selectedTabIndex = tabIndex) {
tabs.forEachIndexed { index, tab ->
Tab(
text = { Text(tab) },
selected = index == tabIndex,
onClick = { tabIndex = index }
)
Column {
TopAppBar(
title = {
Text("Backup Playground")
},
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(R.drawable.symbol_arrow_left_24),
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null
)
}
},
actions = {
if (tabIndex == 1 && SignalStore.backup().canReadWriteToArchiveCdn) {
TextButton(onClick = onDeleteAllArchivedMedia) {
Text(text = "Delete All")
}
}
}
)
TabRow(selectedTabIndex = tabIndex) {
tabs.forEachIndexed { index, tab ->
Tab(
text = { Text(tab) },
selected = index == tabIndex,
onClick = { tabIndex = index }
)
}
}
}
}
@@ -209,7 +253,9 @@ fun Screen(
onSaveToDiskClicked: () -> Unit = {},
onValidateFileClicked: () -> Unit = {},
onUploadToRemoteClicked: () -> Unit = {},
onCheckRemoteBackupStateClicked: () -> Unit = {}
onCheckRemoteBackupStateClicked: () -> Unit = {},
onTriggerBackupJobClicked: () -> Unit = {},
onRestoreFromRemoteClicked: () -> Unit = {}
) {
Surface {
Column(
@@ -239,6 +285,13 @@ fun Screen(
Text("Export")
}
Buttons.LargePrimary(
onClick = onTriggerBackupJobClicked,
enabled = !state.backupState.inProgress
) {
Text("Trigger Backup Job")
}
Dividers.Default()
Buttons.LargeTonal(
@@ -280,6 +333,10 @@ fun Screen(
}
}
BackupState.BACKUP_JOB_DONE -> {
StateLabel("Backup complete and uploaded")
}
BackupState.IMPORT_IN_PROGRESS -> {
StateLabel("Import in progress...")
}
@@ -324,6 +381,10 @@ fun Screen(
Spacer(modifier = Modifier.height(8.dp))
Buttons.LargePrimary(onClick = onRestoreFromRemoteClicked) {
Text("Restore from remote")
}
when (state.uploadState) {
BackupUploadState.NONE -> {
StateLabel("")
@@ -357,13 +418,24 @@ private fun StateLabel(text: String) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MediaList(
enabled: Boolean,
state: InternalBackupPlaygroundViewModel.MediaState,
snackbarHostState: SnackbarHostState,
backupAttachmentMedia: (InternalBackupPlaygroundViewModel.BackupAttachment) -> Unit,
deleteBackupAttachmentMedia: (InternalBackupPlaygroundViewModel.BackupAttachment) -> Unit,
batchBackupAttachmentMedia: (Set<String>) -> Unit,
batchDeleteBackupAttachmentMedia: (Set<String>) -> Unit
archiveAttachmentMedia: (InternalBackupPlaygroundViewModel.BackupAttachment) -> Unit,
deleteArchivedMedia: (InternalBackupPlaygroundViewModel.BackupAttachment) -> Unit,
batchArchiveAttachmentMedia: (Set<AttachmentId>) -> Unit,
batchDeleteBackupAttachmentMedia: (Set<AttachmentId>) -> Unit,
restoreArchivedMedia: (InternalBackupPlaygroundViewModel.BackupAttachment) -> Unit
) {
if (!enabled) {
Text(
text = "You do not have read/write to archive cdn enabled via SignalStore.backup()",
modifier = Modifier
.padding(16.dp)
)
return
}
LaunchedEffect(state.error?.id) {
state.error?.let {
snackbarHostState.showSnackbar(it.errorText)
@@ -384,51 +456,88 @@ fun MediaList(
.combinedClickable(
onClick = {
if (selectionState.selecting) {
selectionState = selectionState.copy(selected = if (selectionState.selected.contains(attachment.mediaId)) selectionState.selected - attachment.mediaId else selectionState.selected + attachment.mediaId)
selectionState = selectionState.copy(selected = if (selectionState.selected.contains(attachment.id)) selectionState.selected - attachment.id else selectionState.selected + attachment.id)
}
},
onLongClick = {
selectionState = if (selectionState.selecting) MediaMultiSelectState() else MediaMultiSelectState(selecting = true, selected = setOf(attachment.mediaId))
selectionState = if (selectionState.selecting) MediaMultiSelectState() else MediaMultiSelectState(selecting = true, selected = setOf(attachment.id))
}
)
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
if (selectionState.selecting) {
Checkbox(
checked = selectionState.selected.contains(attachment.mediaId),
checked = selectionState.selected.contains(attachment.id),
onCheckedChange = { selected ->
selectionState = selectionState.copy(selected = if (selected) selectionState.selected + attachment.mediaId else selectionState.selected - attachment.mediaId)
selectionState = selectionState.copy(selected = if (selected) selectionState.selected + attachment.id else selectionState.selected - attachment.id)
}
)
}
Column(modifier = Modifier.weight(1f, true)) {
Text(text = "Attachment ${attachment.title}")
Text(text = attachment.title)
Text(text = "State: ${attachment.state}")
}
if (attachment.state == InternalBackupPlaygroundViewModel.BackupAttachment.State.INIT ||
attachment.state == InternalBackupPlaygroundViewModel.BackupAttachment.State.IN_PROGRESS
) {
if (attachment.state == InternalBackupPlaygroundViewModel.BackupAttachment.State.IN_PROGRESS) {
CircularProgressIndicator()
} else {
Button(
enabled = !selectionState.selecting,
onClick = {
when (attachment.state) {
InternalBackupPlaygroundViewModel.BackupAttachment.State.LOCAL_ONLY -> backupAttachmentMedia(attachment)
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED -> deleteBackupAttachmentMedia(attachment)
InternalBackupPlaygroundViewModel.BackupAttachment.State.ATTACHMENT_CDN,
InternalBackupPlaygroundViewModel.BackupAttachment.State.LOCAL_ONLY -> archiveAttachmentMedia(attachment)
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_UNDOWNLOADED,
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_FINAL -> selectionState = selectionState.copy(expandedOption = attachment.dbAttachment.attachmentId)
else -> throw AssertionError("Unsupported state: ${attachment.state}")
}
}
) {
Text(
text = when (attachment.state) {
InternalBackupPlaygroundViewModel.BackupAttachment.State.ATTACHMENT_CDN,
InternalBackupPlaygroundViewModel.BackupAttachment.State.LOCAL_ONLY -> "Backup"
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED -> "Remote Delete"
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_UNDOWNLOADED,
InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_FINAL -> "Options..."
else -> throw AssertionError("Unsupported state: ${attachment.state}")
}
)
DropdownMenu(
expanded = attachment.dbAttachment.attachmentId == selectionState.expandedOption,
onDismissRequest = { selectionState = selectionState.copy(expandedOption = null) }
) {
DropdownMenuItem(
text = { Text("Remote Delete") },
onClick = {
selectionState = selectionState.copy(expandedOption = null)
deleteArchivedMedia(attachment)
}
)
DropdownMenuItem(
text = { Text("Pseudo Restore") },
onClick = {
selectionState = selectionState.copy(expandedOption = null)
restoreArchivedMedia(attachment)
}
)
if (attachment.dbAttachment.dataHash != null && attachment.state == InternalBackupPlaygroundViewModel.BackupAttachment.State.UPLOADED_UNDOWNLOADED) {
DropdownMenuItem(
text = { Text("Re-copy with hash") },
onClick = {
selectionState = selectionState.copy(expandedOption = null)
archiveAttachmentMedia(attachment)
}
)
}
}
}
}
}
@@ -451,7 +560,7 @@ fun MediaList(
Text("Cancel")
}
Button(onClick = {
batchBackupAttachmentMedia(selectionState.selected)
batchArchiveAttachmentMedia(selectionState.selected)
selectionState = MediaMultiSelectState()
}) {
Text("Backup")
@@ -469,7 +578,8 @@ fun MediaList(
private data class MediaMultiSelectState(
val selecting: Boolean = false,
val selected: Set<String> = emptySet()
val selected: Set<AttachmentId> = emptySet(),
val expandedOption: AttachmentId? = null
)
@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO)

View File

@@ -10,30 +10,39 @@ import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.disposables.CompositeDisposable
import io.reactivex.rxjava3.kotlin.plusAssign
import io.reactivex.rxjava3.kotlin.subscribeBy
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.Base64
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
import org.thoughtcrime.securesms.backup.v2.BackupMetadata
import org.thoughtcrime.securesms.backup.v2.BackupRepository
import org.thoughtcrime.securesms.database.MessageType
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.ArchiveAttachmentJob
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob
import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.jobs.BackupRestoreJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
import org.thoughtcrime.securesms.jobs.SyncArchivedMediaJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.NetworkResult
import org.whispersystems.signalservice.api.backup.BackupKey
import org.whispersystems.signalservice.api.backup.MediaName
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.util.UUID
import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds
class InternalBackupPlaygroundViewModel : ViewModel() {
private val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
var backupData: ByteArray? = null
val disposables = CompositeDisposable()
@@ -57,6 +66,17 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
}
}
fun triggerBackupJob() {
_state.value = _state.value.copy(backupState = BackupState.EXPORT_IN_PROGRESS)
disposables += Single.fromCallable { ApplicationDependencies.getJobManager().runSynchronously(BackupMessagesJob(), 120_000) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
_state.value = _state.value.copy(backupState = BackupState.BACKUP_JOB_DONE)
}
}
fun import() {
backupData?.let {
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
@@ -68,7 +88,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
disposables += Single.fromCallable { BackupRepository.import(it.size.toLong(), { ByteArrayInputStream(it) }, selfData, plaintext = plaintext) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { nothing ->
.subscribeBy {
backupData = null
_state.value = _state.value.copy(backupState = BackupState.NONE)
}
@@ -85,7 +105,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
disposables += Single.fromCallable { BackupRepository.import(length, inputStreamFactory, selfData, plaintext = plaintext) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { nothing ->
.subscribeBy {
backupData = null
_state.value = _state.value.copy(backupState = BackupState.NONE)
}
@@ -98,7 +118,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
disposables += Single.fromCallable { BackupRepository.validate(length, inputStreamFactory, selfData) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { nothing ->
.subscribeBy {
backupData = null
_state.value = _state.value.copy(backupState = BackupState.NONE)
}
@@ -142,47 +162,78 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
}
}
fun restoreFromRemote() {
_state.value = _state.value.copy(backupState = BackupState.IMPORT_IN_PROGRESS)
disposables += Single.fromCallable {
ApplicationDependencies
.getJobManager()
.startChain(BackupRestoreJob())
.then(SyncArchivedMediaJob())
.then(BackupRestoreMediaJob())
.enqueueAndBlockUntilCompletion(120.seconds.inWholeMilliseconds)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeBy {
_state.value = _state.value.copy(backupState = BackupState.NONE)
}
}
fun loadMedia() {
disposables += Single
.fromCallable { SignalDatabase.attachments.debugGetLatestAttachments() }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.subscribeBy {
_mediaState.set { update(attachments = it.map { a -> BackupAttachment.from(backupKey, a) }) }
_mediaState.set { update(attachments = it.map { a -> BackupAttachment(dbAttachment = a) }) }
}
}
fun archiveAttachmentMedia(attachments: Set<AttachmentId>) {
disposables += Single
.fromCallable { BackupRepository.debugGetArchivedMediaState() }
.fromCallable {
val toArchive = mediaState.value
.attachments
.filter { attachments.contains(it.dbAttachment.attachmentId) }
.map { it.dbAttachment }
BackupRepository.archiveMedia(toArchive)
}
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.doOnSubscribe { _mediaState.set { update(inProgress = inProgressMediaIds + attachments) } }
.doOnTerminate { _mediaState.set { update(inProgress = inProgressMediaIds - attachments) } }
.subscribeBy { result ->
when (result) {
is NetworkResult.Success -> _mediaState.set { update(archiveStateLoaded = true, backedUpMediaIds = result.result.map { it.mediaId }.toSet()) }
is NetworkResult.Success -> {
loadMedia()
result
.result
.sourceNotFoundResponses
.forEach {
reUploadAndArchiveMedia(result.result.mediaIdToAttachmentId(it.mediaId))
}
}
else -> _mediaState.set { copy(error = MediaStateError(errorText = "$result")) }
}
}
}
fun backupAttachmentMedia(mediaIds: Set<String>) {
disposables += Single.fromCallable { mediaIds.mapNotNull { mediaState.value.idToAttachment[it]?.dbAttachment }.toList() }
.map { BackupRepository.archiveMedia(it) }
fun archiveAttachmentMedia(attachment: BackupAttachment) {
disposables += Single.fromCallable { BackupRepository.archiveMedia(attachment.dbAttachment) }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.doOnSubscribe { _mediaState.set { update(inProgressMediaIds = inProgressMediaIds + mediaIds) } }
.doOnTerminate { _mediaState.set { update(inProgressMediaIds = inProgressMediaIds - mediaIds) } }
.doOnSubscribe { _mediaState.set { update(inProgress = inProgressMediaIds + attachment.dbAttachment.attachmentId) } }
.doOnTerminate { _mediaState.set { update(inProgress = inProgressMediaIds - attachment.dbAttachment.attachmentId) } }
.subscribeBy { result ->
when (result) {
is NetworkResult.Success -> {
val response = result.result
val successes = response.responses.filter { it.status == 200 }
val failures = response.responses - successes.toSet()
_mediaState.set {
var updated = update(backedUpMediaIds = backedUpMediaIds + successes.map { it.mediaId })
if (failures.isNotEmpty()) {
updated = updated.copy(error = MediaStateError(errorText = failures.toString()))
}
updated
is NetworkResult.Success -> loadMedia()
is NetworkResult.StatusCodeError -> {
if (result.code == 410) {
reUploadAndArchiveMedia(attachment.id)
} else {
_mediaState.set { copy(error = MediaStateError(errorText = "$result")) }
}
}
@@ -191,49 +242,107 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
}
}
fun backupAttachmentMedia(attachment: BackupAttachment) {
disposables += Single.fromCallable { BackupRepository.archiveMedia(attachment.dbAttachment) }
private fun reUploadAndArchiveMedia(attachmentId: AttachmentId) {
disposables += Single
.fromCallable {
ApplicationDependencies
.getJobManager()
.startChain(AttachmentUploadJob(attachmentId))
.then(ArchiveAttachmentJob(attachmentId))
.enqueueAndBlockUntilCompletion(15.seconds.inWholeMilliseconds)
}
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.doOnSubscribe { _mediaState.set { update(inProgressMediaIds = inProgressMediaIds + attachment.mediaId) } }
.doOnTerminate { _mediaState.set { update(inProgressMediaIds = inProgressMediaIds - attachment.mediaId) } }
.doOnSubscribe { _mediaState.set { update(inProgress = inProgressMediaIds + attachmentId) } }
.doOnTerminate { _mediaState.set { update(inProgress = inProgressMediaIds - attachmentId) } }
.subscribeBy {
when (it) {
is NetworkResult.Success -> {
_mediaState.set { update(backedUpMediaIds = backedUpMediaIds + attachment.mediaId) }
}
else -> _mediaState.set { copy(error = MediaStateError(errorText = "$it")) }
if (it.isPresent && it.get().isComplete) {
loadMedia()
} else {
_mediaState.set { copy(error = MediaStateError(errorText = "Reupload slow or failed, try again")) }
}
}
}
fun deleteBackupAttachmentMedia(mediaIds: Set<String>) {
deleteBackupAttachmentMedia(mediaIds.mapNotNull { mediaState.value.idToAttachment[it] }.toList())
fun deleteArchivedMedia(attachmentIds: Set<AttachmentId>) {
deleteArchivedMedia(mediaState.value.attachments.filter { attachmentIds.contains(it.dbAttachment.attachmentId) })
}
fun deleteBackupAttachmentMedia(attachment: BackupAttachment) {
deleteBackupAttachmentMedia(listOf(attachment))
fun deleteArchivedMedia(attachment: BackupAttachment) {
deleteArchivedMedia(listOf(attachment))
}
private fun deleteBackupAttachmentMedia(attachments: List<BackupAttachment>) {
val ids = attachments.map { it.mediaId }.toSet()
private fun deleteArchivedMedia(attachments: List<BackupAttachment>) {
val ids = attachments.map { it.dbAttachment.attachmentId }.toSet()
disposables += Single.fromCallable { BackupRepository.deleteArchivedMedia(attachments.map { it.dbAttachment }) }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.doOnSubscribe { _mediaState.set { update(inProgressMediaIds = inProgressMediaIds + ids) } }
.doOnTerminate { _mediaState.set { update(inProgressMediaIds = inProgressMediaIds - ids) } }
.doOnSubscribe { _mediaState.set { update(inProgress = inProgressMediaIds + ids) } }
.doOnTerminate { _mediaState.set { update(inProgress = inProgressMediaIds - ids) } }
.subscribeBy {
when (it) {
is NetworkResult.Success -> {
_mediaState.set { update(backedUpMediaIds = backedUpMediaIds - ids) }
}
is NetworkResult.Success -> loadMedia()
else -> _mediaState.set { copy(error = MediaStateError(errorText = "$it")) }
}
}
}
fun deleteAllArchivedMedia() {
disposables += Single
.fromCallable { BackupRepository.debugDeleteAllArchivedMedia() }
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.subscribeBy { result ->
when (result) {
is NetworkResult.Success -> loadMedia()
else -> _mediaState.set { copy(error = MediaStateError(errorText = "$result")) }
}
}
}
fun restoreArchivedMedia(attachment: BackupAttachment) {
disposables += Completable
.fromCallable {
val recipientId = SignalStore.releaseChannelValues().releaseChannelRecipientId!!
val threadId = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(recipientId))
val message = IncomingMessage(
type = MessageType.NORMAL,
from = recipientId,
sentTimeMillis = System.currentTimeMillis(),
serverTimeMillis = System.currentTimeMillis(),
receivedTimeMillis = System.currentTimeMillis(),
body = "Restored from Archive!?",
serverGuid = UUID.randomUUID().toString()
)
val insertMessage = SignalDatabase.messages.insertMessageInbox(message, threadId).get()
SignalDatabase.attachments.debugCopyAttachmentForArchiveRestore(
insertMessage.messageId,
attachment.dbAttachment
)
val archivedAttachment = SignalDatabase.attachments.getAttachmentsForMessage(insertMessage.messageId).first()
ApplicationDependencies.getJobManager().add(
AttachmentDownloadJob(
messageId = insertMessage.messageId,
attachmentId = archivedAttachment.attachmentId,
manual = false,
forceArchiveDownload = true
)
)
}
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.single())
.subscribeBy(
onError = {
_mediaState.set { copy(error = MediaStateError(errorText = "$it")) }
}
)
}
override fun onCleared() {
disposables.clear()
}
@@ -246,7 +355,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
)
enum class BackupState(val inProgress: Boolean = false) {
NONE, EXPORT_IN_PROGRESS(true), EXPORT_DONE, IMPORT_IN_PROGRESS(true)
NONE, EXPORT_IN_PROGRESS(true), EXPORT_DONE, BACKUP_JOB_DONE, IMPORT_IN_PROGRESS(true)
}
enum class BackupUploadState(val inProgress: Boolean = false) {
@@ -261,67 +370,59 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
}
data class MediaState(
val backupStateLoaded: Boolean = false,
val attachments: List<BackupAttachment> = emptyList(),
val backedUpMediaIds: Set<String> = emptySet(),
val inProgressMediaIds: Set<String> = emptySet(),
val inProgressMediaIds: Set<AttachmentId> = emptySet(),
val error: MediaStateError? = null
) {
val idToAttachment: Map<String, BackupAttachment> = attachments.associateBy { it.mediaId }
fun update(
archiveStateLoaded: Boolean = this.backupStateLoaded,
attachments: List<BackupAttachment> = this.attachments,
backedUpMediaIds: Set<String> = this.backedUpMediaIds,
inProgressMediaIds: Set<String> = this.inProgressMediaIds
inProgress: Set<AttachmentId> = this.inProgressMediaIds
): MediaState {
val updatedAttachments = if (archiveStateLoaded) {
attachments.map {
val state = if (inProgressMediaIds.contains(it.mediaId)) {
BackupAttachment.State.IN_PROGRESS
} else if (backedUpMediaIds.contains(it.mediaId)) {
BackupAttachment.State.UPLOADED
} else {
BackupAttachment.State.LOCAL_ONLY
}
val backupKey = SignalStore.svr().getOrCreateMasterKey().deriveBackupKey()
it.copy(state = state)
val updatedAttachments = attachments.map {
val state = if (inProgress.contains(it.dbAttachment.attachmentId)) {
BackupAttachment.State.IN_PROGRESS
} else if (it.dbAttachment.archiveMediaName != null) {
if (it.dbAttachment.remoteDigest != null) {
val mediaId = backupKey.deriveMediaId(MediaName(it.dbAttachment.archiveMediaName)).encode()
if (it.dbAttachment.archiveMediaId == mediaId) {
BackupAttachment.State.UPLOADED_FINAL
} else {
BackupAttachment.State.UPLOADED_UNDOWNLOADED
}
} else {
BackupAttachment.State.UPLOADED_UNDOWNLOADED
}
} else if (it.dbAttachment.dataHash == null) {
BackupAttachment.State.ATTACHMENT_CDN
} else {
BackupAttachment.State.LOCAL_ONLY
}
} else {
attachments
it.copy(state = state)
}
return copy(
backupStateLoaded = archiveStateLoaded,
attachments = updatedAttachments,
backedUpMediaIds = backedUpMediaIds
attachments = updatedAttachments
)
}
}
data class BackupAttachment(
val dbAttachment: DatabaseAttachment,
val state: State = State.INIT,
val mediaId: String = Base64.encodeUrlSafeWithPadding(Random.nextBytes(15))
val state: State = State.LOCAL_ONLY
) {
val id: Any = dbAttachment.attachmentId
val id: AttachmentId = dbAttachment.attachmentId
val title: String = dbAttachment.attachmentId.toString()
enum class State {
INIT,
ATTACHMENT_CDN,
LOCAL_ONLY,
UPLOADED,
UPLOADED_UNDOWNLOADED,
UPLOADED_FINAL,
IN_PROGRESS
}
companion object {
fun from(backupKey: BackupKey, dbAttachment: DatabaseAttachment): BackupAttachment {
return BackupAttachment(
dbAttachment = dbAttachment,
mediaId = backupKey.deriveMediaId(Base64.decode(dbAttachment.dataHash!!)).toString()
)
}
}
}
data class MediaStateError(

View File

@@ -4,11 +4,11 @@ import org.signal.core.util.money.FiatMoney
import org.signal.core.util.money.PlatformCurrencyUtil
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.models.Badge
import org.whispersystems.signalservice.internal.push.DonationsConfiguration
import org.whispersystems.signalservice.internal.push.DonationsConfiguration.BOOST_LEVEL
import org.whispersystems.signalservice.internal.push.DonationsConfiguration.GIFT_LEVEL
import org.whispersystems.signalservice.internal.push.DonationsConfiguration.LevelConfiguration
import org.whispersystems.signalservice.internal.push.DonationsConfiguration.SUBSCRIPTION_LEVELS
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration.BOOST_LEVEL
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration.GIFT_LEVEL
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration.LevelConfiguration
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration.SUBSCRIPTION_LEVELS
import java.math.BigDecimal
import java.util.Currency
@@ -26,7 +26,7 @@ private const val SEPA_DEBIT = "SEPA_DEBIT"
* @param level The subscription level to get amounts for
* @param paymentMethodAvailability Predicate object which checks whether different payment methods are availble.
*/
fun DonationsConfiguration.getSubscriptionAmounts(
fun SubscriptionsConfiguration.getSubscriptionAmounts(
level: Int,
paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability
): Set<FiatMoney> {
@@ -41,7 +41,7 @@ fun DonationsConfiguration.getSubscriptionAmounts(
/**
* Currently, we only support a single gift badge at level GIFT_LEVEL
*/
fun DonationsConfiguration.getGiftBadges(): List<Badge> {
fun SubscriptionsConfiguration.getGiftBadges(): List<Badge> {
val configuration = levels[GIFT_LEVEL]
return listOfNotNull(configuration?.badge?.let { Badges.fromServiceBadge(it) })
}
@@ -49,7 +49,7 @@ fun DonationsConfiguration.getGiftBadges(): List<Badge> {
/**
* Currently, we only support a single gift badge amount per currency
*/
fun DonationsConfiguration.getGiftBadgeAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, FiatMoney> {
fun SubscriptionsConfiguration.getGiftBadgeAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, FiatMoney> {
return getFilteredCurrencies(paymentMethodAvailability).filter {
it.value.oneTime[GIFT_LEVEL]?.isNotEmpty() == true
}.mapKeys {
@@ -62,12 +62,12 @@ fun DonationsConfiguration.getGiftBadgeAmounts(paymentMethodAvailability: Paymen
/**
* Currently, we only support a single boost badge at level BOOST_LEVEL
*/
fun DonationsConfiguration.getBoostBadges(): List<Badge> {
fun SubscriptionsConfiguration.getBoostBadges(): List<Badge> {
val configuration = levels[BOOST_LEVEL]
return listOfNotNull(configuration?.badge?.let { Badges.fromServiceBadge(it) })
}
fun DonationsConfiguration.getBoostAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, List<FiatMoney>> {
fun SubscriptionsConfiguration.getBoostAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, List<FiatMoney>> {
return getFilteredCurrencies(paymentMethodAvailability).filter {
it.value.oneTime[BOOST_LEVEL]?.isNotEmpty() == true
}.mapKeys {
@@ -77,12 +77,12 @@ fun DonationsConfiguration.getBoostAmounts(paymentMethodAvailability: PaymentMet
}
}
fun DonationsConfiguration.getBadge(level: Int): Badge {
fun SubscriptionsConfiguration.getBadge(level: Int): Badge {
require(level == GIFT_LEVEL || level == BOOST_LEVEL || SUBSCRIPTION_LEVELS.contains(level))
return Badges.fromServiceBadge(levels[level]!!.badge)
}
fun DonationsConfiguration.getSubscriptionLevels(): Map<Int, LevelConfiguration> {
fun SubscriptionsConfiguration.getSubscriptionLevels(): Map<Int, LevelConfiguration> {
return levels.filterKeys { SUBSCRIPTION_LEVELS.contains(it) }.toSortedMap()
}
@@ -90,17 +90,17 @@ fun DonationsConfiguration.getSubscriptionLevels(): Map<Int, LevelConfiguration>
* Get a map describing the minimum donation amounts per currency.
* This returns only the currencies available to the user.
*/
fun DonationsConfiguration.getMinimumDonationAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, FiatMoney> {
fun SubscriptionsConfiguration.getMinimumDonationAmounts(paymentMethodAvailability: PaymentMethodAvailability = DefaultPaymentMethodAvailability): Map<Currency, FiatMoney> {
return getFilteredCurrencies(paymentMethodAvailability)
.mapKeys { Currency.getInstance(it.key.uppercase()) }
.mapValues { FiatMoney(it.value.minimum, it.key) }
}
fun DonationsConfiguration.getAvailablePaymentMethods(currencyCode: String): Set<String> {
fun SubscriptionsConfiguration.getAvailablePaymentMethods(currencyCode: String): Set<String> {
return currencies[currencyCode.lowercase()]?.supportedPaymentMethods ?: emptySet()
}
private fun DonationsConfiguration.getFilteredCurrencies(paymentMethodAvailability: PaymentMethodAvailability): Map<String, DonationsConfiguration.CurrencyConfiguration> {
private fun SubscriptionsConfiguration.getFilteredCurrencies(paymentMethodAvailability: PaymentMethodAvailability): Map<String, SubscriptionsConfiguration.CurrencyConfiguration> {
val userPaymentMethods = paymentMethodAvailability.toSet()
val availableCurrencyCodes = PlatformCurrencyUtil.getAvailableCurrencyCodes()
return currencies.filter { (code, config) ->

View File

@@ -5,7 +5,7 @@ import org.signal.core.util.money.FiatMoney
import org.thoughtcrime.securesms.components.settings.app.subscription.getAvailablePaymentMethods
import org.thoughtcrime.securesms.payments.currency.CurrencyUtil
import org.whispersystems.signalservice.api.services.DonationsService
import org.whispersystems.signalservice.internal.push.DonationsConfiguration
import org.whispersystems.signalservice.internal.push.SubscriptionsConfiguration
import java.util.Locale
class GatewaySelectorRepository(
@@ -18,10 +18,10 @@ class GatewaySelectorRepository(
.map { configuration ->
val available = configuration.getAvailablePaymentMethods(currencyCode).map {
when (it) {
DonationsConfiguration.PAYPAL -> listOf(GatewayResponse.Gateway.PAYPAL)
DonationsConfiguration.CARD -> listOf(GatewayResponse.Gateway.CREDIT_CARD, GatewayResponse.Gateway.GOOGLE_PAY)
DonationsConfiguration.SEPA_DEBIT -> listOf(GatewayResponse.Gateway.SEPA_DEBIT)
DonationsConfiguration.IDEAL -> listOf(GatewayResponse.Gateway.IDEAL)
SubscriptionsConfiguration.PAYPAL -> listOf(GatewayResponse.Gateway.PAYPAL)
SubscriptionsConfiguration.CARD -> listOf(GatewayResponse.Gateway.CREDIT_CARD, GatewayResponse.Gateway.GOOGLE_PAY)
SubscriptionsConfiguration.SEPA_DEBIT -> listOf(GatewayResponse.Gateway.SEPA_DEBIT)
SubscriptionsConfiguration.IDEAL -> listOf(GatewayResponse.Gateway.IDEAL)
else -> listOf()
}
}.flatten().toSet()

View File

@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.conversation
import android.Manifest
import android.app.ActivityOptions
import android.content.ActivityNotFoundException
import android.content.Context
@@ -79,6 +80,7 @@ import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository
import org.thoughtcrime.securesms.nicknames.NicknameActivity
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientExporter
@@ -94,7 +96,6 @@ import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ContextUtil
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.ExpirationUtil
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
@@ -198,6 +199,10 @@ class ConversationSettingsFragment : DSLSettingsFragment(
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.action_edit) {
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
@@ -310,7 +315,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
requireContext(),
StoryViewerArgs(
recipientId = state.recipient.id,
isInHiddenStoryMode = state.recipient.shouldHideStory(),
isInHiddenStoryMode = state.recipient.shouldHideStory,
isFromQuote = true
)
)
@@ -416,7 +421,14 @@ class ConversationSettingsFragment : DSLSettingsFragment(
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
.show()
} else {
addToGroupStoryDelegate.addToStory(state.recipient.id)
Permissions.with(this@ConversationSettingsFragment)
.request(Manifest.permission.CAMERA)
.ifNecessary()
.withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.symbol_camera_24)
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
.onAllGranted { addToGroupStoryDelegate.addToStory(state.recipient.id) }
.onAnyDenied { Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show() }
.execute()
}
},
onVideoClick = {
@@ -503,7 +515,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
)
}
if (FeatureFlags.nicknames() && state.recipient.isIndividual && !state.recipient.isSelf) {
if (state.recipient.isIndividual && !state.recipient.isSelf) {
clickPref(
title = DSLSettingsText.from(R.string.NicknameActivity__nickname),
icon = DSLSettingsIcon.from(R.drawable.symbol_edit_24),

View File

@@ -180,7 +180,7 @@ sealed class ConversationSettingsViewModel(
contactLinkState = when {
recipient.isSelf || recipient.isReleaseNotes || recipient.isBlocked -> ContactLinkState.NONE
recipient.isSystemContact -> ContactLinkState.OPEN
recipient.hasE164() && recipient.shouldShowE164() -> ContactLinkState.ADD
recipient.hasE164 && recipient.shouldShowE164 -> ContactLinkState.ADD
else -> ContactLinkState.NONE
}
)

View File

@@ -174,10 +174,10 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
if (recipient.hasAci()) {
if (recipient.hasAci) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireAci().toString())
}
if (recipient.hasPni()) {
if (recipient.hasPni) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requirePni().toString())
}
}
@@ -196,18 +196,18 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
.setPositiveButton(android.R.string.ok) { _, _ ->
SignalDatabase.threads.deleteConversation(SignalDatabase.threads.getThreadIdIfExistsFor(recipient.id))
if (recipient.hasServiceId()) {
if (recipient.hasServiceId) {
SignalDatabase.recipients.debugClearServiceIds(recipient.id)
SignalDatabase.recipients.debugClearProfileData(recipient.id)
}
if (recipient.hasAci()) {
if (recipient.hasAci) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requireAci().toString())
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requirePni(), addressName = recipient.requireAci().toString())
ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requireAci().toString())
}
if (recipient.hasPni()) {
if (recipient.hasPni) {
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requireAci(), addressName = recipient.requirePni().toString())
SignalDatabase.sessions.deleteAllFor(serviceId = SignalStore.account().requirePni(), addressName = recipient.requirePni().toString())
ApplicationDependencies.getProtocolStore().aci().identities().delete(recipient.requirePni().toString())
@@ -252,7 +252,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
if (!recipient.hasE164()) {
if (!recipient.hasE164) {
Toast.makeText(context, "Recipient doesn't have an E164! Can't split.", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
@@ -325,7 +325,6 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
return if (capabilities != null) {
TextUtils.concat(
colorize("PNP/PNI", capabilities.pnpCapability),
colorize("PaymentActivation", capabilities.paymentActivation)
)
} else {

View File

@@ -75,8 +75,7 @@ object AvatarPreference {
}
private class AvatarPreferenceFallbackPhotoProvider : Recipient.FallbackPhotoProvider() {
override fun getPhotoForGroup(): FallbackContactPhoto {
return FallbackPhoto(R.drawable.ic_group_outline_40, ViewUtil.dpToPx(8))
}
override val photoForGroup: FallbackContactPhoto
get() = FallbackPhoto(R.drawable.ic_group_outline_40, ViewUtil.dpToPx(8))
}
}

View File

@@ -48,12 +48,12 @@ object BioTextPreference {
recipient.getDisplayName(context)
}
if (!recipient.showVerified() && !recipient.isIndividual) {
if (!recipient.showVerified && !recipient.isIndividual) {
return name
}
return SpannableStringBuilder(name).apply {
if (recipient.showVerified()) {
if (recipient.showVerified) {
SpanUtil.appendSpacer(this, 8)
SpanUtil.appendCenteredImageSpanWithoutSpace(this, ContextUtil.requireDrawable(context, R.drawable.ic_official_28), 28, 28)
} else if (recipient.isSystemContact) {

View File

@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.settings.conversation.preferences
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.MessageTypes
@@ -46,10 +47,10 @@ object CallPreference {
private fun getCallIcon(call: CallTable.Call): Int {
return when (call.messageType) {
MessageTypes.MISSED_VIDEO_CALL_TYPE, MessageTypes.MISSED_AUDIO_CALL_TYPE -> R.drawable.symbol_missed_incoming_24
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_downleft_24
MessageTypes.INCOMING_AUDIO_CALL_TYPE, MessageTypes.INCOMING_VIDEO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) R.drawable.symbol_missed_incoming_24 else R.drawable.symbol_arrow_downleft_24
MessageTypes.OUTGOING_AUDIO_CALL_TYPE, MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.drawable.symbol_arrow_upright_24
MessageTypes.GROUP_CALL_TYPE -> when {
call.event.isMissedCall() -> R.drawable.symbol_missed_incoming_24
call.isDisplayedAsMissedCallInUi -> R.drawable.symbol_missed_incoming_24
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.drawable.symbol_group_24
call.direction == CallTable.Direction.INCOMING -> R.drawable.symbol_arrow_downleft_24
call.direction == CallTable.Direction.OUTGOING -> R.drawable.symbol_arrow_upright_24
@@ -61,15 +62,14 @@ object CallPreference {
private fun getCallType(call: CallTable.Call): String {
val id = when (call.messageType) {
MessageTypes.MISSED_AUDIO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.MessageRecord_missed_voice_call else R.string.MessageRecord_missed_voice_call_notification_profile
MessageTypes.MISSED_VIDEO_CALL_TYPE -> if (call.event == CallTable.Event.MISSED) R.string.MessageRecord_missed_video_call else R.string.MessageRecord_missed_video_call_notification_profile
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> R.string.MessageRecord_incoming_voice_call
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> R.string.MessageRecord_incoming_video_call
MessageTypes.MISSED_AUDIO_CALL_TYPE -> getMissedCallString(false, call.event)
MessageTypes.MISSED_VIDEO_CALL_TYPE -> getMissedCallString(true, call.event)
MessageTypes.INCOMING_AUDIO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) getMissedCallString(false, call.event) else R.string.MessageRecord_incoming_voice_call
MessageTypes.INCOMING_VIDEO_CALL_TYPE -> if (call.isDisplayedAsMissedCallInUi) getMissedCallString(true, call.event) else R.string.MessageRecord_incoming_video_call
MessageTypes.OUTGOING_AUDIO_CALL_TYPE -> R.string.MessageRecord_outgoing_voice_call
MessageTypes.OUTGOING_VIDEO_CALL_TYPE -> R.string.MessageRecord_outgoing_video_call
MessageTypes.GROUP_CALL_TYPE -> when {
call.event == CallTable.Event.MISSED -> R.string.CallPreference__missed_group_call
call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE -> R.string.CallPreference__missed_group_call_notification_profile
call.isDisplayedAsMissedCallInUi -> if (call.event == CallTable.Event.MISSED_NOTIFICATION_PROFILE) R.string.CallPreference__missed_group_call_notification_profile else R.string.CallPreference__missed_group_call
call.event == CallTable.Event.GENERIC_GROUP_CALL || call.event == CallTable.Event.JOINED -> R.string.CallPreference__group_call
call.direction == CallTable.Direction.INCOMING -> R.string.CallPreference__incoming_group_call
call.direction == CallTable.Direction.OUTGOING -> R.string.CallPreference__outgoing_group_call
@@ -81,6 +81,23 @@ object CallPreference {
return context.getString(id)
}
@StringRes
private fun getMissedCallString(isVideo: Boolean, callEvent: CallTable.Event): Int {
return if (callEvent == CallTable.Event.MISSED_NOTIFICATION_PROFILE) {
if (isVideo) {
R.string.MessageRecord_missed_video_call_notification_profile
} else {
R.string.MessageRecord_missed_voice_call_notification_profile
}
} else {
if (isVideo) {
R.string.MessageRecord_missed_video_call
} else {
R.string.MessageRecord_missed_voice_call
}
}
}
private fun getCallTime(messageRecord: MessageRecord): String {
return DateUtils.getOnlyTimeString(context, messageRecord.timestamp)
}

View File

@@ -34,12 +34,16 @@ import com.google.common.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.database.DatabaseObserver;
import org.thoughtcrime.securesms.database.MessageTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.MessageId;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob;
import org.thoughtcrime.securesms.jobs.SendViewedReceiptJob;
import org.thoughtcrime.securesms.mms.PartUriParser;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.KeyCachingService;
@@ -64,6 +68,8 @@ public class VoiceNotePlaybackService extends MediaSessionService {
private KeyClearedReceiver keyClearedReceiver;
private VoiceNotePlayerCallback voiceNotePlayerCallback;
private final DatabaseObserver.Observer attachmentDeletionObserver = this::onAttachmentDeleted;
@Override
public void onCreate() {
super.onCreate();
@@ -83,6 +89,7 @@ public class VoiceNotePlaybackService extends MediaSessionService {
setMediaNotificationProvider(new VoiceNoteMediaNotificationProvider(this));
setListener(new MediaSessionServiceListener());
ApplicationDependencies.getDatabaseObserver().registerAttachmentObserver(attachmentDeletionObserver);
}
@Override
@@ -95,6 +102,7 @@ public class VoiceNotePlaybackService extends MediaSessionService {
@Override
public void onDestroy() {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(attachmentDeletionObserver);
player.release();
mediaSession.release();
mediaSession = null;
@@ -191,6 +199,40 @@ public class VoiceNotePlaybackService extends MediaSessionService {
}
}
private void onAttachmentDeleted() {
Log.d(TAG, "Database attachment observer invoked.");
ContextCompat.getMainExecutor(getApplicationContext()).execute(() -> {
if (player != null) {
final MediaItem currentItem = player.getCurrentMediaItem();
if (currentItem == null || currentItem.playbackProperties == null) {
Log.d(TAG, "Current item is null or playback properties are null.");
return;
}
final Uri currentlyPlayingUri = currentItem.playbackProperties.uri;
if (currentlyPlayingUri == VoiceNoteMediaItemFactory.NEXT_URI || currentlyPlayingUri == VoiceNoteMediaItemFactory.END_URI) {
Log.v(TAG, "Attachment deleted while voice note service was playing a system tone.");
}
try {
final AttachmentId partId = new PartUriParser(currentlyPlayingUri).getPartId();
final DatabaseAttachment attachment = SignalDatabase.attachments().getAttachment(partId);
if (attachment == null) {
player.stop();
int playingIndex = player.getCurrentMediaItemIndex();
player.removeMediaItem(playingIndex);
Log.d(TAG, "Currently playing item removed.");
} else {
Log.d(TAG, "Attachment was not null, therefore not deleted, therefore no action taken.");
}
} catch (NumberFormatException ex) {
Log.w(TAG, "Could not parse currently playing URI into an attachmentId.", ex);
}
}
});
}
/**
* Some devices, such as the ASUS Zenfone 8, erroneously report multiple broadcast receivers for {@value Intent#ACTION_MEDIA_BUTTON} in the package manager.
* This triggers a failure within the {@link MediaSession} initialization and throws an {@link IllegalStateException}.

View File

@@ -164,7 +164,8 @@ public class CallParticipantView extends ConstraintLayout {
} else {
infoOverlay.setVisibility(View.GONE);
boolean hasContentToRender = (participant.isVideoEnabled() || participant.isScreenSharing()) && participant.isForwardingVideo();
//TODO: [calling] SFU instability causes the forwarding video flag to alternate quickly, should restore after calling server update
boolean hasContentToRender = (participant.isVideoEnabled() || participant.isScreenSharing()); // && participant.isForwardingVideo();
rendererFrame.setVisibility(hasContentToRender ? View.VISIBLE : View.INVISIBLE);
renderer.setVisibility(hasContentToRender ? View.VISIBLE : View.INVISIBLE);

View File

@@ -34,7 +34,7 @@ public class CallParticipantsLayout extends FlexboxLayout {
private CallParticipant focusedParticipant = null;
private boolean shouldRenderInPip;
private boolean isPortrait;
private boolean isIncomingRing;
private boolean hideAvatar;
private int navBarBottomInset;
private LayoutStrategy layoutStrategy;
@@ -54,7 +54,7 @@ public class CallParticipantsLayout extends FlexboxLayout {
@NonNull CallParticipant focusedParticipant,
boolean shouldRenderInPip,
boolean isPortrait,
boolean isIncomingRing,
boolean hideAvatar,
int navBarBottomInset,
@NonNull LayoutStrategy layoutStrategy)
{
@@ -62,7 +62,7 @@ public class CallParticipantsLayout extends FlexboxLayout {
this.focusedParticipant = focusedParticipant;
this.shouldRenderInPip = shouldRenderInPip;
this.isPortrait = isPortrait;
this.isIncomingRing = isIncomingRing;
this.hideAvatar = hideAvatar;
this.navBarBottomInset = navBarBottomInset;
this.layoutStrategy = layoutStrategy;
@@ -134,7 +134,7 @@ public class CallParticipantsLayout extends FlexboxLayout {
callParticipantView.setBottomInset(navBarBottomInset);
}
if (isIncomingRing) {
if (hideAvatar) {
callParticipantView.hideAvatar();
} else {
callParticipantView.showAvatar();

View File

@@ -49,7 +49,7 @@ data class CallParticipantsState(
val allRemoteParticipants: List<CallParticipant> = remoteParticipants.allParticipants
val isFolded: Boolean = foldableState.isFolded
val isLargeVideoGroup: Boolean = allRemoteParticipants.size > SMALL_GROUP_MAX && !isInPipMode && !isFolded
val isIncomingRing: Boolean = callState == WebRtcViewModel.State.CALL_INCOMING
val hideAvatar: Boolean = callState.isIncomingOrHandledElsewhere
val raisedHands: List<GroupCallRaiseHandEvent>
get() {
@@ -151,7 +151,7 @@ data class CallParticipantsState(
fun getIncomingRingingGroupDescription(context: Context): String? {
if (callState == WebRtcViewModel.State.CALL_INCOMING &&
groupCallState == WebRtcViewModel.GroupCallState.RINGING &&
ringerRecipient.hasServiceId()
ringerRecipient.hasServiceId
) {
val ringerName = ringerRecipient.getShortDisplayName(context)
val membersWithoutYouOrRinger: List<GroupMemberEntry.FullMember> = groupMembers.filterNot { it.member.isSelf || ringerRecipient.requireServiceId() == it.member.serviceId.orElse(null) }

View File

@@ -346,11 +346,12 @@ public class PictureInPictureGestureHelper extends GestureDetector.SimpleOnGestu
}
}
@SuppressLint("RtlHardcoded")
private enum Corner {
TOP_LEFT(Gravity.TOP | Gravity.START, true, true),
TOP_RIGHT(Gravity.TOP | Gravity.END, false, true),
BOTTOM_LEFT(Gravity.BOTTOM | Gravity.START, true, false),
BOTTOM_RIGHT(Gravity.BOTTOM | Gravity.END, false, false);
TOP_LEFT(Gravity.TOP | Gravity.LEFT, true, true),
TOP_RIGHT(Gravity.TOP | Gravity.RIGHT, false, true),
BOTTOM_LEFT(Gravity.BOTTOM | Gravity.LEFT, true, false),
BOTTOM_RIGHT(Gravity.BOTTOM | Gravity.RIGHT, false, false);
final int gravity;
final boolean leftSide;

View File

@@ -16,7 +16,7 @@ class WebRtcCallParticipantsPage {
private final boolean isRenderInPip;
private final boolean isPortrait;
private final boolean isLandscapeEnabled;
private final boolean isIncomingRing;
private final boolean hideAvatar;
private final int navBarBottomInset;
static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List<CallParticipant> callParticipants,
@@ -24,10 +24,10 @@ class WebRtcCallParticipantsPage {
boolean isRenderInPip,
boolean isPortrait,
boolean isLandscapeEnabled,
boolean isIncomingRing,
boolean hideAvatar,
int navBarBottomInset)
{
return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip, isPortrait, isLandscapeEnabled, isIncomingRing, navBarBottomInset);
return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip, isPortrait, isLandscapeEnabled, hideAvatar, navBarBottomInset);
}
static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant,
@@ -44,7 +44,7 @@ class WebRtcCallParticipantsPage {
boolean isRenderInPip,
boolean isPortrait,
boolean isLandscapeEnabled,
boolean isIncomingRing,
boolean hideAvatar,
int navBarBottomInset)
{
this.callParticipants = callParticipants;
@@ -53,7 +53,7 @@ class WebRtcCallParticipantsPage {
this.isRenderInPip = isRenderInPip;
this.isPortrait = isPortrait;
this.isLandscapeEnabled = isLandscapeEnabled;
this.isIncomingRing = isIncomingRing;
this.hideAvatar = hideAvatar;
this.navBarBottomInset = navBarBottomInset;
}
@@ -77,8 +77,8 @@ class WebRtcCallParticipantsPage {
return isPortrait;
}
public boolean isIncomingRing() {
return isIncomingRing;
public boolean shouldHideAvatar() {
return hideAvatar;
}
public int getNavBarBottomInset() {
@@ -98,7 +98,7 @@ class WebRtcCallParticipantsPage {
isRenderInPip == that.isRenderInPip &&
isPortrait == that.isPortrait &&
isLandscapeEnabled == that.isLandscapeEnabled &&
isIncomingRing == that.isIncomingRing &&
hideAvatar == that.hideAvatar &&
callParticipants.equals(that.callParticipants) &&
focusedParticipant.equals(that.focusedParticipant) &&
navBarBottomInset == that.navBarBottomInset;
@@ -106,6 +106,6 @@ class WebRtcCallParticipantsPage {
@Override
public int hashCode() {
return Objects.hash(callParticipants, focusedParticipant, isSpeaker, isRenderInPip, isPortrait, isLandscapeEnabled, isIncomingRing, navBarBottomInset);
return Objects.hash(callParticipants, focusedParticipant, isSpeaker, isRenderInPip, isPortrait, isLandscapeEnabled, hideAvatar, navBarBottomInset);
}
}

View File

@@ -86,7 +86,7 @@ class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipa
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip(), page.isPortrait(), page.isIncomingRing(), page.getNavBarBottomInset(), page.getLayoutStrategy());
callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip(), page.isPortrait(), page.shouldHideAvatar(), page.getNavBarBottomInset(), page.getLayoutStrategy());
}
}

View File

@@ -96,6 +96,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
private RecipientId recipientId;
private ImageView answer;
private TextView answerWithoutVideoLabel;
private ImageView cameraDirectionToggle;
private AccessibleToggleButton ringToggle;
private PictureInPictureGestureHelper pictureInPictureGestureHelper;
private ImageView overflow;
@@ -177,6 +178,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
incomingRingStatus = findViewById(R.id.call_screen_incoming_ring_status);
answer = findViewById(R.id.call_screen_answer_call);
answerWithoutVideoLabel = findViewById(R.id.call_screen_answer_without_video_label);
cameraDirectionToggle = findViewById(R.id.call_screen_camera_direction_toggle);
ringToggle = findViewById(R.id.call_screen_audio_ring_toggle);
overflow = findViewById(R.id.call_screen_overflow_button);
hangup = findViewById(R.id.call_screen_end_call);
@@ -271,6 +273,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
runIfNonNull(controlsListener, listener -> listener.onRingGroupChanged(isOn, ringToggle.isActivated()));
});
cameraDirectionToggle.setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
smallLocalRender.findViewById(R.id.call_participant_switch_camera).setOnClickListener(v -> runIfNonNull(controlsListener, ControlsListener::onCameraDirectionChanged));
overflow.setOnClickListener(v -> {
@@ -352,6 +355,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
rotatableControls.add(audioToggle);
rotatableControls.add(micToggle);
rotatableControls.add(videoToggle);
rotatableControls.add(cameraDirectionToggle);
rotatableControls.add(decline);
rotatableControls.add(smallLocalAudioIndicator);
rotatableControls.add(ringToggle);
@@ -429,7 +433,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
if (!state.getGridParticipants().isEmpty()) {
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode(), isPortrait, isLandscapeEnabled, state.isIncomingRing(), navBarBottomInset));
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode(), isPortrait, isLandscapeEnabled, state.getHideAvatar(), navBarBottomInset));
}
if (state.getFocusedParticipant() != CallParticipant.EMPTY && state.getAllRemoteParticipants().size() > 1) {
@@ -917,6 +921,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
}
private void updateButtonStateForLargeButtons() {
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle);
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup);
overflow.setImageResource(R.drawable.webrtc_call_screen_overflow_menu);
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle);
@@ -927,6 +932,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout {
}
private void updateButtonStateForSmallButtons() {
cameraDirectionToggle.setImageResource(R.drawable.webrtc_call_screen_camera_toggle_small);
hangup.setImageResource(R.drawable.webrtc_call_screen_hangup_small);
overflow.setImageResource(R.drawable.webrtc_call_screen_overflow_menu_small);
micToggle.setBackgroundResource(R.drawable.webrtc_call_screen_mic_toggle_small);

View File

@@ -416,6 +416,8 @@ public class WebRtcCallViewModel extends ViewModel {
case CALL_ACCEPTED_ELSEWHERE:
case CALL_DECLINED_ELSEWHERE:
case CALL_ONGOING_ELSEWHERE:
callState = WebRtcControls.CallState.HANDLED_ELSEWHERE;
break;
case CALL_NEEDS_PERMISSION:
case CALL_BUSY:
case CALL_DISCONNECTED:

View File

@@ -105,7 +105,7 @@ public final class WebRtcControls {
* This is only true at the very start of a call and will then never be true again
*/
public boolean hideControlsSheetInitially() {
return displayIncomingCallButtons() || callState == CallState.NONE;
return displayIncomingCallButtons() || callState == CallState.NONE || isHandledElsewhere();
}
public boolean displayErrorControls() {
@@ -263,6 +263,10 @@ public final class WebRtcControls {
return callState == CallState.INCOMING;
}
private boolean isHandledElsewhere() {
return callState == CallState.HANDLED_ELSEWHERE;
}
private boolean isAtLeastOutgoing() {
return callState.isAtLeast(CallState.OUTGOING);
}
@@ -284,6 +288,7 @@ public final class WebRtcControls {
public enum CallState {
NONE,
ERROR,
HANDLED_ELSEWHERE,
PRE_JOIN,
RECONNECTING,
INCOMING,

View File

@@ -34,12 +34,11 @@ import androidx.compose.runtime.rxjava3.subscribeAsState
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.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
@@ -183,7 +182,7 @@ private fun CallInfo(
item {
Rows.TextRow(
text = stringResource(id = R.string.CallLinkDetailsFragment__share_link),
icon = ImageVector.vectorResource(id = R.drawable.symbol_link_24),
icon = painterResource(id = R.drawable.symbol_link_24),
iconModifier = Modifier
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
@@ -446,7 +445,7 @@ private fun CallParticipantRow(
if (showIcons && showHandRaised) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_raise_hand_24),
painter = painterResource(id = R.drawable.symbol_raise_hand_24),
contentDescription = null,
modifier = Modifier.align(Alignment.CenterVertically)
)
@@ -454,7 +453,7 @@ private fun CallParticipantRow(
if (showIcons && !isVideoEnabled) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_video_slash_24),
painter = painterResource(id = R.drawable.symbol_video_slash_24),
contentDescription = null,
modifier = Modifier.align(Alignment.CenterVertically)
)
@@ -466,7 +465,7 @@ private fun CallParticipantRow(
}
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_mic_slash_24),
painter = painterResource(id = R.drawable.symbol_mic_slash_24),
contentDescription = null,
modifier = Modifier.align(Alignment.CenterVertically)
)
@@ -478,7 +477,7 @@ private fun CallParticipantRow(
}
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_minus_circle_24),
painter = painterResource(id = R.drawable.symbol_minus_circle_24),
contentDescription = null,
modifier = Modifier
.clickable(onClick = onBlockClicked)

View File

@@ -209,7 +209,7 @@ class ControlsAndInfoController(
}
}
private fun onControlTopChanged() {
fun onControlTopChanged() {
val guidelineTop = max(frame.top, coordinator.height - behavior.peekHeight)
aboveControlsGuideline.setGuidelineBegin(guidelineTop)
webRtcCallView.onControlTopChanged()
@@ -321,6 +321,7 @@ class ControlsAndInfoController(
val margin = if (controlState.displaySmallCallButtons()) 4.dp else 8.dp
setControlConstraints(R.id.call_screen_speaker_toggle, controlState.displayAudioToggle(), margin)
setControlConstraints(R.id.call_screen_camera_direction_toggle, controlState.displayCameraToggle(), margin)
setControlConstraints(R.id.call_screen_video_toggle, controlState.displayVideoToggle(), margin)
setControlConstraints(R.id.call_screen_audio_mic_toggle, controlState.displayMuteAudio(), margin)
setControlConstraints(R.id.call_screen_audio_ring_toggle, controlState.displayRingToggle(), margin)

View File

@@ -5,7 +5,6 @@
package org.thoughtcrime.securesms.components.webrtc.controls
import android.content.Context
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.expandIn
@@ -44,7 +43,6 @@ import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.reactivex.rxjava3.core.BackpressureStrategy
import kotlinx.coroutines.delay
import org.signal.core.ui.theme.SignalTheme
@@ -160,7 +158,7 @@ private fun RaiseHand(
val context = LocalContext.current
TextButton(
onClick = {
showLowerHandDialog(context)
ApplicationDependencies.getSignalCallManager().raiseHand(false)
},
modifier = Modifier.wrapContentWidth(Alignment.End)
) {
@@ -182,16 +180,6 @@ private fun RaiseHand(
}
}
private fun showLowerHandDialog(context: Context) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.CallOverflowPopupWindow__lower_your_hand)
.setPositiveButton(
R.string.CallOverflowPopupWindow__lower_hand
) { _, _ -> ApplicationDependencies.getSignalCallManager().raiseHand(false) }
.setNegativeButton(R.string.CallOverflowPopupWindow__cancel, null)
.show()
}
@Composable
private fun getSnackbarText(state: RaiseHandState): String {
if (state.isEmpty) {

View File

@@ -16,7 +16,7 @@ class CallLinkIncomingRequestRepository {
fun getGroupsInCommon(recipientId: RecipientId): Observable<GroupsInCommon> {
return Recipient.observable(recipientId).flatMapSingle { recipient ->
if (recipient.hasGroupsInCommon()) {
if (recipient.hasGroupsInCommon) {
Single.fromCallable {
val groupsInCommon = SignalDatabase.groups.getGroupsContainingMember(recipient.id, true)
val total = groupsInCommon.size

View File

@@ -23,11 +23,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
@@ -165,7 +164,7 @@ private fun CallLinkIncomingRequestSheetContent(
item {
Rows.TextRow(
text = stringResource(id = R.string.CallLinkIncomingRequestSheet__approve_entry),
icon = ImageVector.vectorResource(R.drawable.symbol_check_circle_24),
icon = painterResource(R.drawable.symbol_check_circle_24),
onClick = onApproveEntry
)
}
@@ -173,7 +172,7 @@ private fun CallLinkIncomingRequestSheetContent(
item {
Rows.TextRow(
text = stringResource(id = R.string.CallLinkIncomingRequestSheet__deny_entry),
icon = ImageVector.vectorResource(R.drawable.symbol_x_circle_24),
icon = painterResource(R.drawable.symbol_x_circle_24),
onClick = onDenyEntry
)
}
@@ -219,7 +218,7 @@ private fun Title(
style = MaterialTheme.typography.headlineMedium
)
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.symbol_person_circle_24),
painter = painterResource(id = R.drawable.symbol_person_circle_24),
contentDescription = null,
modifier = Modifier
.padding(start = 6.dp)

View File

@@ -33,7 +33,7 @@ class ContactsManagementRepository(context: Context) {
error("Cannot hide groups, self, or distribution lists.")
}
val rotateProfileKey = !recipient.hasGroupsInCommon()
val rotateProfileKey = !recipient.hasGroupsInCommon
SignalDatabase.recipients.markHidden(recipient.id, rotateProfileKey, false)
if (rotateProfileKey) {
ApplicationDependencies.getJobManager().add(RotateProfileKeyJob())

View File

@@ -309,9 +309,8 @@ open class ContactSearchAdapter(
}
private class MyStoryFallbackPhotoProvider(private val name: String, private val targetSize: Int) : Recipient.FallbackPhotoProvider() {
override fun getPhotoForLocalNumber(): FallbackContactPhoto {
return GeneratedContactPhoto(name, R.drawable.symbol_person_40, targetSize)
}
override val photoForLocalNumber: FallbackContactPhoto
get() = GeneratedContactPhoto(name, R.drawable.symbol_person_40, targetSize)
}
override fun onAttachedToWindow() {
@@ -461,7 +460,7 @@ open class ContactSearchAdapter(
} else if (displayOptions.displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.combinedAboutAndEmoji != null) {
number.text = recipient.combinedAboutAndEmoji
number.visible = true
} else if (displayOptions.displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.hasE164()) {
} else if (displayOptions.displaySecondaryInformation == DisplaySecondaryInformation.ALWAYS && recipient.hasE164) {
number.visible = false
} else {
super.bindNumberField(model)
@@ -527,7 +526,7 @@ open class ContactSearchAdapter(
}
val recipient = getRecipient(model)
val suffix: CharSequence? = if (recipient.isSystemContact && !recipient.showVerified()) {
val suffix: CharSequence? = if (recipient.isSystemContact && !recipient.showVerified) {
SpannableStringBuilder().apply {
val drawable = ContextUtil.requireDrawable(context, R.drawable.symbol_person_circle_24).apply {
setTint(ContextCompat.getColor(context, R.color.signal_colorOnSurface))

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