mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-16 16:06:08 +00:00
Compare commits
126 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da43ff1e95 | ||
|
|
f053ebbd51 | ||
|
|
87606af29c | ||
|
|
c811bdcffa | ||
|
|
0536628da3 | ||
|
|
1fa53cfcb8 | ||
|
|
a9ea3854d2 | ||
|
|
dc35261e00 | ||
|
|
716bc1f5e7 | ||
|
|
db27204084 | ||
|
|
42aeceffe2 | ||
|
|
03845eabaf | ||
|
|
62af9dad50 | ||
|
|
ee58d47926 | ||
|
|
d74260b536 | ||
|
|
15d8a698c5 | ||
|
|
62cf3feeaa | ||
|
|
947ab7d48b | ||
|
|
a82b9ee25f | ||
|
|
1e4d96b7c4 | ||
|
|
735a8e680c | ||
|
|
d9e9fe1d6a | ||
|
|
4bcd1df4f8 | ||
|
|
9762899272 | ||
|
|
ce1b73970c | ||
|
|
58282e589b | ||
|
|
75bd113545 | ||
|
|
7a6bd0e1f2 | ||
|
|
f673c4eb83 | ||
|
|
cbb04e8f0c | ||
|
|
cd03da54d5 | ||
|
|
5f31f5966c | ||
|
|
d8bbfe2678 | ||
|
|
7a2d408ca2 | ||
|
|
5e4dfcc65f | ||
|
|
7811e51b41 | ||
|
|
9703a868e5 | ||
|
|
1b7784b01f | ||
|
|
a83abaca1d | ||
|
|
29b3f09d8a | ||
|
|
d36b2a23f5 | ||
|
|
8f1722c718 | ||
|
|
5416c3b8aa | ||
|
|
89eeae36c4 | ||
|
|
eec2685e67 | ||
|
|
318b59a6b2 | ||
|
|
a2e0468cd9 | ||
|
|
689eacd618 | ||
|
|
8617a074ad | ||
|
|
046b8da880 | ||
|
|
34a36ddfea | ||
|
|
9330448198 | ||
|
|
b3336b4d84 | ||
|
|
9553c94097 | ||
|
|
c1845ae1c4 | ||
|
|
b6cc3852b0 | ||
|
|
eefc86f27e | ||
|
|
09404157aa | ||
|
|
abfd9f8f41 | ||
|
|
e04381fd75 | ||
|
|
30cc3ff9fc | ||
|
|
6f5f299035 | ||
|
|
02eed02cb8 | ||
|
|
c1d29b5c39 | ||
|
|
db4442939d | ||
|
|
6ece776382 | ||
|
|
0eda714755 | ||
|
|
831d099503 | ||
|
|
fa23e4ca70 | ||
|
|
982f602178 | ||
|
|
713298109a | ||
|
|
8793981804 | ||
|
|
9bd4e9524c | ||
|
|
791dc2724f | ||
|
|
ba3473c61a | ||
|
|
3ea194255d | ||
|
|
ea081e981f | ||
|
|
2ce6ea9a2a | ||
|
|
295c9310e9 | ||
|
|
7447ed2eac | ||
|
|
d5bf16b91a | ||
|
|
76665c1f0d | ||
|
|
dd28523b05 | ||
|
|
16588c401e | ||
|
|
dbf8a7ca87 | ||
|
|
e92c76434e | ||
|
|
7adb581271 | ||
|
|
869476a41b | ||
|
|
8daf1bca20 | ||
|
|
d044b3c931 | ||
|
|
0fcb19e1cc | ||
|
|
2a6977da75 | ||
|
|
26bd435bf6 | ||
|
|
91f8d6075c | ||
|
|
9ed9a330f4 | ||
|
|
8bbf6b790f | ||
|
|
a277e9b307 | ||
|
|
f8e6bcf290 | ||
|
|
3ba2b46bb0 | ||
|
|
b50eab230d | ||
|
|
3f91824325 | ||
|
|
879e05148b | ||
|
|
78e36b85d4 | ||
|
|
544cc06f13 | ||
|
|
133b7ef3f1 | ||
|
|
08a407dc23 | ||
|
|
6c697fad8b | ||
|
|
c904a7aa97 | ||
|
|
ad131d7c65 | ||
|
|
e12d2d1e98 | ||
|
|
f01e044662 | ||
|
|
03d3ae7043 | ||
|
|
6b60a22879 | ||
|
|
bbded8caa8 | ||
|
|
3a6352d2a3 | ||
|
|
8293d6bc4c | ||
|
|
56bdb28c2f | ||
|
|
b081fb1e13 | ||
|
|
58c1f64dfe | ||
|
|
92b7147dcd | ||
|
|
fa3a85c948 | ||
|
|
9da4513694 | ||
|
|
de520036a9 | ||
|
|
97ca15a1c0 | ||
|
|
713a34a5e7 | ||
|
|
d688280a30 |
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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 -->
|
||||
|
||||
@@ -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\"")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]!!
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)?)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ class VerificationCodeView @JvmOverloads constructor(context: Context, attrs: At
|
||||
}
|
||||
}
|
||||
|
||||
interface OnCodeEnteredListener {
|
||||
fun interface OnCodeEnteredListener {
|
||||
fun onCodeComplete(code: String)
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) ->
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user