Compare commits

...

65 Commits

Author SHA1 Message Date
Cody Henthorne
068eaff801 Bump version to 8.5.0 2026-03-25 17:02:37 -04:00
Cody Henthorne
e0bb3a48c2 Update translations and other static files. 2026-03-25 16:41:04 -04:00
Alex Hart
f2e4881026 Add underpinnings to allow for local plaintext export.
Co-authored-by: Cody Henthorne <cody@signal.org>
2026-03-25 16:31:10 -04:00
Alex Hart
b605148ac4 Wake lock during restore. 2026-03-25 16:31:10 -04:00
Cody Henthorne
2b9126d74b Update group terminated banner. 2026-03-25 16:31:10 -04:00
Alex Hart
206f6d84e7 Remove size line from backup info. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
01836b3a7c Update emoji to unicode 17. 2026-03-25 16:31:09 -04:00
Michelle Tang
e68691c966 Show final disappearing timer value for collapsed events. 2026-03-25 16:31:09 -04:00
Alex Hart
957f473e77 Ensure we upgrade properly from v1. 2026-03-25 16:31:09 -04:00
Alex Hart
8a023100ea Write backup file timestamp in utc. 2026-03-25 16:31:09 -04:00
Alex Hart
5bfdca509c Fix dark mode on update card. 2026-03-25 16:31:09 -04:00
Tushar Soni
9a837254ec Simplify recipient list truncation in CreateFolderScreen.
Resolves signalapp/Signal-Android#14439
Closes signalapp/Signal-Android#14473
2026-03-25 16:31:09 -04:00
Alex Hart
3f27769d20 Enable new local backup export for external users. 2026-03-25 16:31:09 -04:00
Michelle Tang
4f260c2063 Backfill collapsible messages. 2026-03-25 16:31:09 -04:00
Michelle Tang
75df16e842 Fix collapsing tests. 2026-03-25 16:31:09 -04:00
Michelle Tang
fce6651e26 Fix pinned messages with attachments. 2026-03-25 16:31:09 -04:00
andrew-signal
b06783bc90 Bump to libsignal v0.89.2 2026-03-25 16:31:09 -04:00
Jesse Weinstein
72a1a9b0ff Fix unquoted imports in proto files.
Closes signalapp/Signal-Android#14669
2026-03-25 16:31:09 -04:00
DivyaKhunt07
5568a14490 Fix unexpected keyboard appearance on repeated back swipe.
Resolves signalapp/Signal-Android#14618
Closes signalapp/Signal-Android#14633
2026-03-25 16:31:09 -04:00
Michelle Tang
378ebb00c4 Allow multiselect deleting for collapsed events. 2026-03-25 16:31:09 -04:00
Cody Henthorne
c81f40eb74 Add additional group terminate checks. 2026-03-25 16:31:09 -04:00
Alex Hart
d97bde3959 Update to utilize main activity instead of passthrough. 2026-03-25 16:31:09 -04:00
jeffrey-signal
4d301a4f66 Show conversation settings in the detail pane on large screens. 2026-03-25 16:31:09 -04:00
Alex Hart
9941b2d123 Fix several bugs in the local backup restore flow. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
089d8a50b2 Promote the new APNG renderer to a normal flag. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
eb8ad5218d Filter archived stories from the stories landing page query. 2026-03-25 16:31:09 -04:00
Michelle Tang
21b1401fc4 Update safety number tappable area. 2026-03-25 16:31:09 -04:00
Michelle Tang
58ea9a1f48 Rename collapsed events for 1:1. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
2bb9578ef9 Use sqlite-jdbc for unit tests to enable FTS5 and JSON1 support. 2026-03-25 16:31:09 -04:00
Michelle Tang
c3b8768570 Turn on collapsing chat events for internal users. 2026-03-25 16:31:09 -04:00
Alex Hart
94e3dabc20 Confirm backup location after successful local backup restore. 2026-03-25 16:31:09 -04:00
Jesse Weinstein
542a820e22 Remove UriSerializer typealias -- it is only used in two places 2026-03-25 16:31:09 -04:00
Greyson Parrelli
8f7cc52255 Fix bug around collision detection filtering. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
63888f1c99 Refactor name collision tables to improve perf. 2026-03-25 16:31:09 -04:00
jeffrey-signal
a588522c9b Support navigating back to MainActivity with no conversation selected. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
7a2eca3bd5 Fix all media storage overview performance. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
a8ba0dccca Fix story reply thumbnails. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
782c83cc4e Fix story download bug. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
46e6ae915c Add better loading states for story archive and starred. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
8a887b65a1 Extract base archive classes into their own module. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
08491579dd Add links to the all media view. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
25b01a30be Improve memory usage of new APNG renderer by making it streaming. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
48374e6950 Add support for starring messages. 2026-03-25 16:31:09 -04:00
jeffrey-signal
6496f236ea Fix secure backups learn more link.
Resolves signalapp/Signal-Android#14657
2026-03-25 16:31:09 -04:00
Alex Hart
e767434c2b Perform StorageServiceRestore on skip if already registered. 2026-03-25 16:31:09 -04:00
Michelle Tang
bb6507a456 Disabled new lines for when statements for ktlint. 2026-03-25 16:31:09 -04:00
Greyson Parrelli
c3f9e5d972 Add new APNG renderer, just for internal users for now. 2026-03-25 16:31:08 -04:00
Alex Hart
34d87cf6e1 Warning dialogs for local backup restore. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
e657a4adf3 Guard auto-lower hand behind labs. 2026-03-25 16:31:08 -04:00
jeffrey-signal
9594599d60 Fix unread filter deactivating when scrolling through the conversation list. 2026-03-25 16:31:08 -04:00
Cody Henthorne
a0c0acb8fc Add group terminate support. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
0896718e5c Annotate labs features as such. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
be4bf27ede Remove attachment table JSON join. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
7253aaaa14 Add the ability to filter search by date and author. 2026-03-25 16:31:08 -04:00
jeffrey-signal
72cbe61f6c Prepare conversation fragment navigation for two-pane conversation settings. 2026-03-25 16:31:08 -04:00
Alex Hart
78d3db319c Fix local backup restore AEP handling and conditional re-enable. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
c7a6c7ad9e Minor improvements to SVRB error handling. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
8bc183b994 Fix validation error with session switchover events. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
ef6e5abc17 Add retry logic for camera binding failures. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
e96e6e8d18 Use note to self icon in share sheet. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
cee33a23ac Use transaction when loading logs. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
c5de7581ee Show error message on SN screen when there's no ACI. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
5dc626078f Compress shared contact avatar before launching add-to-contacts intent.
Old way let us use photos that could put us over the 1mb transaction
size limit.
2026-03-25 16:31:08 -04:00
Greyson Parrelli
9de75b3e1f Show groups that have the same member list during group creation. 2026-03-25 16:31:08 -04:00
Greyson Parrelli
f09bf5b14c Make regV5 resumable if the app closes. 2026-03-19 17:13:11 -04:00
693 changed files with 25317 additions and 5317 deletions

View File

@@ -20,6 +20,7 @@ ktlint_standard_unnecessary-parentheses-before-trailing-lambda = disabled
ktlint_standard_value-parameter-comment = disabled
ktlint_standard_class-signature = disabled
ktlint_standard_function-expression-body = disabled
ktlint_standard_blank-line-between-when-conditions = disabled
# Disable ktlint on generated source code, see
# https://github.com/JLLeitschuh/ktlint-gradle/issues/746

View File

@@ -24,8 +24,8 @@ plugins {
apply(from = "static-ips.gradle.kts")
val canonicalVersionCode = 1670
val canonicalVersionName = "8.4.1"
val canonicalVersionCode = 1671
val canonicalVersionName = "8.5.0"
val currentHotfixVersion = 0
val maxHotfixVersions = 100
@@ -92,6 +92,7 @@ wire {
protoPath {
srcDir("${project.rootDir}/lib/libsignal-service/src/main/protowire")
srcDir("${project.rootDir}/lib/archive/src/main/protowire")
}
// Handled by libsignal
prune("signalservice.DecryptionErrorMessage")
@@ -594,6 +595,7 @@ dependencies {
ktlintRuleset(libs.ktlint.twitter.compose)
coreLibraryDesugaring(libs.android.tools.desugar)
implementation(project(":lib:archive"))
implementation(project(":lib:libsignal-service"))
implementation(project(":lib:paging"))
implementation(project(":core:util"))
@@ -613,6 +615,7 @@ dependencies {
implementation(project(":core:models-jvm"))
implementation(project(":feature:camera"))
implementation(project(":feature:registration"))
implementation(project(":lib:apng"))
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.appcompat) {
@@ -747,6 +750,7 @@ dependencies {
testImplementation(testFixtures(project(":lib:libsignal-service")))
testImplementation(testLibs.espresso.core)
testImplementation(testLibs.kotlinx.coroutines.test)
testImplementation(testLibs.sqlite.jdbc)
testImplementation(libs.androidx.compose.ui.test.junit4)
"perfImplementation"(libs.androidx.compose.ui.test.manifest)

View File

@@ -13,6 +13,8 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.archive.proto.Frame
import org.signal.archive.stream.PlainTextBackupReader
import org.signal.core.models.ServiceId
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
@@ -20,8 +22,6 @@ import org.signal.core.util.readFully
import org.signal.libsignal.messagebackup.ComparableBackup
import org.signal.libsignal.messagebackup.MessageBackup
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore

View File

@@ -358,5 +358,9 @@ class V2ConversationItemShapeTest {
override fun onToggleVote(poll: PollRecord, pollOption: PollOption, isChecked: Boolean) = Unit
override fun onViewPinnedMessage(messageId: Long) = Unit
override fun onExpandEvents(messageId: Long) = Unit
override fun onCollapseEvents(messageId: Long) = Unit
}
}

View File

@@ -0,0 +1,301 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.mockk.every
import io.mockk.mockkStatic
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.models.ServiceId.ACI
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.RemoteConfig
import java.util.UUID
import kotlin.time.Duration.Companion.days
@RunWith(AndroidJUnit4::class)
class CollapsingMessagesTests {
private lateinit var message: MessageTable
private lateinit var thread: ThreadTable
@Rule
@JvmField
val databaseRule = SignalDatabaseRule()
private lateinit var alice: RecipientId
private var aliceThread: Long = 0
private lateinit var bob: RecipientId
@Before
fun setUp() {
mockkStatic(RemoteConfig::class)
every { RemoteConfig.collapseEvents } returns true
message = SignalDatabase.messages
message.deleteAllThreads()
thread = SignalDatabase.threads
alice = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
aliceThread = SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(alice))
bob = SignalDatabase.recipients.getOrInsertFromServiceId(ACI.from(UUID.randomUUID()))
}
@Test
fun givenCollapsibleMessage_whenIInsert_thenItBecomesHead() {
val messageId = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val msg = message.getMessageRecord(messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg.collapsedState)
assertEquals(messageId, msg.collapsedHeadId)
}
@Test
fun givenSameCollapsibleTypes_whenIInsert_thenAllCollapseUnderHead() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
val msg1 = message.getMessageRecord(messageId1)
val msg2 = message.getMessageRecord(messageId2)
val msg3 = message.getMessageRecord(messageId3)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msg2.collapsedState)
assertEquals(messageId1, msg2.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msg3.collapsedState)
assertEquals(messageId1, msg3.collapsedHeadId)
}
@Test
fun givenDifferentCollapsedTypes_whenIInsert_thenNoCollapsing() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = MmsHelper.insert(message = OutgoingMessage.identityVerifiedMessage(Recipient.resolved(alice), 2000L), threadId = aliceThread)
val msg1 = message.getMessageRecord(messageId1)
val msg2 = message.getMessageRecord(messageId2)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
assertEquals(messageId2, msg2.collapsedHeadId)
}
@Test
fun givenNonCollapsibleTypes_whenIInsert_thenNoCollapsing() {
val messageId = MmsHelper.insert(recipient = Recipient.resolved(alice), sentTimeMillis = 1000L)
val msg = message.getMessageRecord(messageId)
assertEquals(CollapsedState.NONE, msg.collapsedState)
assertEquals(0, msg.collapsedHeadId)
}
@Test
fun givenMessagesOnDifferentDays_whenIInsert_thenNoCollapsing() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
message.writableDatabase.update(
MessageTable.TABLE_NAME,
contentValuesOf(MessageTable.DATE_RECEIVED to (System.currentTimeMillis() - 1.days.inWholeMilliseconds)),
"${MessageTable.ID} = ?",
arrayOf(messageId1.toString())
)
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val msg2 = message.getMessageRecord(messageId2)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
assertEquals(messageId2, msg2.collapsedHeadId)
}
@Test
fun givenRegularMessageBetweenCollapsed_whenIInsertCollapsed_thenNoCollapsing() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = MmsHelper.insert(recipient = Recipient.resolved(alice), sentTimeMillis = 2000L)
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
val msg1 = message.getMessageRecord(messageId1)
val msg2 = message.getMessageRecord(messageId2)
val msg3 = message.getMessageRecord(messageId3)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.NONE, msg2.collapsedState)
assertEquals(0, msg2.collapsedHeadId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg3.collapsedState)
assertEquals(messageId3, msg3.collapsedHeadId)
}
@Test
fun givenDifferentThreads_whenIInsertCollapsed_thenNoCollapsing() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = message.insertCallLog(bob, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val msg1 = message.getMessageRecord(messageId1)
val msg2 = message.getMessageRecord(messageId2)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
assertEquals(messageId2, msg2.collapsedHeadId)
}
@Test
fun givenCollapsedMessages_whenIDeleteFirstMessage_thenNextMessageBecomesHead() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
message.deleteMessage(messageId1, aliceThread)
val msg2 = message.getMessageRecord(messageId2)
val msg3 = message.getMessageRecord(messageId3)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg2.collapsedState)
assertEquals(messageId2, msg2.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msg3.collapsedState)
assertEquals(messageId2, msg3.collapsedHeadId)
}
@Test
fun givenCollapsedMessages_whenIDeleteNonFirstMessage_thenFirstMessageStaysHead() {
val messageId1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false).messageId
val messageId2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false).messageId
val messageId3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false).messageId
message.deleteMessage(messageId2, aliceThread)
val msg1 = message.getMessageRecord(messageId1)
val msg3 = message.getMessageRecord(messageId3)
assertEquals(CollapsedState.HEAD_COLLAPSED, msg1.collapsedState)
assertEquals(messageId1, msg1.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msg3.collapsedState)
assertEquals(messageId1, msg3.collapsedHeadId)
}
@Test
fun givenTwoCollapsingTypes_whenIDeleteHeadOfFirstGroup_thenSecondGroupIsUnchanged() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
val recipient = Recipient.resolved(alice)
val identity1Id = MmsHelper.insert(message = OutgoingMessage.identityVerifiedMessage(recipient, 3000L), threadId = call1.threadId)
val identity2Id = MmsHelper.insert(message = OutgoingMessage.identityVerifiedMessage(recipient, 4000L), threadId = call1.threadId)
message.deleteMessage(call1.messageId, call1.threadId)
val msgCall2 = message.getMessageRecord(call2.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall2.collapsedState)
assertEquals(call2.messageId, msgCall2.collapsedHeadId)
val msgIdentity1 = message.getMessageRecord(identity1Id)
val msgIdentity2 = message.getMessageRecord(identity2Id)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgIdentity1.collapsedState)
assertEquals(identity1Id, msgIdentity1.collapsedHeadId)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgIdentity2.collapsedState)
assertEquals(identity1Id, msgIdentity2.collapsedHeadId)
}
@Test
fun givenPendingCollapsingEvents_whenIMarkSeenAtASpecificTime_thenEverythingBeforeThatTimeIsCollapsed() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
message.collapsePendingCollapsibleEvents(aliceThread, System.currentTimeMillis())
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
val msgCall1 = message.getMessageRecord(call1.messageId)
val msgCall2 = message.getMessageRecord(call2.messageId)
val msgCall3 = message.getMessageRecord(call3.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
assertEquals(CollapsedState.COLLAPSED, msgCall2.collapsedState)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall3.collapsedState)
}
@Test
fun givenPendingCollapsingEvents_whenIMarkAllAsSeen_thenEverythingIsCollapsed() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
message.collapseAllPendingCollapsibleEvents()
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
val msgCall1 = message.getMessageRecord(call1.messageId)
val msgCall2 = message.getMessageRecord(call2.messageId)
val msgCall3 = message.getMessageRecord(call3.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
assertEquals(CollapsedState.COLLAPSED, msgCall2.collapsedState)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall3.collapsedState)
}
@Test
fun givenCollapsedEvents_whenITrimTheThreadByCount_thenIExpectANewHead() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
val call4 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 4000L, false)
val msgCall1 = message.getMessageRecord(call1.messageId)
val msgCall2 = message.getMessageRecord(call2.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall2.collapsedState)
thread.trimThread(threadId = aliceThread, syncThreadTrimDeletes = false, length = 2)
val msgCall3 = message.getMessageRecord(call3.messageId)
val msgCall4 = message.getMessageRecord(call4.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall3.collapsedState)
assertEquals(CollapsedState.PENDING_COLLAPSED, msgCall4.collapsedState)
assertEquals(call3.messageId, msgCall4.collapsedHeadId)
}
@Test
fun givenCollapsedEvents_whenITrimTheThreadByDate_thenIExpectANewHead() {
val call1 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 1000L, false)
val call2 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 2000L, false)
val trimBeforeDate = System.currentTimeMillis()
val call3 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 3000L, false)
val call4 = message.insertCallLog(alice, MessageTypes.INCOMING_AUDIO_CALL_TYPE, 4000L, false)
message.collapsePendingCollapsibleEvents(aliceThread, System.currentTimeMillis())
val msgCall1 = message.getMessageRecord(call1.messageId)
val msgCall2 = message.getMessageRecord(call2.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall1.collapsedState)
assertEquals(CollapsedState.COLLAPSED, msgCall2.collapsedState)
thread.trimThread(threadId = aliceThread, syncThreadTrimDeletes = false, trimBeforeDate = trimBeforeDate)
val msgCall3 = message.getMessageRecord(call3.messageId)
val msgCall4 = message.getMessageRecord(call4.messageId)
assertEquals(CollapsedState.HEAD_COLLAPSED, msgCall3.collapsedState)
assertEquals(CollapsedState.COLLAPSED, msgCall4.collapsedState)
assertEquals(call3.messageId, msgCall4.collapsedHeadId)
}
}

View File

@@ -1,541 +0,0 @@
package org.thoughtcrime.securesms.database
import android.app.Application
import androidx.test.ext.junit.runners.AndroidJUnit4
import net.zetetic.database.sqlcipher.SQLiteDatabase
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.ForeignKeyConstraint
import org.signal.core.util.Index
import org.signal.core.util.getForeignKeys
import org.signal.core.util.getIndexes
import org.signal.core.util.readToList
import org.signal.core.util.requireNonNullString
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.testing.SignalActivityRule
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSQLiteDatabase
/**
* A test that guarantees that a freshly-created database looks the same as one that went through the upgrade path.
*/
@RunWith(AndroidJUnit4::class)
class DatabaseConsistencyTest {
@get:Rule
val harness = SignalActivityRule()
@Test
fun testUpgradeConsistency() {
val currentVersionStatements = SignalDatabase.rawDatabase.getAllCreateStatements()
val testHelper = InMemoryTestHelper(AppDependencies.application).also {
it.onUpgrade(it.writableDatabase, 181, SignalDatabaseMigrations.DATABASE_VERSION)
}
val upgradedStatements = testHelper.readableDatabase.getAllCreateStatements()
if (currentVersionStatements != upgradedStatements) {
var message = "\n"
val currentByName = currentVersionStatements.associateBy { it.name }
val upgradedByName = upgradedStatements.associateBy { it.name }
if (currentByName.keys != upgradedByName.keys) {
val exclusiveToCurrent = currentByName.keys - upgradedByName.keys
val exclusiveToUpgrade = upgradedByName.keys - currentByName.keys
message += "SQL entities exclusive to the newly-created database: $exclusiveToCurrent\n"
message += "SQL entities exclusive to the upgraded database: $exclusiveToUpgrade\n\n"
} else {
for (currentEntry in currentByName) {
val upgradedValue: Statement = upgradedByName[currentEntry.key]!!
if (upgradedValue.sql != currentEntry.value.sql) {
message += "Statement differed:\n"
message += "newly-created:\n"
message += "${currentEntry.value.sql}\n\n"
message += "upgraded:\n"
message += "${upgradedValue.sql}\n\n"
}
}
}
assertTrue(message, false)
}
}
@Test
fun testForeignKeyIndexCoverage() {
/** We may deem certain indexes non-critical if deletion frequency is low or table size is small. */
val ignoredColumns: List<Pair<String, String>> = listOf(
StorySendTable.TABLE_NAME to StorySendTable.DISTRIBUTION_ID
)
val foreignKeys: List<ForeignKeyConstraint> = SignalDatabase.rawDatabase.getForeignKeys()
val indexesByFirstColumn: List<Index> = SignalDatabase.rawDatabase.getIndexes()
val notFound: List<Pair<String, String>> = foreignKeys
.filterNot { ignoredColumns.contains(it.table to it.column) }
.filterNot { foreignKey ->
indexesByFirstColumn.hasPrimaryIndexFor(foreignKey.table, foreignKey.column)
}
.map { it.table to it.column }
assertTrue("Missing indexes to cover: $notFound", notFound.isEmpty())
}
private fun List<Index>.hasPrimaryIndexFor(table: String, column: String): Boolean {
return this.any { index -> index.table == table && index.columns[0] == column }
}
private data class Statement(
val name: String,
val sql: String
)
private fun SQLiteDatabase.getAllCreateStatements(): List<Statement> {
return this.rawQuery("SELECT name, sql FROM sqlite_schema WHERE sql NOT NULL AND name != 'sqlite_sequence'")
.readToList { cursor ->
Statement(
name = cursor.requireNonNullString("name"),
sql = cursor.requireNonNullString("sql").normalizeSql()
)
}
.filterNot { it.name.startsWith("sqlite_stat") }
.sortedBy { it.name }
}
private fun String.normalizeSql(): String {
return this
.split("\n")
.map { it.trim() }
.joinToString(separator = " ")
.replace(Regex.fromLiteral(" ,"), ",")
.replace(",([^\\s])".toRegex(), ", $1")
.replace(Regex("\\s+"), " ")
.replace(Regex.fromLiteral("( "), "(")
.replace(Regex.fromLiteral(" )"), ")")
.replace(Regex("CREATE TABLE \"([a-zA-Z_]+)\""), "CREATE TABLE $1") // for some reason SQLite will wrap table names in quotes for upgraded tables. This unwraps them.
}
private class InMemoryTestHelper(private val application: Application) : SQLiteOpenHelper(application, null, null, 1) {
override fun onCreate(db: SQLiteDatabase) {
for (statement in SNAPSHOT_V181) {
db.execSQL(statement.sql)
}
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
SignalDatabaseMigrations.migrate(application, SignalSQLiteDatabase(db), 181, SignalDatabaseMigrations.DATABASE_VERSION)
}
/**
* This is the list of statements that existed at version 181. Never change this.
*/
private val SNAPSHOT_V181 = listOf(
Statement(
name = "message",
sql = "CREATE TABLE message (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n date_server INTEGER DEFAULT -1,\n thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n recipient_device_id INTEGER,\n type INTEGER NOT NULL,\n body TEXT,\n read INTEGER DEFAULT 0,\n ct_l TEXT,\n exp INTEGER,\n m_type INTEGER,\n m_size INTEGER,\n st INTEGER,\n tr_id TEXT,\n subscription_id INTEGER DEFAULT -1, \n receipt_timestamp INTEGER DEFAULT -1, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n viewed_receipt_count INTEGER DEFAULT 0,\n mismatched_identities TEXT DEFAULT NULL,\n network_failures TEXT DEFAULT NULL,\n expires_in INTEGER DEFAULT 0,\n expire_started INTEGER DEFAULT 0,\n notified INTEGER DEFAULT 0,\n quote_id INTEGER DEFAULT 0,\n quote_author INTEGER DEFAULT 0,\n quote_body TEXT DEFAULT NULL,\n quote_missing INTEGER DEFAULT 0,\n quote_mentions BLOB DEFAULT NULL,\n quote_type INTEGER DEFAULT 0,\n shared_contacts TEXT DEFAULT NULL,\n unidentified INTEGER DEFAULT 0,\n link_previews TEXT DEFAULT NULL,\n view_once INTEGER DEFAULT 0,\n reactions_unread INTEGER DEFAULT 0,\n reactions_last_seen INTEGER DEFAULT -1,\n remote_deleted INTEGER DEFAULT 0,\n mentions_self INTEGER DEFAULT 0,\n notified_timestamp INTEGER DEFAULT 0,\n server_guid TEXT DEFAULT NULL,\n message_ranges BLOB DEFAULT NULL,\n story_type INTEGER DEFAULT 0,\n parent_story_id INTEGER DEFAULT 0,\n export_state BLOB DEFAULT NULL,\n exported INTEGER DEFAULT 0,\n scheduled_date INTEGER DEFAULT -1\n )"
),
Statement(
name = "part",
sql = "CREATE TABLE part (_id INTEGER PRIMARY KEY, mid INTEGER, seq INTEGER DEFAULT 0, ct TEXT, name TEXT, chset INTEGER, cd TEXT, fn TEXT, cid TEXT, cl TEXT, ctt_s INTEGER, ctt_t TEXT, encrypted INTEGER, pending_push INTEGER, _data TEXT, data_size INTEGER, file_name TEXT, unique_id INTEGER NOT NULL, digest BLOB, fast_preflight_id TEXT, voice_note INTEGER DEFAULT 0, borderless INTEGER DEFAULT 0, video_gif INTEGER DEFAULT 0, data_random BLOB, quote INTEGER DEFAULT 0, width INTEGER DEFAULT 0, height INTEGER DEFAULT 0, caption TEXT DEFAULT NULL, sticker_pack_id TEXT DEFAULT NULL, sticker_pack_key DEFAULT NULL, sticker_id INTEGER DEFAULT -1, sticker_emoji STRING DEFAULT NULL, data_hash TEXT DEFAULT NULL, blur_hash TEXT DEFAULT NULL, transform_properties TEXT DEFAULT NULL, transfer_file TEXT DEFAULT NULL, display_order INTEGER DEFAULT 0, upload_timestamp INTEGER DEFAULT 0, cdn_number INTEGER DEFAULT 0)"
),
Statement(
name = "thread",
sql = "CREATE TABLE thread (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n date INTEGER DEFAULT 0, \n meaningful_messages INTEGER DEFAULT 0,\n recipient_id INTEGER NOT NULL UNIQUE REFERENCES recipient (_id) ON DELETE CASCADE,\n read INTEGER DEFAULT 1, \n type INTEGER DEFAULT 0, \n error INTEGER DEFAULT 0, \n snippet TEXT, \n snippet_type INTEGER DEFAULT 0, \n snippet_uri TEXT DEFAULT NULL, \n snippet_content_type TEXT DEFAULT NULL, \n snippet_extras TEXT DEFAULT NULL, \n unread_count INTEGER DEFAULT 0, \n archived INTEGER DEFAULT 0, \n status INTEGER DEFAULT 0, \n delivery_receipt_count INTEGER DEFAULT 0, \n read_receipt_count INTEGER DEFAULT 0, \n expires_in INTEGER DEFAULT 0, \n last_seen INTEGER DEFAULT 0, \n has_sent INTEGER DEFAULT 0, \n last_scrolled INTEGER DEFAULT 0, \n pinned INTEGER DEFAULT 0, \n unread_self_mention_count INTEGER DEFAULT 0\n)"
),
Statement(
name = "identities",
sql = "CREATE TABLE identities (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address INTEGER UNIQUE, \n identity_key TEXT, \n first_use INTEGER DEFAULT 0, \n timestamp INTEGER DEFAULT 0, \n verified INTEGER DEFAULT 0, \n nonblocking_approval INTEGER DEFAULT 0\n )"
),
Statement(
name = "drafts",
sql = "CREATE TABLE drafts (\n _id INTEGER PRIMARY KEY, \n thread_id INTEGER, \n type TEXT, \n value TEXT\n )"
),
Statement(
name = "push",
sql = "CREATE TABLE push (_id INTEGER PRIMARY KEY, type INTEGER, source TEXT, source_uuid TEXT, device_id INTEGER, body TEXT, content TEXT, timestamp INTEGER, server_timestamp INTEGER DEFAULT 0, server_delivered_timestamp INTEGER DEFAULT 0, server_guid TEXT DEFAULT NULL)"
),
Statement(
name = "groups",
sql = "CREATE TABLE groups (\n _id INTEGER PRIMARY KEY, \n group_id TEXT, \n recipient_id INTEGER,\n title TEXT,\n avatar_id INTEGER, \n avatar_key BLOB,\n avatar_content_type TEXT, \n avatar_relay TEXT,\n timestamp INTEGER,\n active INTEGER DEFAULT 1,\n avatar_digest BLOB, \n mms INTEGER DEFAULT 0, \n master_key BLOB, \n revision BLOB, \n decrypted_group BLOB, \n expected_v2_id TEXT DEFAULT NULL, \n former_v1_members TEXT DEFAULT NULL, \n distribution_id TEXT DEFAULT NULL, \n display_as_story INTEGER DEFAULT 0, \n auth_service_id TEXT DEFAULT NULL, \n last_force_update_timestamp INTEGER DEFAULT 0\n )"
),
Statement(
name = "group_membership",
sql = "CREATE TABLE group_membership ( _id INTEGER PRIMARY KEY, group_id TEXT NOT NULL, recipient_id INTEGER NOT NULL, UNIQUE(group_id, recipient_id) )"
),
Statement(
name = "recipient",
sql = "CREATE TABLE recipient (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n uuid TEXT UNIQUE DEFAULT NULL,\n username TEXT UNIQUE DEFAULT NULL,\n phone TEXT UNIQUE DEFAULT NULL,\n email TEXT UNIQUE DEFAULT NULL,\n group_id TEXT UNIQUE DEFAULT NULL,\n group_type INTEGER DEFAULT 0,\n blocked INTEGER DEFAULT 0,\n message_ringtone TEXT DEFAULT NULL, \n message_vibrate INTEGER DEFAULT 0, \n call_ringtone TEXT DEFAULT NULL, \n call_vibrate INTEGER DEFAULT 0, \n notification_channel TEXT DEFAULT NULL, \n mute_until INTEGER DEFAULT 0, \n color TEXT DEFAULT NULL, \n seen_invite_reminder INTEGER DEFAULT 0,\n default_subscription_id INTEGER DEFAULT -1,\n message_expiration_time INTEGER DEFAULT 0,\n registered INTEGER DEFAULT 0,\n system_given_name TEXT DEFAULT NULL, \n system_family_name TEXT DEFAULT NULL, \n system_display_name TEXT DEFAULT NULL, \n system_photo_uri TEXT DEFAULT NULL, \n system_phone_label TEXT DEFAULT NULL, \n system_phone_type INTEGER DEFAULT -1, \n system_contact_uri TEXT DEFAULT NULL, \n system_info_pending INTEGER DEFAULT 0, \n profile_key TEXT DEFAULT NULL, \n profile_key_credential TEXT DEFAULT NULL, \n signal_profile_name TEXT DEFAULT NULL, \n profile_family_name TEXT DEFAULT NULL, \n profile_joined_name TEXT DEFAULT NULL, \n signal_profile_avatar TEXT DEFAULT NULL, \n profile_sharing INTEGER DEFAULT 0, \n last_profile_fetch INTEGER DEFAULT 0, \n unidentified_access_mode INTEGER DEFAULT 0, \n force_sms_selection INTEGER DEFAULT 0, \n storage_service_key TEXT UNIQUE DEFAULT NULL, \n mention_setting INTEGER DEFAULT 0, \n storage_proto TEXT DEFAULT NULL,\n capabilities INTEGER DEFAULT 0,\n last_session_reset BLOB DEFAULT NULL,\n wallpaper BLOB DEFAULT NULL,\n wallpaper_file TEXT DEFAULT NULL,\n about TEXT DEFAULT NULL,\n about_emoji TEXT DEFAULT NULL,\n extras BLOB DEFAULT NULL,\n groups_in_common INTEGER DEFAULT 0,\n chat_colors BLOB DEFAULT NULL,\n custom_chat_colors_id INTEGER DEFAULT 0,\n badges BLOB DEFAULT NULL,\n pni TEXT DEFAULT NULL,\n distribution_list_id INTEGER DEFAULT NULL,\n needs_pni_signature INTEGER DEFAULT 0,\n unregistered_timestamp INTEGER DEFAULT 0,\n hidden INTEGER DEFAULT 0,\n reporting_token BLOB DEFAULT NULL,\n system_nickname TEXT DEFAULT NULL\n)"
),
Statement(
name = "group_receipts",
sql = "CREATE TABLE group_receipts (\n _id INTEGER PRIMARY KEY, \n mms_id INTEGER, \n address INTEGER, \n status INTEGER, \n timestamp INTEGER, \n unidentified INTEGER DEFAULT 0\n )"
),
Statement(
name = "one_time_prekeys",
sql = "CREATE TABLE one_time_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL, \n private_key TEXT NOT NULL,\n UNIQUE(account_id, key_id)\n )"
),
Statement(
name = "signed_prekeys",
sql = "CREATE TABLE signed_prekeys (\n _id INTEGER PRIMARY KEY,\n account_id TEXT NOT NULL,\n key_id INTEGER UNIQUE, \n public_key TEXT NOT NULL,\n private_key TEXT NOT NULL,\n signature TEXT NOT NULL, \n timestamp INTEGER DEFAULT 0,\n UNIQUE(account_id, key_id)\n )"
),
Statement(
name = "sessions",
sql = "CREATE TABLE sessions (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n account_id TEXT NOT NULL,\n address TEXT NOT NULL,\n device INTEGER NOT NULL,\n record BLOB NOT NULL,\n UNIQUE(account_id, address, device)\n )"
),
Statement(
name = "sender_keys",
sql = "CREATE TABLE sender_keys (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n distribution_id TEXT NOT NULL,\n record BLOB NOT NULL, \n created_at INTEGER NOT NULL, \n UNIQUE(address,device, distribution_id) ON CONFLICT REPLACE\n )"
),
Statement(
name = "sender_key_shared",
sql = "CREATE TABLE sender_key_shared (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n distribution_id TEXT NOT NULL, \n address TEXT NOT NULL, \n device INTEGER NOT NULL, \n timestamp INTEGER DEFAULT 0, \n UNIQUE(distribution_id,address, device) ON CONFLICT REPLACE\n )"
),
Statement(
name = "pending_retry_receipts",
sql = "CREATE TABLE pending_retry_receipts(_id INTEGER PRIMARY KEY AUTOINCREMENT, author TEXT NOT NULL, device INTEGER NOT NULL, sent_timestamp INTEGER NOT NULL, received_timestamp TEXT NOT NULL, thread_id INTEGER NOT NULL, UNIQUE(author,sent_timestamp) ON CONFLICT REPLACE)"
),
Statement(
name = "sticker",
sql = "CREATE TABLE sticker (_id INTEGER PRIMARY KEY AUTOINCREMENT, pack_id TEXT NOT NULL, pack_key TEXT NOT NULL, pack_title TEXT NOT NULL, pack_author TEXT NOT NULL, sticker_id INTEGER, cover INTEGER, pack_order INTEGER, emoji TEXT NOT NULL, content_type TEXT DEFAULT NULL, last_used INTEGER, installed INTEGER,file_path TEXT NOT NULL, file_length INTEGER, file_random BLOB, UNIQUE(pack_id, sticker_id, cover) ON CONFLICT IGNORE)"
),
Statement(
name = "storage_key",
sql = "CREATE TABLE storage_key (_id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER, key TEXT UNIQUE)"
),
Statement(
name = "mention",
sql = "CREATE TABLE mention(_id INTEGER PRIMARY KEY AUTOINCREMENT, thread_id INTEGER, message_id INTEGER, recipient_id INTEGER, range_start INTEGER, range_length INTEGER)"
),
Statement(
name = "payments",
sql = "CREATE TABLE payments(_id INTEGER PRIMARY KEY, uuid TEXT DEFAULT NULL, recipient INTEGER DEFAULT 0, recipient_address TEXT DEFAULT NULL, timestamp INTEGER, note TEXT DEFAULT NULL, direction INTEGER, state INTEGER, failure_reason INTEGER, amount BLOB NOT NULL, fee BLOB NOT NULL, transaction_record BLOB DEFAULT NULL, receipt BLOB DEFAULT NULL, payment_metadata BLOB DEFAULT NULL, receipt_public_key TEXT DEFAULT NULL, block_index INTEGER DEFAULT 0, block_timestamp INTEGER DEFAULT 0, seen INTEGER, UNIQUE(uuid) ON CONFLICT ABORT)"
),
Statement(
name = "chat_colors",
sql = "CREATE TABLE chat_colors (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n chat_colors BLOB\n)"
),
Statement(
name = "emoji_search",
sql = "CREATE TABLE emoji_search (\n _id INTEGER PRIMARY KEY,\n label TEXT NOT NULL,\n emoji TEXT NOT NULL,\n rank INTEGER DEFAULT 2147483647 \n )"
),
Statement(
name = "avatar_picker",
sql = "CREATE TABLE avatar_picker (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n last_used INTEGER DEFAULT 0,\n group_id TEXT DEFAULT NULL,\n avatar BLOB NOT NULL\n)"
),
Statement(
name = "group_call_ring",
sql = "CREATE TABLE group_call_ring (\n _id INTEGER PRIMARY KEY,\n ring_id INTEGER UNIQUE,\n date_received INTEGER,\n ring_state INTEGER\n)"
),
Statement(
name = "reaction",
sql = "CREATE TABLE reaction (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n emoji TEXT NOT NULL,\n date_sent INTEGER NOT NULL,\n date_received INTEGER NOT NULL,\n UNIQUE(message_id, author_id) ON CONFLICT REPLACE\n)"
),
Statement(
name = "donation_receipt",
sql = "CREATE TABLE donation_receipt (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n receipt_type TEXT NOT NULL,\n receipt_date INTEGER NOT NULL,\n amount TEXT NOT NULL,\n currency TEXT NOT NULL,\n subscription_level INTEGER NOT NULL\n)"
),
Statement(
name = "story_sends",
sql = "CREATE TABLE story_sends (\n _id INTEGER PRIMARY KEY,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n allows_replies INTEGER NOT NULL,\n distribution_id TEXT NOT NULL REFERENCES distribution_list (distribution_id) ON DELETE CASCADE\n)"
),
Statement(
name = "cds",
sql = "CREATE TABLE cds (\n _id INTEGER PRIMARY KEY,\n e164 TEXT NOT NULL UNIQUE ON CONFLICT IGNORE,\n last_seen_at INTEGER DEFAULT 0\n )"
),
Statement(
name = "remote_megaphone",
sql = "CREATE TABLE remote_megaphone (\n _id INTEGER PRIMARY KEY,\n uuid TEXT UNIQUE NOT NULL,\n priority INTEGER NOT NULL,\n countries TEXT,\n minimum_version INTEGER NOT NULL,\n dont_show_before INTEGER NOT NULL,\n dont_show_after INTEGER NOT NULL,\n show_for_days INTEGER NOT NULL,\n conditional_id TEXT,\n primary_action_id TEXT,\n secondary_action_id TEXT,\n image_url TEXT,\n image_uri TEXT DEFAULT NULL,\n title TEXT NOT NULL,\n body TEXT NOT NULL,\n primary_action_text TEXT,\n secondary_action_text TEXT,\n shown_at INTEGER DEFAULT 0,\n finished_at INTEGER DEFAULT 0,\n primary_action_data TEXT DEFAULT NULL,\n secondary_action_data TEXT DEFAULT NULL,\n snoozed_at INTEGER DEFAULT 0,\n seen_count INTEGER DEFAULT 0\n)"
),
Statement(
name = "pending_pni_signature_message",
sql = "CREATE TABLE pending_pni_signature_message (\n _id INTEGER PRIMARY KEY,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n sent_timestamp INTEGER NOT NULL,\n device_id INTEGER NOT NULL\n )"
),
Statement(
name = "call",
sql = "CREATE TABLE call (\n _id INTEGER PRIMARY KEY,\n call_id INTEGER NOT NULL UNIQUE,\n message_id INTEGER NOT NULL REFERENCES message (_id) ON DELETE CASCADE,\n peer INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,\n type INTEGER NOT NULL,\n direction INTEGER NOT NULL,\n event INTEGER NOT NULL\n)"
),
Statement(
name = "message_fts",
sql = "CREATE VIRTUAL TABLE message_fts USING fts5(body, thread_id UNINDEXED, content=message, content_rowid=_id)"
),
Statement(
name = "remapped_recipients",
sql = "CREATE TABLE remapped_recipients (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )"
),
Statement(
name = "remapped_threads",
sql = "CREATE TABLE remapped_threads (\n _id INTEGER PRIMARY KEY AUTOINCREMENT, \n old_id INTEGER UNIQUE, \n new_id INTEGER\n )"
),
Statement(
name = "msl_payload",
sql = "CREATE TABLE msl_payload (\n _id INTEGER PRIMARY KEY,\n date_sent INTEGER NOT NULL,\n content BLOB NOT NULL,\n content_hint INTEGER NOT NULL,\n urgent INTEGER NOT NULL DEFAULT 1\n )"
),
Statement(
name = "msl_recipient",
sql = "CREATE TABLE msl_recipient (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL, \n device INTEGER NOT NULL\n )"
),
Statement(
name = "msl_message",
sql = "CREATE TABLE msl_message (\n _id INTEGER PRIMARY KEY,\n payload_id INTEGER NOT NULL REFERENCES msl_payload (_id) ON DELETE CASCADE,\n message_id INTEGER NOT NULL\n )"
),
Statement(
name = "notification_profile",
sql = "CREATE TABLE notification_profile (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n emoji TEXT NOT NULL,\n color TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n allow_all_calls INTEGER NOT NULL DEFAULT 0,\n allow_all_mentions INTEGER NOT NULL DEFAULT 0\n)"
),
Statement(
name = "notification_profile_schedule",
sql = "CREATE TABLE notification_profile_schedule (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n enabled INTEGER NOT NULL DEFAULT 0,\n start INTEGER NOT NULL,\n end INTEGER NOT NULL,\n days_enabled TEXT NOT NULL\n)"
),
Statement(
name = "notification_profile_allowed_members",
sql = "CREATE TABLE notification_profile_allowed_members (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n notification_profile_id INTEGER NOT NULL REFERENCES notification_profile (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL,\n UNIQUE(notification_profile_id, recipient_id) ON CONFLICT REPLACE\n)"
),
Statement(
name = "distribution_list",
sql = "CREATE TABLE distribution_list (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT UNIQUE NOT NULL,\n distribution_id TEXT UNIQUE NOT NULL,\n recipient_id INTEGER UNIQUE REFERENCES recipient (_id),\n allows_replies INTEGER DEFAULT 1,\n deletion_timestamp INTEGER DEFAULT 0,\n is_unknown INTEGER DEFAULT 0,\n privacy_mode INTEGER DEFAULT 0\n )"
),
Statement(
name = "distribution_list_member",
sql = "CREATE TABLE distribution_list_member (\n _id INTEGER PRIMARY KEY AUTOINCREMENT,\n list_id INTEGER NOT NULL REFERENCES distribution_list (_id) ON DELETE CASCADE,\n recipient_id INTEGER NOT NULL REFERENCES recipient (_id),\n privacy_mode INTEGER DEFAULT 0\n )"
),
Statement(
name = "recipient_group_type_index",
sql = "CREATE INDEX recipient_group_type_index ON recipient (group_type)"
),
Statement(
name = "recipient_pni_index",
sql = "CREATE UNIQUE INDEX recipient_pni_index ON recipient (pni)"
),
Statement(
name = "recipient_service_id_profile_key",
sql = "CREATE INDEX recipient_service_id_profile_key ON recipient (uuid, profile_key) WHERE uuid NOT NULL AND profile_key NOT NULL"
),
Statement(
name = "mms_read_and_notified_and_thread_id_index",
sql = "CREATE INDEX mms_read_and_notified_and_thread_id_index ON message (read, notified, thread_id)"
),
Statement(
name = "mms_type_index",
sql = "CREATE INDEX mms_type_index ON message (type)"
),
Statement(
name = "mms_date_sent_index",
sql = "CREATE INDEX mms_date_sent_index ON message (date_sent, recipient_id, thread_id)"
),
Statement(
name = "mms_date_server_index",
sql = "CREATE INDEX mms_date_server_index ON message (date_server)"
),
Statement(
name = "mms_thread_date_index",
sql = "CREATE INDEX mms_thread_date_index ON message (thread_id, date_received)"
),
Statement(
name = "mms_reactions_unread_index",
sql = "CREATE INDEX mms_reactions_unread_index ON message (reactions_unread)"
),
Statement(
name = "mms_story_type_index",
sql = "CREATE INDEX mms_story_type_index ON message (story_type)"
),
Statement(
name = "mms_parent_story_id_index",
sql = "CREATE INDEX mms_parent_story_id_index ON message (parent_story_id)"
),
Statement(
name = "mms_thread_story_parent_story_scheduled_date_index",
sql = "CREATE INDEX mms_thread_story_parent_story_scheduled_date_index ON message (thread_id, date_received, story_type, parent_story_id, scheduled_date)"
),
Statement(
name = "message_quote_id_quote_author_scheduled_date_index",
sql = "CREATE INDEX message_quote_id_quote_author_scheduled_date_index ON message (quote_id, quote_author, scheduled_date)"
),
Statement(
name = "mms_exported_index",
sql = "CREATE INDEX mms_exported_index ON message (exported)"
),
Statement(
name = "mms_id_type_payment_transactions_index",
sql = "CREATE INDEX mms_id_type_payment_transactions_index ON message (_id,type) WHERE type & 12884901888 != 0"
),
Statement(
name = "part_mms_id_index",
sql = "CREATE INDEX part_mms_id_index ON part (mid)"
),
Statement(
name = "pending_push_index",
sql = "CREATE INDEX pending_push_index ON part (pending_push)"
),
Statement(
name = "part_sticker_pack_id_index",
sql = "CREATE INDEX part_sticker_pack_id_index ON part (sticker_pack_id)"
),
Statement(
name = "part_data_hash_index",
sql = "CREATE INDEX part_data_hash_index ON part (data_hash)"
),
Statement(
name = "part_data_index",
sql = "CREATE INDEX part_data_index ON part (_data)"
),
Statement(
name = "thread_recipient_id_index",
sql = "CREATE INDEX thread_recipient_id_index ON thread (recipient_id)"
),
Statement(
name = "archived_count_index",
sql = "CREATE INDEX archived_count_index ON thread (archived, meaningful_messages)"
),
Statement(
name = "thread_pinned_index",
sql = "CREATE INDEX thread_pinned_index ON thread (pinned)"
),
Statement(
name = "thread_read",
sql = "CREATE INDEX thread_read ON thread (read)"
),
Statement(
name = "draft_thread_index",
sql = "CREATE INDEX draft_thread_index ON drafts (thread_id)"
),
Statement(
name = "group_id_index",
sql = "CREATE UNIQUE INDEX group_id_index ON groups (group_id)"
),
Statement(
name = "group_recipient_id_index",
sql = "CREATE UNIQUE INDEX group_recipient_id_index ON groups (recipient_id)"
),
Statement(
name = "expected_v2_id_index",
sql = "CREATE UNIQUE INDEX expected_v2_id_index ON groups (expected_v2_id)"
),
Statement(
name = "group_distribution_id_index",
sql = "CREATE UNIQUE INDEX group_distribution_id_index ON groups(distribution_id)"
),
Statement(
name = "group_receipt_mms_id_index",
sql = "CREATE INDEX group_receipt_mms_id_index ON group_receipts (mms_id)"
),
Statement(
name = "sticker_pack_id_index",
sql = "CREATE INDEX sticker_pack_id_index ON sticker (pack_id)"
),
Statement(
name = "sticker_sticker_id_index",
sql = "CREATE INDEX sticker_sticker_id_index ON sticker (sticker_id)"
),
Statement(
name = "storage_key_type_index",
sql = "CREATE INDEX storage_key_type_index ON storage_key (type)"
),
Statement(
name = "mention_message_id_index",
sql = "CREATE INDEX mention_message_id_index ON mention (message_id)"
),
Statement(
name = "mention_recipient_id_thread_id_index",
sql = "CREATE INDEX mention_recipient_id_thread_id_index ON mention (recipient_id, thread_id)"
),
Statement(
name = "timestamp_direction_index",
sql = "CREATE INDEX timestamp_direction_index ON payments (timestamp, direction)"
),
Statement(
name = "timestamp_index",
sql = "CREATE INDEX timestamp_index ON payments (timestamp)"
),
Statement(
name = "receipt_public_key_index",
sql = "CREATE UNIQUE INDEX receipt_public_key_index ON payments (receipt_public_key)"
),
Statement(
name = "msl_payload_date_sent_index",
sql = "CREATE INDEX msl_payload_date_sent_index ON msl_payload (date_sent)"
),
Statement(
name = "msl_recipient_recipient_index",
sql = "CREATE INDEX msl_recipient_recipient_index ON msl_recipient (recipient_id, device, payload_id)"
),
Statement(
name = "msl_recipient_payload_index",
sql = "CREATE INDEX msl_recipient_payload_index ON msl_recipient (payload_id)"
),
Statement(
name = "msl_message_message_index",
sql = "CREATE INDEX msl_message_message_index ON msl_message (message_id, payload_id)"
),
Statement(
name = "date_received_index",
sql = "CREATE INDEX date_received_index on group_call_ring (date_received)"
),
Statement(
name = "notification_profile_schedule_profile_index",
sql = "CREATE INDEX notification_profile_schedule_profile_index ON notification_profile_schedule (notification_profile_id)"
),
Statement(
name = "notification_profile_allowed_members_profile_index",
sql = "CREATE INDEX notification_profile_allowed_members_profile_index ON notification_profile_allowed_members (notification_profile_id)"
),
Statement(
name = "donation_receipt_type_index",
sql = "CREATE INDEX donation_receipt_type_index ON donation_receipt (receipt_type)"
),
Statement(
name = "donation_receipt_date_index",
sql = "CREATE INDEX donation_receipt_date_index ON donation_receipt (receipt_date)"
),
Statement(
name = "story_sends_recipient_id_sent_timestamp_allows_replies_index",
sql = "CREATE INDEX story_sends_recipient_id_sent_timestamp_allows_replies_index ON story_sends (recipient_id, sent_timestamp, allows_replies)"
),
Statement(
name = "story_sends_message_id_distribution_id_index",
sql = "CREATE INDEX story_sends_message_id_distribution_id_index ON story_sends (message_id, distribution_id)"
),
Statement(
name = "distribution_list_member_list_id_recipient_id_privacy_mode_index",
sql = "CREATE UNIQUE INDEX distribution_list_member_list_id_recipient_id_privacy_mode_index ON distribution_list_member (list_id, recipient_id, privacy_mode)"
),
Statement(
name = "pending_pni_recipient_sent_device_index",
sql = "CREATE UNIQUE INDEX pending_pni_recipient_sent_device_index ON pending_pni_signature_message (recipient_id, sent_timestamp, device_id)"
),
Statement(
name = "call_call_id_index",
sql = "CREATE INDEX call_call_id_index ON call (call_id)"
),
Statement(
name = "call_message_id_index",
sql = "CREATE INDEX call_message_id_index ON call (message_id)"
),
Statement(
name = "message_ai",
sql = "CREATE TRIGGER message_ai AFTER INSERT ON message BEGIN\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END"
),
Statement(
name = "message_ad",
sql = "CREATE TRIGGER message_ad AFTER DELETE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n END"
),
Statement(
name = "message_au",
sql = "CREATE TRIGGER message_au AFTER UPDATE ON message BEGIN\n INSERT INTO message_fts(message_fts, rowid, body, thread_id) VALUES('delete', old._id, old.body, old.thread_id);\n INSERT INTO message_fts(rowid, body, thread_id) VALUES (new._id, new.body, new.thread_id);\n END"
),
Statement(
name = "msl_message_delete",
sql = "CREATE TRIGGER msl_message_delete AFTER DELETE ON message \n BEGIN \n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old._id);\n END"
),
Statement(
name = "msl_attachment_delete",
sql = "CREATE TRIGGER msl_attachment_delete AFTER DELETE ON part\n BEGIN\n \tDELETE FROM msl_payload WHERE _id IN (SELECT payload_id FROM msl_message WHERE message_id = old.mid);\n END"
)
)
}
}

View File

@@ -313,7 +313,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
timestamp = wallClock,
groupId = groupId,
update = updateDescription,
isGroupAdd = false,
isNotifiable = false,
serverGuid = null
)
}

View File

@@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationM
import org.thoughtcrime.securesms.conversation.v2.data.ConversationElementKey
import org.thoughtcrime.securesms.conversation.v2.data.IncomingTextOnly
import org.thoughtcrime.securesms.conversation.v2.data.OutgoingTextOnly
import org.thoughtcrime.securesms.database.CollapsedState
import org.thoughtcrime.securesms.database.MessageTypes
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.StoryType
@@ -122,7 +123,10 @@ class ConversationElementGenerator {
false,
0,
null,
null
CollapsedState.NONE,
0,
null,
false
)
val conversationMessage = ConversationMessageFactory.createWithUnresolvedData(

View File

@@ -353,5 +353,13 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra
override fun onViewPinnedMessage(messageId: Long) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onExpandEvents(messageId: Long) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
override fun onCollapseEvents(messageId: Long) {
Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show()
}
}
}

View File

@@ -458,6 +458,13 @@
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".search.SingleContactSelectionActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"
android:label="@string/AndroidManifest__select_contacts"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".giph.ui.GiphyActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
@@ -668,6 +675,13 @@
android:theme="@style/TextSecure.DarkNoActionBar"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".starred.StarredMessagesActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="false"
android:theme="@style/Theme.Signal.DayNight.NoActionBar"
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".mediaoverview.MediaOverviewActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 176 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

@@ -148,5 +148,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
void onViewPollClicked(long messageId);
void onToggleVote(@NonNull PollRecord poll, @NonNull PollOption pollOption, Boolean isChecked);
void onViewPinnedMessage(long messageId);
void onExpandEvents(long messageId);
void onCollapseEvents(long messageId);
}
}

View File

@@ -76,7 +76,7 @@ public final class BlockUnblockDialog {
Resources resources = context.getResources();
if (recipient.isGroup()) {
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
if (SignalDatabase.groups().isMember(recipient.requireGroupId())) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_and_leave_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_you_will_no_longer_receive_messages_or_updates);
builder.setPositiveButton(R.string.BlockUnblockDialog_block_and_leave, ((dialog, which) -> onBlock.run()));
@@ -121,7 +121,7 @@ public final class BlockUnblockDialog {
Resources resources = context.getResources();
if (recipient.isGroup()) {
if (SignalDatabase.groups().isActive(recipient.requireGroupId())) {
if (SignalDatabase.groups().isMember(recipient.requireGroupId())) {
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
builder.setMessage(R.string.BlockUnblockDialog_group_members_will_be_able_to_add_you);
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));

View File

@@ -90,6 +90,7 @@ import org.signal.core.ui.isSplitPane
import org.signal.core.ui.permissions.Permissions
import org.signal.core.util.Util
import org.signal.core.util.concurrent.LifecycleDisposable
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.getSerializableCompat
import org.signal.core.util.logging.Log
import org.signal.donations.StripeApi
@@ -142,6 +143,7 @@ import org.thoughtcrime.securesms.main.MainNavigationDetailLocation
import org.thoughtcrime.securesms.main.MainNavigationDetailLocationEffect
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.main.MainNavigationRail
import org.thoughtcrime.securesms.main.MainNavigationRouter
import org.thoughtcrime.securesms.main.MainNavigationViewModel
import org.thoughtcrime.securesms.main.MainSnackbar
import org.thoughtcrime.securesms.main.MainSnackbarHostKey
@@ -169,6 +171,7 @@ import org.thoughtcrime.securesms.notifications.profiles.NotificationProfiles
import org.thoughtcrime.securesms.profiles.manage.UsernameEditFragment
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
import org.thoughtcrime.securesms.service.KeyCachingService
import org.thoughtcrime.securesms.starred.StarredMessagesActivity
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.stories.archive.StoryArchiveActivity
import org.thoughtcrime.securesms.stories.landing.StoriesLandingFragment
@@ -192,12 +195,21 @@ import org.thoughtcrime.securesms.window.rememberThreePaneScaffoldNavigatorDeleg
import org.whispersystems.signalservice.api.websocket.WebSocketConnectionState
import org.signal.core.ui.R as CoreUiR
class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner, MainNavigator.NavigatorProvider, Material3OnScrollHelperBinder, ConversationListFragment.Callback, CallLogFragment.Callback, GooglePayComponent {
class MainActivity :
PassphraseRequiredActivity(),
VoiceNoteMediaControllerOwner,
MainNavigator.NavigatorProvider,
Material3OnScrollHelperBinder,
ConversationListFragment.Callback,
MainNavigationRouter,
CallLogFragment.Callback,
GooglePayComponent {
companion object {
private val TAG = Log.tag(MainActivity::class)
private const val KEY_STARTING_TAB = "STARTING_TAB"
private const val KEY_DETAIL_LOCATION = "DETAIL_LOCATION"
const val RESULT_CONFIG_CHANGED = Activity.RESULT_FIRST_USER + 901
@JvmStatic
@@ -210,6 +222,11 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
fun clearTopAndOpenTab(context: Context, startingTab: MainNavigationListLocation): Intent {
return clearTop(context).putExtra(KEY_STARTING_TAB, startingTab)
}
@JvmStatic
fun clearTopAndOpenDetail(context: Context, location: MainNavigationDetailLocation): Intent {
return clearTop(context).putExtra(KEY_DETAIL_LOCATION, location)
}
}
private val dynamicTheme = DynamicNoActionBarTheme()
@@ -496,6 +513,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
is MainNavigationDetailLocation.Calls -> callsNavHostController.navigateToDetailLocation(location)
is MainNavigationDetailLocation.Stories -> storiesNavHostController.navigateToDetailLocation(location)
}
}
@@ -811,6 +829,13 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
handleDeepLinkIntent(intent)
val extras = intent.extras ?: return
val detailLocation = extras.getParcelableCompat(KEY_DETAIL_LOCATION, MainNavigationDetailLocation::class.java)
if (detailLocation != null) {
mainNavigationViewModel.goTo(detailLocation)
return
}
val startingTab = extras.getSerializableCompat(KEY_STARTING_TAB, MainNavigationListLocation::class.java)
when (startingTab) {
@@ -1148,6 +1173,10 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
toolbarViewModel.setChatFilter(ConversationFilter.OFF)
}
override fun onStarredMessagesClick() {
startActivity(StarredMessagesActivity.createIntent(this@MainActivity))
}
override fun onSettingsClick() {
openSettings.launch(AppSettingsActivity.home(this@MainActivity))
}
@@ -1205,6 +1234,14 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
toolbarViewModel.setSearchQuery(query)
}
override fun onSearchFilterClick() {
supportFragmentManager.fragments.forEach { fragment ->
if (fragment is ConversationListFragment) {
fragment.showSearchFilterBottomSheet()
}
}
}
override fun onNotificationProfileTooltipDismissed() {
SignalStore.notificationProfile.hasSeenTooltip = true
toolbarViewModel.setShowNotificationProfilesTooltip(false)
@@ -1276,4 +1313,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
}
}
}
override fun goTo(location: MainNavigationListLocation) = mainNavigationViewModel.goTo(location)
override fun goTo(location: MainNavigationDetailLocation) = mainNavigationViewModel.goTo(location)
}

View File

@@ -5,6 +5,6 @@
package org.thoughtcrime.securesms.backup.v2
typealias ArchiveRecipient = org.thoughtcrime.securesms.backup.v2.proto.Recipient
typealias ArchiveGroup = org.thoughtcrime.securesms.backup.v2.proto.Group
typealias ArchiveCallLink = org.thoughtcrime.securesms.backup.v2.proto.CallLink
typealias ArchiveRecipient = org.signal.archive.proto.Recipient
typealias ArchiveGroup = org.signal.archive.proto.Group
typealias ArchiveCallLink = org.signal.archive.proto.CallLink

View File

@@ -18,9 +18,19 @@ import kotlinx.coroutines.withContext
import okio.ByteString
import okio.ByteString.Companion.toByteString
import org.greenrobot.eventbus.EventBus
import org.signal.archive.proto.BackupDebugInfo
import org.signal.archive.proto.BackupInfo
import org.signal.archive.proto.Frame
import org.signal.archive.stream.BackupExportWriter
import org.signal.archive.stream.BackupImportReader
import org.signal.archive.stream.EncryptedBackupReader
import org.signal.archive.stream.EncryptedBackupWriter
import org.signal.archive.stream.PlainTextBackupReader
import org.signal.archive.stream.PlainTextBackupWriter
import org.signal.core.models.AccountEntropyPool
import org.signal.core.models.ServiceId.ACI
import org.signal.core.models.ServiceId.PNI
import org.signal.core.models.backup.BackupId
import org.signal.core.models.backup.MediaName
import org.signal.core.models.backup.MediaRootBackupKey
import org.signal.core.models.backup.MessageBackupKey
@@ -75,15 +85,6 @@ import org.thoughtcrime.securesms.backup.v2.processor.ChatItemArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.NotificationProfileArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.RecipientArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.processor.StickerArchiveProcessor
import org.thoughtcrime.securesms.backup.v2.proto.BackupDebugInfo
import org.thoughtcrime.securesms.backup.v2.proto.BackupInfo
import org.thoughtcrime.securesms.backup.v2.proto.Frame
import org.thoughtcrime.securesms.backup.v2.stream.BackupExportWriter
import org.thoughtcrime.securesms.backup.v2.stream.BackupImportReader
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.EncryptedBackupWriter
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupReader
import org.thoughtcrime.securesms.backup.v2.stream.PlainTextBackupWriter
import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
@@ -109,6 +110,7 @@ import org.thoughtcrime.securesms.jobs.ArchiveAttachmentBackfillJob
import org.thoughtcrime.securesms.jobs.ArchiveThumbnailBackfillJob
import org.thoughtcrime.securesms.jobs.ArchiveThumbnailUploadJob
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob
import org.thoughtcrime.securesms.jobs.BackfillCollapsedMessageJob
import org.thoughtcrime.securesms.jobs.BackupDeleteJob
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
@@ -812,6 +814,34 @@ object BackupRepository {
}
}
@WorkerThread
fun exportForLocalPlaintextArchive(
outputStream: OutputStream,
progressEmitter: ExportProgressListener?,
cancellationSignal: () -> Boolean,
includeMedia: Boolean
): List<AttachmentTable.LocalArchivableAttachment> {
val writer = LibSignalJsonBackupWriter(NonClosingOutputStream(outputStream))
val collectedAttachments = mutableListOf<AttachmentTable.LocalArchivableAttachment>()
export(
currentTime = System.currentTimeMillis(),
isLocal = true,
writer = writer,
backupMode = BackupMode.PLAINTEXT_EXPORT,
progressEmitter = progressEmitter,
cancellationSignal = cancellationSignal,
extraFrameOperation = null,
messageInclusionCutoffTime = 0
) { dbSnapshot ->
if (includeMedia) {
collectedAttachments.addAll(dbSnapshot.attachmentTable.getLocalArchivableAttachmentsForPlaintextExport())
}
}
return collectedAttachments
}
/**
* Export a backup that will be uploaded to the archive CDN.
*/
@@ -1088,13 +1118,13 @@ object BackupRepository {
/**
* Imports a local backup file that was exported to disk.
*/
fun importLocal(mainStreamFactory: () -> InputStream, mainStreamLength: Long, selfData: SelfData): ImportResult {
val backupKey = SignalStore.backup.messageBackupKey
fun importLocal(mainStreamFactory: () -> InputStream, mainStreamLength: Long, selfData: SelfData, backupId: BackupId, messageBackupKey: MessageBackupKey): ImportResult {
val backupKey = messageBackupKey
val frameReader = try {
EncryptedBackupReader.createForLocalOrLinking(
key = backupKey,
aci = selfData.aci,
backupId = backupId,
length = mainStreamLength,
dataStream = mainStreamFactory
)
@@ -1310,51 +1340,59 @@ object BackupRepository {
val totalLength = frameReader.getStreamLength()
var frameCount = 0
for (frame in frameReader) {
val frameAccount = frame.account
val frameRecipient = frame.recipient
val frameChat = frame.chat
val frameAdHocCall = frame.adHocCall
val frameStickerPack = frame.stickerPack
val frameNotificationProfile = frame.notificationProfile
val frameChatFolder = frame.chatFolder
val frameChatItem = frame.chatItem
when {
frame.account != null -> {
AccountDataArchiveProcessor.import(frame.account, selfId, importState)
frameAccount != null -> {
AccountDataArchiveProcessor.import(frameAccount, selfId, importState)
eventTimer.emit("account")
frameCount++
}
frame.recipient != null -> {
RecipientArchiveProcessor.import(frame.recipient, importState)
frameRecipient != null -> {
RecipientArchiveProcessor.import(frameRecipient, importState)
eventTimer.emit("recipient")
frameCount++
}
frame.chat != null -> {
ChatArchiveProcessor.import(frame.chat, importState)
frameChat != null -> {
ChatArchiveProcessor.import(frameChat, importState)
eventTimer.emit("chat")
frameCount++
}
frame.adHocCall != null -> {
AdHocCallArchiveProcessor.import(frame.adHocCall, importState)
frameAdHocCall != null -> {
AdHocCallArchiveProcessor.import(frameAdHocCall, importState)
eventTimer.emit("call")
frameCount++
}
frame.stickerPack != null -> {
StickerArchiveProcessor.import(frame.stickerPack)
frameStickerPack != null -> {
StickerArchiveProcessor.import(frameStickerPack)
eventTimer.emit("sticker-pack")
frameCount++
}
frame.notificationProfile != null -> {
NotificationProfileArchiveProcessor.import(frame.notificationProfile, importState)
frameNotificationProfile != null -> {
NotificationProfileArchiveProcessor.import(frameNotificationProfile, importState)
eventTimer.emit("notification-profile")
frameCount++
}
frame.chatFolder != null -> {
ChatFolderArchiveProcessor.import(frame.chatFolder, importState)
frameChatFolder != null -> {
ChatFolderArchiveProcessor.import(frameChatFolder, importState)
eventTimer.emit("chat-folder")
frameCount++
}
frame.chatItem != null -> {
chatItemInserter.import(frame.chatItem)
frameChatItem != null -> {
chatItemInserter.import(frameChatItem)
eventTimer.emit("chatItem")
frameCount++
@@ -1491,6 +1529,10 @@ object BackupRepository {
AppDependencies.jobManager.addAll(groupJobs)
stopwatch.split("group-jobs")
if (RemoteConfig.collapseEvents) {
AppDependencies.jobManager.add(BackfillCollapsedMessageJob())
}
SignalStore.backup.firstAppVersion = header.firstAppVersion
SignalStore.internal.importedBackupDebugInfo = header.debugInfo.let { BackupDebugInfo.ADAPTER.decodeOrNull(it.toByteArray()) }
@@ -2522,7 +2564,8 @@ sealed interface RestoreTimestampResult {
enum class BackupMode {
REMOTE,
LINK_SYNC,
LOCAL;
LOCAL,
PLAINTEXT_EXPORT;
val isLinkAndSync: Boolean
get() = this == LINK_SYNC

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.backup.v2
import org.signal.archive.proto.BackupInfo
import org.signal.archive.proto.Frame
import org.signal.archive.stream.BackupExportWriter
import org.signal.core.util.logging.Log
import org.signal.core.util.writeVarInt32
import org.signal.libsignal.messagebackup.BackupJsonExporter
import java.io.ByteArrayOutputStream
import java.io.OutputStream
/**
* A [BackupExportWriter] that serializes frames to newline-delimited JSON (JSONL) using
* libsignal's [BackupJsonExporter], which applies sanitization (strips disappearing messages
* and view-once attachments) and optional validation.
*/
class LibSignalJsonBackupWriter(private val outputStream: OutputStream) : BackupExportWriter {
private val TAG = Log.tag(LibSignalJsonBackupWriter::class)
private var exporter: BackupJsonExporter? = null
override fun write(header: BackupInfo) {
val (newExporter, initialChunk) = BackupJsonExporter.start(header.encode())
exporter = newExporter
outputStream.write(initialChunk.toByteArray())
outputStream.write("\n".toByteArray())
}
override fun write(frame: Frame) {
val frameBytes = frame.encode()
val buf = ByteArrayOutputStream(frameBytes.size + 5)
buf.writeVarInt32(frameBytes.size)
buf.write(frameBytes)
val results = exporter!!.exportFrames(buf.toByteArray())
for (result in results) {
result.line?.let {
outputStream.write(it.toByteArray())
outputStream.write("\n".toByteArray())
}
result.errorMessage?.let {
Log.w(TAG, "Frame validation warning: $it")
}
}
}
override fun close() {
exporter?.use {
val error = it.finishExport()
if (error != null) {
Log.w(TAG, "Backup export validation error: $error")
}
}
outputStream.close()
}
}

View File

@@ -164,7 +164,13 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, self
MessageTable.DELETED_BY
)
.from("${MessageTable.TABLE_NAME} INDEXED BY $dateReceivedIndex")
.where("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND ($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds}) AND $DATE_RECEIVED >= $lastSeenReceivedTime $cutoffQuery")
.where(
buildString {
append("$STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND ")
append("($EXPIRES_IN == 0 OR $EXPIRES_IN > ${1.days.inWholeMilliseconds})")
append(" AND $DATE_RECEIVED >= $lastSeenReceivedTime $cutoffQuery")
}
)
.limit(count)
.orderBy("$DATE_RECEIVED ASC")
.run()

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.backup.v2.database
import android.content.ContentValues
import org.signal.archive.proto.AccountData
import org.signal.core.models.ServiceId
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
@@ -15,7 +16,6 @@ import org.signal.core.util.update
import org.signal.libsignal.zkgroup.InvalidInputException
import org.thoughtcrime.securesms.backup.v2.exporters.ContactArchiveExporter
import org.thoughtcrime.securesms.backup.v2.exporters.GroupArchiveExporter
import org.thoughtcrime.securesms.backup.v2.proto.AccountData
import org.thoughtcrime.securesms.database.GroupTable
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.RecipientTable
@@ -97,7 +97,7 @@ fun RecipientTable.getGroupsForBackup(selfAci: ServiceId.ACI): GroupArchiveExpor
"${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY}",
"${GroupTable.TABLE_NAME}.${GroupTable.SHOW_AS_STORY_STATE}",
"${GroupTable.TABLE_NAME}.${GroupTable.TITLE}",
"${GroupTable.TABLE_NAME}.${GroupTable.ACTIVE}",
"${GroupTable.TABLE_NAME}.${GroupTable.IS_MEMBER}",
"${GroupTable.TABLE_NAME}.${GroupTable.V2_DECRYPTED_GROUP}"
)
.from(

View File

@@ -6,8 +6,8 @@
package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import org.signal.archive.proto.AdHocCall
import org.signal.core.util.requireLong
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
import org.thoughtcrime.securesms.backup.v2.util.clampToValidBackupRange
import org.thoughtcrime.securesms.database.CallTable
import java.io.Closeable

View File

@@ -7,10 +7,10 @@ package org.thoughtcrime.securesms.backup.v2.database
import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.signal.archive.proto.CallLink
import org.signal.core.util.nullIfEmpty
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.backup.v2.ArchiveRecipient
import org.thoughtcrime.securesms.backup.v2.proto.CallLink
import org.thoughtcrime.securesms.backup.v2.util.clampToValidBackupRange
import org.thoughtcrime.securesms.database.CallLinkTable
import java.io.Closeable

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.backup.v2.exporters
import android.database.Cursor
import org.signal.archive.proto.Chat
import org.signal.core.util.decodeOrNull
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
@@ -13,7 +14,6 @@ import org.signal.core.util.requireInt
import org.signal.core.util.requireIntOrNull
import org.signal.core.util.requireLong
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.util.ChatStyleConverter
import org.thoughtcrime.securesms.backup.v2.util.isValid
import org.thoughtcrime.securesms.conversation.colors.ChatColors

View File

@@ -9,6 +9,38 @@ import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.json.JSONArray
import org.json.JSONException
import org.signal.archive.proto.AdminDeletedMessage
import org.signal.archive.proto.ChatItem
import org.signal.archive.proto.ChatUpdateMessage
import org.signal.archive.proto.ContactAttachment
import org.signal.archive.proto.ContactMessage
import org.signal.archive.proto.DirectStoryReplyMessage
import org.signal.archive.proto.ExpirationTimerChatUpdate
import org.signal.archive.proto.GenericGroupUpdate
import org.signal.archive.proto.GroupCall
import org.signal.archive.proto.GroupChangeChatUpdate
import org.signal.archive.proto.GroupExpirationTimerUpdate
import org.signal.archive.proto.GroupV2MigrationUpdate
import org.signal.archive.proto.IndividualCall
import org.signal.archive.proto.LearnedProfileChatUpdate
import org.signal.archive.proto.MessageAttachment
import org.signal.archive.proto.PaymentNotification
import org.signal.archive.proto.PinMessageUpdate
import org.signal.archive.proto.Poll
import org.signal.archive.proto.PollTerminateUpdate
import org.signal.archive.proto.ProfileChangeChatUpdate
import org.signal.archive.proto.Quote
import org.signal.archive.proto.Reaction
import org.signal.archive.proto.RemoteDeletedMessage
import org.signal.archive.proto.SendStatus
import org.signal.archive.proto.SessionSwitchoverChatUpdate
import org.signal.archive.proto.SimpleChatUpdate
import org.signal.archive.proto.StandardMessage
import org.signal.archive.proto.Sticker
import org.signal.archive.proto.StickerMessage
import org.signal.archive.proto.Text
import org.signal.archive.proto.ThreadMergeChatUpdate
import org.signal.archive.proto.ViewOnceMessage
import org.signal.core.models.ServiceId
import org.signal.core.util.Base64
import org.signal.core.util.EventTimer
@@ -41,38 +73,6 @@ import org.thoughtcrime.securesms.backup.v2.BackupMode
import org.thoughtcrime.securesms.backup.v2.ExportOddities
import org.thoughtcrime.securesms.backup.v2.ExportSkips
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.proto.AdminDeletedMessage
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment
import org.thoughtcrime.securesms.backup.v2.proto.ContactMessage
import org.thoughtcrime.securesms.backup.v2.proto.DirectStoryReplyMessage
import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GenericGroupUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
import org.thoughtcrime.securesms.backup.v2.proto.GroupChangeChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupExpirationTimerUpdate
import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationUpdate
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.LearnedProfileChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification
import org.thoughtcrime.securesms.backup.v2.proto.PinMessageUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Poll
import org.thoughtcrime.securesms.backup.v2.proto.PollTerminateUpdate
import org.thoughtcrime.securesms.backup.v2.proto.ProfileChangeChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.Quote
import org.thoughtcrime.securesms.backup.v2.proto.Reaction
import org.thoughtcrime.securesms.backup.v2.proto.RemoteDeletedMessage
import org.thoughtcrime.securesms.backup.v2.proto.SendStatus
import org.thoughtcrime.securesms.backup.v2.proto.SessionSwitchoverChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.SimpleChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.StandardMessage
import org.thoughtcrime.securesms.backup.v2.proto.Sticker
import org.thoughtcrime.securesms.backup.v2.proto.StickerMessage
import org.thoughtcrime.securesms.backup.v2.proto.Text
import org.thoughtcrime.securesms.backup.v2.proto.ThreadMergeChatUpdate
import org.thoughtcrime.securesms.backup.v2.proto.ViewOnceMessage
import org.thoughtcrime.securesms.backup.v2.util.clampToValidBackupRange
import org.thoughtcrime.securesms.backup.v2.util.toRemoteFilePointer
import org.thoughtcrime.securesms.contactshare.Contact
@@ -117,8 +117,8 @@ import java.util.concurrent.ExecutorService
import java.util.concurrent.Future
import kotlin.math.max
import kotlin.time.Duration.Companion.days
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange as BackupBodyRange
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
import org.signal.archive.proto.BodyRange as BackupBodyRange
import org.signal.archive.proto.GiftBadge as BackupGiftBadge
private val TAG = Log.tag(ChatItemArchiveExporter::class.java)
private val MAX_INLINED_BODY_SIZE = 128.kibiBytes.bytes.toInt()
@@ -958,8 +958,8 @@ private fun BackupMessageRecord.toRemoteLinkPreviews(attachments: List<DatabaseA
return emptyList()
}
private fun LinkPreview.toRemoteLinkPreview(backupMode: BackupMode): org.thoughtcrime.securesms.backup.v2.proto.LinkPreview {
return org.thoughtcrime.securesms.backup.v2.proto.LinkPreview(
private fun LinkPreview.toRemoteLinkPreview(backupMode: BackupMode): org.signal.archive.proto.LinkPreview {
return org.signal.archive.proto.LinkPreview(
url = url,
title = title.nullIfEmpty(),
image = (thumbnail.orNull() as? DatabaseAttachment)?.toRemoteMessageAttachment(backupMode = backupMode)?.pointer,
@@ -1685,17 +1685,18 @@ private fun ChatItem.validateChatItem(exportState: ExportState, selfRecipientId:
return null
}
if (this.updateMessage != null && this.updateMessage.isOnlyForIndividualChats() && exportState.threadIdToRecipientId[this.chatId] !in exportState.contactRecipientIds) {
val validatedUpdateMessage = this.updateMessage
if (validatedUpdateMessage != null && validatedUpdateMessage.isOnlyForIndividualChats() && exportState.threadIdToRecipientId[this.chatId] !in exportState.contactRecipientIds) {
Log.w(TAG, ExportSkips.individualChatUpdateInWrongTypeOfChat(this.dateSent))
return null
}
if (this.updateMessage != null && this.updateMessage.isOnlyForGroupChats() && exportState.threadIdToRecipientId[this.chatId] !in exportState.groupRecipientIds) {
if (validatedUpdateMessage != null && validatedUpdateMessage.isOnlyForGroupChats() && exportState.threadIdToRecipientId[this.chatId] !in exportState.groupRecipientIds) {
Log.w(TAG, ExportSkips.groupChatUpdateInWrongTypeOfChat(this.dateSent))
return null
}
if (this.updateMessage != null && this.updateMessage.canOnlyBeAuthoredBySelf() && this.authorId != selfRecipientId.toLong()) {
if (validatedUpdateMessage != null && validatedUpdateMessage.canOnlyBeAuthoredBySelf() && this.authorId != selfRecipientId.toLong()) {
Log.w(TAG, ExportSkips.individualChatUpdateNotAuthoredBySelf(this.dateSent))
return null
}
@@ -1723,7 +1724,9 @@ private fun ChatUpdateMessage.isOnlyForIndividualChats(): Boolean {
this.simpleUpdate?.type == SimpleChatUpdate.Type.END_SESSION ||
this.simpleUpdate?.type == SimpleChatUpdate.Type.CHAT_SESSION_REFRESH ||
this.simpleUpdate?.type == SimpleChatUpdate.Type.PAYMENT_ACTIVATION_REQUEST ||
this.simpleUpdate?.type == SimpleChatUpdate.Type.PAYMENTS_ACTIVATED
this.simpleUpdate?.type == SimpleChatUpdate.Type.PAYMENTS_ACTIVATED ||
this.sessionSwitchover != null ||
this.threadMerge != null
}
private fun ChatUpdateMessage.isOnlyForGroupChats(): Boolean {
@@ -1775,17 +1778,15 @@ private fun List<MessageAttachment>.withFixedVoiceNotes(textPresent: Boolean): L
}
private fun ChatItem.withDowngradeVoiceNotes(): ChatItem {
if (this.standardMessage == null) {
return this
}
val msg = this.standardMessage ?: return this
if (this.standardMessage.attachments.none { it.flag == MessageAttachment.Flag.VOICE_MESSAGE }) {
if (msg.attachments.none { it.flag == MessageAttachment.Flag.VOICE_MESSAGE }) {
return this
}
return this.copy(
standardMessage = this.standardMessage.copy(
attachments = this.standardMessage.attachments.map {
standardMessage = msg.copy(
attachments = msg.attachments.map {
if (it.flag == MessageAttachment.Flag.VOICE_MESSAGE) {
it.copy(flag = MessageAttachment.Flag.NONE)
} else {

View File

@@ -7,6 +7,8 @@ package org.thoughtcrime.securesms.backup.v2.exporters
import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.signal.archive.proto.Contact
import org.signal.archive.proto.Self
import org.signal.core.models.ServiceId
import org.signal.core.util.Base64
import org.signal.core.util.logging.Log
@@ -18,8 +20,6 @@ import org.signal.core.util.requireLong
import org.signal.core.util.requireString
import org.signal.core.util.toByteArray
import org.thoughtcrime.securesms.backup.v2.ArchiveRecipient
import org.thoughtcrime.securesms.backup.v2.proto.Contact
import org.thoughtcrime.securesms.backup.v2.proto.Self
import org.thoughtcrime.securesms.backup.v2.util.clampToValidBackupRange
import org.thoughtcrime.securesms.backup.v2.util.isValidUsername
import org.thoughtcrime.securesms.backup.v2.util.toRemote

View File

@@ -7,6 +7,8 @@ package org.thoughtcrime.securesms.backup.v2.exporters
import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.signal.archive.proto.DistributionList
import org.signal.archive.proto.DistributionListItem
import org.signal.core.util.logging.Log
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireLong
@@ -17,8 +19,6 @@ import org.thoughtcrime.securesms.backup.v2.ArchiveRecipient
import org.thoughtcrime.securesms.backup.v2.ExportOddities
import org.thoughtcrime.securesms.backup.v2.ExportState
import org.thoughtcrime.securesms.backup.v2.database.getMembersForBackup
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList
import org.thoughtcrime.securesms.backup.v2.proto.DistributionListItem
import org.thoughtcrime.securesms.backup.v2.util.clampToValidBackupRange
import org.thoughtcrime.securesms.database.DistributionListTables
import org.thoughtcrime.securesms.database.model.DistributionListId

View File

@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.backup.v2.exporters
import android.database.Cursor
import okio.ByteString.Companion.toByteString
import org.signal.archive.proto.Group
import org.signal.core.models.ServiceId
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
@@ -24,7 +25,6 @@ import org.signal.storageservice.storage.protos.groups.local.DecryptedRequesting
import org.signal.storageservice.storage.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.backup.v2.ArchiveGroup
import org.thoughtcrime.securesms.backup.v2.ArchiveRecipient
import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.backup.v2.util.toRemote
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.GroupTable
@@ -50,7 +50,7 @@ class GroupArchiveExporter(private val selfAci: ServiceId.ACI, private val curso
val extras = RecipientTableCursorUtil.getExtras(cursor)
val showAsStoryState: GroupTable.ShowAsStoryState = GroupTable.ShowAsStoryState.deserialize(cursor.requireInt(GroupTable.SHOW_AS_STORY_STATE))
val isActive: Boolean = cursor.requireBoolean(GroupTable.ACTIVE)
val isMember: Boolean = cursor.requireBoolean(GroupTable.IS_MEMBER)
val decryptedGroup: DecryptedGroup = DecryptedGroup.ADAPTER.decode(cursor.requireBlob(GroupTable.V2_DECRYPTED_GROUP)!!)
return ArchiveRecipient(
@@ -61,7 +61,7 @@ class GroupArchiveExporter(private val selfAci: ServiceId.ACI, private val curso
blocked = cursor.requireBoolean(RecipientTable.BLOCKED),
hideStory = extras?.hideStory() ?: false,
storySendMode = showAsStoryState.toRemote(),
snapshot = decryptedGroup.toRemote(isActive, selfAci),
snapshot = decryptedGroup.toRemote(isMember, selfAci),
avatarColor = cursor.requireString(RecipientTable.AVATAR_COLOR)?.let { AvatarColor.deserialize(it) }?.toRemote()
)
)
@@ -80,9 +80,9 @@ private fun GroupTable.ShowAsStoryState.toRemote(): Group.StorySendMode {
}
}
private fun DecryptedGroup.toRemote(isActive: Boolean, selfAci: ServiceId.ACI): Group.GroupSnapshot? {
private fun DecryptedGroup.toRemote(isMember: Boolean, selfAci: ServiceId.ACI): Group.GroupSnapshot? {
val selfAciBytes = selfAci.toByteString()
val memberFilter = { m: DecryptedMember -> isActive || m.aciBytes != selfAciBytes }
val memberFilter = { m: DecryptedMember -> isMember || m.aciBytes != selfAciBytes }
return Group.GroupSnapshot(
title = Group.GroupAttributeBlob(title = this.title),
@@ -96,7 +96,8 @@ private fun DecryptedGroup.toRemote(isActive: Boolean, selfAci: ServiceId.ACI):
inviteLinkPassword = this.inviteLinkPassword,
description = this.description.takeUnless { it.isBlank() }?.let { Group.GroupAttributeBlob(descriptionText = it) },
announcements_only = this.isAnnouncementGroup == EnabledState.ENABLED,
members_banned = this.bannedMembers.map { it.toRemote() }
members_banned = this.bannedMembers.map { it.toRemote() },
terminated = this.terminated
)
}

View File

@@ -5,10 +5,10 @@
package org.thoughtcrime.securesms.backup.v2.importer
import org.signal.archive.proto.AdHocCall
import org.signal.core.util.insertInto
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.AdHocCall
import org.thoughtcrime.securesms.database.CallTable
import org.thoughtcrime.securesms.database.SignalDatabase

View File

@@ -5,12 +5,12 @@
package org.thoughtcrime.securesms.backup.v2.importer
import org.signal.archive.proto.CallLink
import org.signal.core.util.isEmpty
import org.signal.core.util.logging.Log
import org.signal.ringrtc.CallLinkRootKey
import org.signal.ringrtc.CallLinkState
import org.thoughtcrime.securesms.backup.v2.ArchiveCallLink
import org.thoughtcrime.securesms.backup.v2.proto.CallLink
import org.thoughtcrime.securesms.database.CallLinkTable
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.recipients.RecipientId

View File

@@ -6,13 +6,13 @@
package org.thoughtcrime.securesms.backup.v2.importer
import androidx.core.content.contentValuesOf
import org.signal.archive.proto.Chat
import org.signal.core.util.SqlUtil
import org.signal.core.util.insertInto
import org.signal.core.util.toInt
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.database.restoreWallpaperAttachment
import org.thoughtcrime.securesms.backup.v2.proto.Chat
import org.thoughtcrime.securesms.backup.v2.util.parseChatWallpaper
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment

View File

@@ -7,6 +7,23 @@ package org.thoughtcrime.securesms.backup.v2.importer
import android.content.ContentValues
import androidx.core.content.contentValuesOf
import org.signal.archive.proto.BodyRange
import org.signal.archive.proto.ChatItem
import org.signal.archive.proto.ChatUpdateMessage
import org.signal.archive.proto.ContactAttachment
import org.signal.archive.proto.DirectStoryReplyMessage
import org.signal.archive.proto.GroupCall
import org.signal.archive.proto.IndividualCall
import org.signal.archive.proto.LinkPreview
import org.signal.archive.proto.MessageAttachment
import org.signal.archive.proto.PaymentNotification
import org.signal.archive.proto.Quote
import org.signal.archive.proto.Reaction
import org.signal.archive.proto.SendStatus
import org.signal.archive.proto.SimpleChatUpdate
import org.signal.archive.proto.StandardMessage
import org.signal.archive.proto.Sticker
import org.signal.archive.proto.ViewOnceMessage
import org.signal.core.models.ServiceId
import org.signal.core.util.Base64
import org.signal.core.util.Hex
@@ -24,23 +41,6 @@ import org.thoughtcrime.securesms.attachments.PointerAttachment
import org.thoughtcrime.securesms.attachments.TombstoneAttachment
import org.thoughtcrime.securesms.backup.v2.ImportSkips
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.BodyRange
import org.thoughtcrime.securesms.backup.v2.proto.ChatItem
import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage
import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment
import org.thoughtcrime.securesms.backup.v2.proto.DirectStoryReplyMessage
import org.thoughtcrime.securesms.backup.v2.proto.GroupCall
import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall
import org.thoughtcrime.securesms.backup.v2.proto.LinkPreview
import org.thoughtcrime.securesms.backup.v2.proto.MessageAttachment
import org.thoughtcrime.securesms.backup.v2.proto.PaymentNotification
import org.thoughtcrime.securesms.backup.v2.proto.Quote
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.backup.v2.proto.Sticker
import org.thoughtcrime.securesms.backup.v2.proto.ViewOnceMessage
import org.thoughtcrime.securesms.backup.v2.util.toLocalAttachment
import org.thoughtcrime.securesms.contactshare.Contact
import org.thoughtcrime.securesms.database.AttachmentTable
@@ -89,7 +89,7 @@ import java.math.BigInteger
import java.sql.SQLException
import java.util.Optional
import java.util.UUID
import org.thoughtcrime.securesms.backup.v2.proto.GiftBadge as BackupGiftBadge
import org.signal.archive.proto.GiftBadge as BackupGiftBadge
/**
* An object that will ingest all of the [ChatItem]s you want to write, buffer them until hitting a specified batch size, and then batch insert them
@@ -194,7 +194,8 @@ class ChatItemArchiveImporter(
return
}
if (chatItem.adminDeletedMessage != null && importState.remoteToLocalRecipientId[chatItem.adminDeletedMessage.adminId] == null) {
val adminDeletedMessage = chatItem.adminDeletedMessage
if (adminDeletedMessage != null && importState.remoteToLocalRecipientId[adminDeletedMessage.adminId] == null) {
Log.w(TAG, ImportSkips.missingAdminDeleteRecipient(chatItem.dateSent, chatItem.chatId))
return
}
@@ -286,17 +287,22 @@ class ChatItemArchiveImporter(
val followUps: MutableList<(Long) -> Unit> = mutableListOf()
if (this.updateMessage != null) {
if (this.updateMessage.individualCall != null && this.updateMessage.individualCall.callId != null) {
val updateMessage = this.updateMessage
if (updateMessage != null) {
val individualCall = updateMessage.individualCall
val groupCall = updateMessage.groupCall
val pollTerminate = updateMessage.pollTerminate
val pinMessage = updateMessage.pinMessage
if (individualCall != null && individualCall.callId != null) {
followUps += { messageRowId ->
val values = contentValuesOf(
CallTable.CALL_ID to updateMessage.individualCall.callId,
CallTable.CALL_ID to individualCall.callId,
CallTable.MESSAGE_ID to messageRowId,
CallTable.PEER to chatRecipientId.serialize(),
CallTable.TYPE to CallTable.Type.serialize(if (updateMessage.individualCall.type == IndividualCall.Type.VIDEO_CALL) CallTable.Type.VIDEO_CALL else CallTable.Type.AUDIO_CALL),
CallTable.DIRECTION to CallTable.Direction.serialize(if (updateMessage.individualCall.direction == IndividualCall.Direction.OUTGOING) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING),
CallTable.TYPE to CallTable.Type.serialize(if (individualCall.type == IndividualCall.Type.VIDEO_CALL) CallTable.Type.VIDEO_CALL else CallTable.Type.AUDIO_CALL),
CallTable.DIRECTION to CallTable.Direction.serialize(if (individualCall.direction == IndividualCall.Direction.OUTGOING) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING),
CallTable.EVENT to CallTable.Event.serialize(
when (updateMessage.individualCall.state) {
when (individualCall.state) {
IndividualCall.State.MISSED -> CallTable.Event.MISSED
IndividualCall.State.MISSED_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE
IndividualCall.State.ACCEPTED -> CallTable.Event.ACCEPTED
@@ -304,24 +310,24 @@ class ChatItemArchiveImporter(
else -> CallTable.Event.MISSED
}
),
CallTable.TIMESTAMP to updateMessage.individualCall.startedCallTimestamp,
CallTable.READ to updateMessage.individualCall.read
CallTable.TIMESTAMP to individualCall.startedCallTimestamp,
CallTable.READ to individualCall.read
)
db.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
}
} else if (this.updateMessage.groupCall != null && this.updateMessage.groupCall.callId != null) {
} else if (groupCall != null && groupCall.callId != null) {
followUps += { messageRowId ->
val ringer: RecipientId? = this.updateMessage.groupCall.ringerRecipientId?.let { importState.remoteToLocalRecipientId[it] }
val ringer: RecipientId? = groupCall.ringerRecipientId?.let { importState.remoteToLocalRecipientId[it] }
val values = contentValuesOf(
CallTable.CALL_ID to updateMessage.groupCall.callId,
CallTable.CALL_ID to groupCall.callId,
CallTable.MESSAGE_ID to messageRowId,
CallTable.PEER to chatRecipientId.serialize(),
CallTable.RINGER to ringer?.serialize(),
CallTable.TYPE to CallTable.Type.serialize(CallTable.Type.GROUP_CALL),
CallTable.DIRECTION to CallTable.Direction.serialize(if (ringer == selfId) CallTable.Direction.OUTGOING else CallTable.Direction.INCOMING),
CallTable.EVENT to CallTable.Event.serialize(
when (updateMessage.groupCall.state) {
when (groupCall.state) {
GroupCall.State.ACCEPTED -> CallTable.Event.ACCEPTED
GroupCall.State.MISSED -> CallTable.Event.MISSED
GroupCall.State.MISSED_NOTIFICATION_PROFILE -> CallTable.Event.MISSED_NOTIFICATION_PROFILE
@@ -333,17 +339,17 @@ class ChatItemArchiveImporter(
else -> CallTable.Event.GENERIC_GROUP_CALL
}
),
CallTable.TIMESTAMP to updateMessage.groupCall.startedCallTimestamp,
CallTable.TIMESTAMP to groupCall.startedCallTimestamp,
CallTable.READ to CallTable.ReadState.serialize(CallTable.ReadState.READ)
)
db.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values)
}
} else if (this.updateMessage.pollTerminate != null) {
} else if (pollTerminate != null) {
followUps += { endPollMessageId ->
val pollMessageId = SignalDatabase.messages.getMessageFor(updateMessage.pollTerminate.targetSentTimestamp, fromRecipientId)?.id ?: -1
val pollMessageId = SignalDatabase.messages.getMessageFor(pollTerminate.targetSentTimestamp, fromRecipientId)?.id ?: -1
val pollId = SignalDatabase.polls.getPollId(pollMessageId)
val messageExtras = MessageExtras(pollTerminate = PollTerminate(question = updateMessage.pollTerminate.question, messageId = pollMessageId, targetTimestamp = updateMessage.pollTerminate.targetSentTimestamp))
val messageExtras = MessageExtras(pollTerminate = PollTerminate(question = pollTerminate.question, messageId = pollMessageId, targetTimestamp = pollTerminate.targetSentTimestamp))
db.update(MessageTable.TABLE_NAME)
.values(MessageTable.MESSAGE_EXTRAS to messageExtras.encode())
.where("${MessageTable.ID} = ?", endPollMessageId)
@@ -353,16 +359,16 @@ class ChatItemArchiveImporter(
SignalDatabase.polls.endPoll(pollId = pollId, endingMessageId = endPollMessageId)
}
}
} else if (this.updateMessage.pinMessage != null) {
} else if (pinMessage != null) {
followUps += { pinUpdateMessageId ->
val targetAuthorId = importState.remoteToLocalRecipientId[updateMessage.pinMessage.authorId]
val targetAuthorId = importState.remoteToLocalRecipientId[pinMessage.authorId]
if (targetAuthorId != null) {
val pinnedMessageId = SignalDatabase.messages.getMessageFor(updateMessage.pinMessage.targetSentTimestamp, targetAuthorId)?.id ?: -1
val pinnedMessageId = SignalDatabase.messages.getMessageFor(pinMessage.targetSentTimestamp, targetAuthorId)?.id ?: -1
val messageExtras = MessageExtras(
pinnedMessage = PinnedMessage(
pinnedMessageId = pinnedMessageId,
targetAuthorAci = recipients.getRecord(targetAuthorId).aci!!.toByteString(),
targetTimestamp = updateMessage.pinMessage.targetSentTimestamp
targetTimestamp = pinMessage.targetSentTimestamp
)
)
@@ -397,8 +403,9 @@ class ChatItemArchiveImporter(
}
}
if (this.contactMessage != null) {
val contact = this.contactMessage.contact?.let { backupContact ->
val contactMessage = this.contactMessage
if (contactMessage != null) {
val contact = contactMessage.contact?.let { backupContact ->
Contact(
backupContact.name.toLocal(),
backupContact.organization,
@@ -453,8 +460,9 @@ class ChatItemArchiveImporter(
}
}
if (this.directStoryReplyMessage != null) {
val (trimmedBodyText, longTextAttachment) = this.directStoryReplyMessage.parseBodyText(importState)
val directStoryReplyMessage = this.directStoryReplyMessage
if (directStoryReplyMessage != null) {
val (trimmedBodyText, longTextAttachment) = directStoryReplyMessage.parseBodyText(importState)
if (trimmedBodyText != null) {
contentValues.put(MessageTable.BODY, trimmedBodyText)
}
@@ -469,25 +477,26 @@ class ChatItemArchiveImporter(
}
}
if (this.standardMessage != null) {
val mentions = this.standardMessage.text?.bodyRanges.filterToLocalMentions()
val standardMessage = this.standardMessage
if (standardMessage != null) {
val mentions = standardMessage.text?.bodyRanges.filterToLocalMentions()
if (mentions.isNotEmpty()) {
followUps += { messageId ->
SignalDatabase.mentions.insert(threadId, messageId, mentions)
}
}
val linkPreviews = this.standardMessage.linkPreview.map { it.toLocalLinkPreview() }
val linkPreviews = standardMessage.linkPreview.map { it.toLocalLinkPreview() }
val linkPreviewAttachments: List<Attachment> = linkPreviews.mapNotNull { it.thumbnail.orNull() }
val attachments: List<Attachment> = this.standardMessage.attachments.mapNotNull { attachment ->
val attachments: List<Attachment> = standardMessage.attachments.mapNotNull { attachment ->
attachment.toLocalAttachment()
}
val (trimmedBodyText, longTextAttachment) = this.standardMessage.parseBodyText(importState)
val (trimmedBodyText, longTextAttachment) = standardMessage.parseBodyText(importState)
if (trimmedBodyText != null) {
contentValues.put(MessageTable.BODY, trimmedBodyText)
}
val quoteAttachments: List<Attachment> = this.standardMessage.quote?.toLocalAttachments() ?: emptyList()
val quoteAttachments: List<Attachment> = standardMessage.quote?.toLocalAttachments() ?: emptyList()
val hasAttachments = attachments.isNotEmpty() || linkPreviewAttachments.isNotEmpty() || quoteAttachments.isNotEmpty() || longTextAttachment != null
@@ -515,8 +524,9 @@ class ChatItemArchiveImporter(
}
}
if (this.stickerMessage != null) {
val sticker = this.stickerMessage.sticker
val stickerMessage = this.stickerMessage
if (stickerMessage != null) {
val sticker = stickerMessage.sticker
val attachment = sticker.toLocalAttachment()
if (attachment != null) {
followUps += { messageRowId ->
@@ -525,8 +535,9 @@ class ChatItemArchiveImporter(
}
}
if (this.viewOnceMessage != null) {
val attachment = this.viewOnceMessage.attachment?.toLocalAttachment()
val viewOnceMessage = this.viewOnceMessage
if (viewOnceMessage != null) {
val attachment = viewOnceMessage.attachment?.toLocalAttachment()
if (attachment != null) {
followUps += { messageRowId ->
SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(attachment), emptyList())
@@ -534,7 +545,8 @@ class ChatItemArchiveImporter(
}
}
if (this.poll != null) {
val poll = this.poll
if (poll != null) {
contentValues.put(MessageTable.BODY, poll.question)
contentValues.put(MessageTable.VOTES_LAST_SEEN, System.currentTimeMillis())
@@ -582,15 +594,14 @@ class ChatItemArchiveImporter(
* If the attachment is non-null, then you should store it along with the message, as it contains the long text.
*/
private fun StandardMessage.parseBodyText(importState: ImportState): Pair<String?, Attachment?> {
if (this.longText != null) {
return null to this.longText.toLocalAttachment(contentType = "text/x-signal-plain")
val longText = this.longText
if (longText != null) {
return null to longText.toLocalAttachment(contentType = "text/x-signal-plain")
}
if (this.text?.body == null) {
return null to null
}
val body = this.text?.body ?: return null to null
val splitResult = MessageUtil.getSplitMessage(AppDependencies.application, this.text.body)
val splitResult = MessageUtil.getSplitMessage(AppDependencies.application, body)
if (splitResult.textSlide.isPresent) {
return splitResult.body to splitResult.textSlide.get().asAttachment()
}
@@ -606,15 +617,15 @@ class ChatItemArchiveImporter(
* If the attachment is non-null, then you should store it along with the message, as it contains the long text.
*/
private fun DirectStoryReplyMessage.parseBodyText(importState: ImportState): Pair<String?, Attachment?> {
if (this.textReply?.longText != null) {
return null to this.textReply.longText.toLocalAttachment(contentType = "text/x-signal-plain")
val textReply = this.textReply
val longText = textReply?.longText
if (longText != null) {
return null to longText.toLocalAttachment(contentType = "text/x-signal-plain")
}
if (this.textReply?.text == null) {
return null to null
}
val body = textReply?.text?.body ?: return null to null
val splitResult = MessageUtil.getSplitMessage(AppDependencies.application, this.textReply.text.body)
val splitResult = MessageUtil.getSplitMessage(AppDependencies.application, body)
if (splitResult.textSlide.isPresent) {
return splitResult.body to splitResult.textSlide.get().asAttachment()
}
@@ -625,16 +636,20 @@ class ChatItemArchiveImporter(
private fun ChatItem.toMessageContentValues(fromRecipientId: RecipientId, chatRecipientId: RecipientId, threadId: Long): ContentValues {
val contentValues = ContentValues()
val toRecipientId = if (this.outgoing != null) chatRecipientId else selfId
val outgoing = this.outgoing
val incoming = this.incoming
val directionless = this.directionless
val toRecipientId = if (outgoing != null) chatRecipientId else selfId
contentValues.put(MessageTable.TYPE, this.getMessageType())
contentValues.put(MessageTable.DATE_SENT, this.dateSent)
contentValues.put(MessageTable.DATE_SERVER, this.incoming?.dateServerSent ?: -1)
contentValues.put(MessageTable.DATE_SERVER, incoming?.dateServerSent ?: -1)
contentValues.put(MessageTable.FROM_RECIPIENT_ID, fromRecipientId.serialize())
contentValues.put(MessageTable.TO_RECIPIENT_ID, toRecipientId.serialize())
contentValues.put(MessageTable.THREAD_ID, threadId)
contentValues.put(MessageTable.DATE_RECEIVED, this.incoming?.dateReceived ?: this.outgoing?.dateReceived?.takeUnless { it == 0L } ?: this.dateSent)
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, this.outgoing?.sendStatus?.maxOfOrNull { it.timestamp } ?: 0)
contentValues.put(MessageTable.DATE_RECEIVED, incoming?.dateReceived ?: outgoing?.dateReceived?.takeUnless { it == 0L } ?: this.dateSent)
contentValues.put(MessageTable.RECEIPT_TIMESTAMP, outgoing?.sendStatus?.maxOfOrNull { it.timestamp } ?: 0)
contentValues.putNull(MessageTable.LATEST_REVISION_ID)
contentValues.putNull(MessageTable.ORIGINAL_MESSAGE_ID)
contentValues.put(MessageTable.REVISION_NUMBER, 0)
@@ -642,29 +657,29 @@ class ChatItemArchiveImporter(
contentValues.put(MessageTable.EXPIRE_STARTED, this.expireStartDate ?: 0)
when {
this.outgoing != null -> {
val viewed = this.outgoing.sendStatus.any { it.viewed != null }
val hasReadReceipt = viewed || this.outgoing.sendStatus.any { it.read != null }
val hasDeliveryReceipt = viewed || hasReadReceipt || this.outgoing.sendStatus.any { it.delivered != null }
outgoing != null -> {
val viewed = outgoing.sendStatus.any { it.viewed != null }
val hasReadReceipt = viewed || outgoing.sendStatus.any { it.read != null }
val hasDeliveryReceipt = viewed || hasReadReceipt || outgoing.sendStatus.any { it.delivered != null }
contentValues.put(MessageTable.VIEWED_COLUMN, viewed.toInt())
contentValues.put(MessageTable.HAS_READ_RECEIPT, hasReadReceipt.toInt())
contentValues.put(MessageTable.HAS_DELIVERY_RECEIPT, hasDeliveryReceipt.toInt())
contentValues.put(MessageTable.UNIDENTIFIED, this.outgoing.sendStatus.count { it.sealedSender })
contentValues.put(MessageTable.UNIDENTIFIED, outgoing.sendStatus.count { it.sealedSender })
contentValues.put(MessageTable.READ, 1)
contentValues.addNetworkFailures(this, importState)
contentValues.addIdentityKeyMismatches(this, importState)
}
this.incoming != null -> {
incoming != null -> {
contentValues.put(MessageTable.VIEWED_COLUMN, 0)
contentValues.put(MessageTable.HAS_READ_RECEIPT, 0)
contentValues.put(MessageTable.HAS_DELIVERY_RECEIPT, 0)
contentValues.put(MessageTable.UNIDENTIFIED, this.incoming.sealedSender.toInt())
contentValues.put(MessageTable.READ, this.incoming.read.toInt())
contentValues.put(MessageTable.UNIDENTIFIED, incoming.sealedSender.toInt())
contentValues.put(MessageTable.READ, incoming.read.toInt())
contentValues.put(MessageTable.NOTIFIED, 1)
}
this.directionless != null -> {
directionless != null -> {
contentValues.put(MessageTable.VIEWED_COLUMN, 0)
contentValues.put(MessageTable.HAS_READ_RECEIPT, 0)
contentValues.put(MessageTable.HAS_DELIVERY_RECEIPT, 0)
@@ -680,21 +695,30 @@ class ChatItemArchiveImporter(
contentValues.put(MessageTable.VIEW_ONCE, 0)
contentValues.put(MessageTable.PARENT_STORY_ID, 0)
if (this.pinDetails != null) {
val pinnedUntil = if (this.pinDetails.pinNeverExpires == true) MessageTable.PIN_FOREVER else this.pinDetails.pinExpiresAtTimestamp
val pinDetails = this.pinDetails
if (pinDetails != null) {
val pinnedUntil = if (pinDetails.pinNeverExpires == true) MessageTable.PIN_FOREVER else pinDetails.pinExpiresAtTimestamp
contentValues.put(MessageTable.PINNED_UNTIL, pinnedUntil ?: 0)
contentValues.put(MessageTable.PINNED_AT, this.pinDetails.pinnedAtTimestamp)
contentValues.put(MessageTable.PINNED_AT, pinDetails.pinnedAtTimestamp)
}
val itemStandardMessage = this.standardMessage
val itemRemoteDeletedMessage = this.remoteDeletedMessage
val itemUpdateMessage = this.updateMessage
val itemPaymentNotification = this.paymentNotification
val itemGiftBadge = this.giftBadge
val itemViewOnceMessage = this.viewOnceMessage
val itemDirectStoryReplyMessage = this.directStoryReplyMessage
val itemAdminDeletedMessage = this.adminDeletedMessage
when {
this.standardMessage != null -> contentValues.addStandardMessage(this.standardMessage)
this.remoteDeletedMessage != null -> contentValues.put(MessageTable.DELETED_BY, fromRecipientId.toLong())
this.updateMessage != null -> contentValues.addUpdateMessage(this.updateMessage, fromRecipientId, toRecipientId)
this.paymentNotification != null -> contentValues.addPaymentNotification(this, chatRecipientId)
this.giftBadge != null -> contentValues.addGiftBadge(this.giftBadge)
this.viewOnceMessage != null -> contentValues.addViewOnce(this.viewOnceMessage)
this.directStoryReplyMessage != null -> contentValues.addDirectStoryReply(this.directStoryReplyMessage, toRecipientId)
this.adminDeletedMessage != null -> contentValues.put(MessageTable.DELETED_BY, importState.remoteToLocalRecipientId[this.adminDeletedMessage.adminId]!!.toLong())
itemStandardMessage != null -> contentValues.addStandardMessage(itemStandardMessage)
itemRemoteDeletedMessage != null -> contentValues.put(MessageTable.DELETED_BY, fromRecipientId.toLong())
itemUpdateMessage != null -> contentValues.addUpdateMessage(itemUpdateMessage, fromRecipientId, toRecipientId)
itemPaymentNotification != null -> contentValues.addPaymentNotification(this, chatRecipientId)
itemGiftBadge != null -> contentValues.addGiftBadge(itemGiftBadge)
itemViewOnceMessage != null -> contentValues.addViewOnce(itemViewOnceMessage)
itemDirectStoryReplyMessage != null -> contentValues.addDirectStoryReply(itemDirectStoryReplyMessage, toRecipientId)
itemAdminDeletedMessage != null -> contentValues.put(MessageTable.DELETED_BY, importState.remoteToLocalRecipientId[itemAdminDeletedMessage.adminId]!!.toLong())
}
return contentValues
@@ -733,15 +757,13 @@ class ChatItemArchiveImporter(
}
private fun ChatItem.toReactionContentValues(messageId: Long): List<ContentValues> {
val reactions: List<Reaction> = when {
this.standardMessage != null -> this.standardMessage.reactions
this.contactMessage != null -> this.contactMessage.reactions
this.stickerMessage != null -> this.stickerMessage.reactions
this.viewOnceMessage != null -> this.viewOnceMessage.reactions
this.directStoryReplyMessage != null -> this.directStoryReplyMessage.reactions
this.poll != null -> this.poll.reactions
else -> emptyList()
}
val reactions: List<Reaction> = this.standardMessage?.reactions
?: this.contactMessage?.reactions
?: this.stickerMessage?.reactions
?: this.viewOnceMessage?.reactions
?: this.directStoryReplyMessage?.reactions
?: this.poll?.reactions
?: emptyList()
return reactions
.mapNotNull {
@@ -763,16 +785,14 @@ class ChatItemArchiveImporter(
}
private fun ChatItem.toGroupReceiptContentValues(messageId: Long, chatBackupRecipientId: Long): List<ContentValues> {
if (this.outgoing == null) {
return emptyList()
}
val outgoing = this.outgoing ?: return emptyList()
// TODO [backup] This seems like an indirect/bad way to detect if this is a 1:1 or group convo
if (this.outgoing.sendStatus.size == 1 && this.outgoing.sendStatus[0].recipientId == chatBackupRecipientId) {
if (outgoing.sendStatus.size == 1 && outgoing.sendStatus[0].recipientId == chatBackupRecipientId) {
return emptyList()
}
return this.outgoing.sendStatus.mapNotNull { sendStatus ->
return outgoing.sendStatus.mapNotNull { sendStatus ->
val recipientId = importState.remoteToLocalRecipientId[sendStatus.recipientId]
if (recipientId != null) {
@@ -791,16 +811,17 @@ class ChatItemArchiveImporter(
}
private fun ChatItem.getMessageType(): Long {
var type: Long = if (this.outgoing != null) {
if (this.outgoing.sendStatus.any { it.pending != null }) {
val outgoing = this.outgoing
var type: Long = if (outgoing != null) {
if (outgoing.sendStatus.any { it.pending != null }) {
MessageTypes.BASE_SENDING_TYPE
} else if (this.outgoing.sendStatus.any { it.failed?.reason == SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH }) {
} else if (outgoing.sendStatus.any { it.failed?.reason == SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH }) {
MessageTypes.BASE_SENT_FAILED_TYPE
} else if (this.outgoing.sendStatus.any { it.failed?.reason == SendStatus.Failed.FailureReason.UNKNOWN }) {
} else if (outgoing.sendStatus.any { it.failed?.reason == SendStatus.Failed.FailureReason.UNKNOWN }) {
MessageTypes.BASE_SENT_FAILED_TYPE
} else if (this.outgoing.sendStatus.any { it.failed?.reason == SendStatus.Failed.FailureReason.NETWORK }) {
} else if (outgoing.sendStatus.any { it.failed?.reason == SendStatus.Failed.FailureReason.NETWORK }) {
MessageTypes.BASE_SENT_FAILED_TYPE
} else if (this.outgoing.sendStatus.all { it.skipped != null }) {
} else if (outgoing.sendStatus.all { it.skipped != null }) {
MessageTypes.BASE_SENDING_SKIPPED_TYPE
} else {
MessageTypes.BASE_SENT_TYPE
@@ -825,25 +846,38 @@ class ChatItemArchiveImporter(
}
private fun ContentValues.addStandardMessage(standardMessage: StandardMessage) {
if (standardMessage.text != null) {
this.put(MessageTable.BODY, standardMessage.text.body)
val text = standardMessage.text
if (text != null) {
this.put(MessageTable.BODY, text.body)
if (standardMessage.text.bodyRanges.isNotEmpty()) {
this.put(MessageTable.MESSAGE_RANGES, standardMessage.text.bodyRanges.toLocalBodyRanges()?.encode())
if (text.bodyRanges.isNotEmpty()) {
this.put(MessageTable.MESSAGE_RANGES, text.bodyRanges.toLocalBodyRanges()?.encode())
}
}
if (standardMessage.quote != null) {
this.addQuote(standardMessage.quote)
val quote = standardMessage.quote
if (quote != null) {
this.addQuote(quote)
}
}
private fun ContentValues.addUpdateMessage(updateMessage: ChatUpdateMessage, fromRecipientId: RecipientId, toRecipientId: RecipientId) {
var typeFlags: Long = 0
val simpleUpdate = updateMessage.simpleUpdate
val expirationTimerChange = updateMessage.expirationTimerChange
val profileChange = updateMessage.profileChange
val learnedProfileChange = updateMessage.learnedProfileChange
val pollTerminate = updateMessage.pollTerminate
val pinMessage = updateMessage.pinMessage
val sessionSwitchover = updateMessage.sessionSwitchover
val threadMerge = updateMessage.threadMerge
val individualCall = updateMessage.individualCall
val groupCall = updateMessage.groupCall
val groupChange = updateMessage.groupChange
when {
updateMessage.simpleUpdate != null -> {
simpleUpdate != null -> {
val typeWithoutBase = (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
typeFlags = when (updateMessage.simpleUpdate.type) {
typeFlags = when (simpleUpdate.type) {
SimpleChatUpdate.Type.UNKNOWN -> typeWithoutBase
SimpleChatUpdate.Type.JOINED_SIGNAL -> MessageTypes.JOINED_TYPE or typeWithoutBase
SimpleChatUpdate.Type.IDENTITY_UPDATE -> MessageTypes.KEY_EXCHANGE_IDENTITY_UPDATE_BIT or typeWithoutBase
@@ -864,59 +898,59 @@ class ChatItemArchiveImporter(
}
// Identity verification changes have to/from swapped
if (updateMessage.simpleUpdate.type == SimpleChatUpdate.Type.IDENTITY_VERIFIED || updateMessage.simpleUpdate.type == SimpleChatUpdate.Type.IDENTITY_DEFAULT) {
if (simpleUpdate.type == SimpleChatUpdate.Type.IDENTITY_VERIFIED || simpleUpdate.type == SimpleChatUpdate.Type.IDENTITY_DEFAULT) {
put(MessageTable.FROM_RECIPIENT_ID, toRecipientId.serialize())
put(MessageTable.TO_RECIPIENT_ID, fromRecipientId.serialize())
}
}
updateMessage.expirationTimerChange != null -> {
expirationTimerChange != null -> {
typeFlags = getAsLong(MessageTable.TYPE) or MessageTypes.EXPIRATION_TIMER_UPDATE_BIT
put(MessageTable.EXPIRES_IN, updateMessage.expirationTimerChange.expiresInMs)
put(MessageTable.EXPIRES_IN, expirationTimerChange.expiresInMs)
}
updateMessage.profileChange != null -> {
profileChange != null -> {
typeFlags = MessageTypes.PROFILE_CHANGE_TYPE
val profileChangeDetails = ProfileChangeDetails(profileNameChange = ProfileChangeDetails.StringChange(previous = updateMessage.profileChange.previousName, newValue = updateMessage.profileChange.newName))
val profileChangeDetails = ProfileChangeDetails(profileNameChange = ProfileChangeDetails.StringChange(previous = profileChange.previousName, newValue = profileChange.newName))
val messageExtras = MessageExtras(profileChangeDetails = profileChangeDetails).encode()
put(MessageTable.MESSAGE_EXTRAS, messageExtras)
}
updateMessage.learnedProfileChange != null -> {
learnedProfileChange != null -> {
typeFlags = MessageTypes.PROFILE_CHANGE_TYPE
val profileChangeDetails = ProfileChangeDetails(learnedProfileName = ProfileChangeDetails.LearnedProfileName(e164 = updateMessage.learnedProfileChange.e164?.toString(), username = updateMessage.learnedProfileChange.username))
val profileChangeDetails = ProfileChangeDetails(learnedProfileName = ProfileChangeDetails.LearnedProfileName(e164 = learnedProfileChange.e164?.toString(), username = learnedProfileChange.username))
val messageExtras = MessageExtras(profileChangeDetails = profileChangeDetails).encode()
put(MessageTable.MESSAGE_EXTRAS, messageExtras)
}
updateMessage.pollTerminate != null -> {
pollTerminate != null -> {
typeFlags = MessageTypes.SPECIAL_TYPE_POLL_TERMINATE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
}
updateMessage.pinMessage != null -> {
pinMessage != null -> {
typeFlags = MessageTypes.SPECIAL_TYPE_PINNED_MESSAGE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
}
updateMessage.sessionSwitchover != null -> {
sessionSwitchover != null -> {
typeFlags = MessageTypes.SESSION_SWITCHOVER_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
val sessionSwitchoverDetails = SessionSwitchoverEvent(e164 = updateMessage.sessionSwitchover.e164.toString()).encode()
val sessionSwitchoverDetails = SessionSwitchoverEvent(e164 = sessionSwitchover.e164.toString()).encode()
put(MessageTable.BODY, Base64.encodeWithPadding(sessionSwitchoverDetails))
}
updateMessage.threadMerge != null -> {
threadMerge != null -> {
typeFlags = MessageTypes.THREAD_MERGE_TYPE or (getAsLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK.inv())
val threadMergeDetails = ThreadMergeEvent(previousE164 = updateMessage.threadMerge.previousE164.toString()).encode()
val threadMergeDetails = ThreadMergeEvent(previousE164 = threadMerge.previousE164.toString()).encode()
put(MessageTable.BODY, Base64.encodeWithPadding(threadMergeDetails))
}
updateMessage.individualCall != null -> {
if (updateMessage.individualCall.state == IndividualCall.State.MISSED || updateMessage.individualCall.state == IndividualCall.State.MISSED_NOTIFICATION_PROFILE) {
typeFlags = if (updateMessage.individualCall.type == IndividualCall.Type.AUDIO_CALL) {
individualCall != null -> {
if (individualCall.state == IndividualCall.State.MISSED || individualCall.state == IndividualCall.State.MISSED_NOTIFICATION_PROFILE) {
typeFlags = if (individualCall.type == IndividualCall.Type.AUDIO_CALL) {
MessageTypes.MISSED_AUDIO_CALL_TYPE
} else {
MessageTypes.MISSED_VIDEO_CALL_TYPE
}
} else {
typeFlags = if (updateMessage.individualCall.direction == IndividualCall.Direction.OUTGOING) {
if (updateMessage.individualCall.type == IndividualCall.Type.AUDIO_CALL) {
typeFlags = if (individualCall.direction == IndividualCall.Direction.OUTGOING) {
if (individualCall.type == IndividualCall.Type.AUDIO_CALL) {
MessageTypes.OUTGOING_AUDIO_CALL_TYPE
} else {
MessageTypes.OUTGOING_VIDEO_CALL_TYPE
}
} else {
if (updateMessage.individualCall.type == IndividualCall.Type.AUDIO_CALL) {
if (individualCall.type == IndividualCall.Type.AUDIO_CALL) {
MessageTypes.INCOMING_AUDIO_CALL_TYPE
} else {
MessageTypes.INCOMING_VIDEO_CALL_TYPE
@@ -925,28 +959,24 @@ class ChatItemArchiveImporter(
}
this.put(MessageTable.READ, 1)
}
updateMessage.groupCall != null -> {
val startedCallRecipientId = if (updateMessage.groupCall.startedCallRecipientId != null) {
importState.remoteToLocalRecipientId[updateMessage.groupCall.startedCallRecipientId]
} else {
null
}
groupCall != null -> {
val startedCallRecipientId = groupCall.startedCallRecipientId?.let { importState.remoteToLocalRecipientId[it] }
val startedCall = if (startedCallRecipientId != null) {
recipients.getRecord(startedCallRecipientId).aci
} else {
null
}
this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(updateMessage.groupCall, startedCall))
this.put(MessageTable.READ, updateMessage.groupCall.read.toInt())
this.put(MessageTable.BODY, GroupCallUpdateDetailsUtil.createBodyFromBackup(groupCall, startedCall))
this.put(MessageTable.READ, groupCall.read.toInt())
typeFlags = MessageTypes.GROUP_CALL_TYPE
}
updateMessage.groupChange != null -> {
groupChange != null -> {
put(MessageTable.BODY, "")
put(
MessageTable.MESSAGE_EXTRAS,
MessageExtras(
gv2UpdateDescription =
GV2UpdateDescription(groupChangeUpdate = updateMessage.groupChange)
GV2UpdateDescription(groupChangeUpdate = groupChange)
).encode()
)
typeFlags = getAsLong(MessageTable.TYPE) or MessageTypes.GROUP_V2_BIT or MessageTypes.GROUP_UPDATE_BIT
@@ -962,18 +992,18 @@ class ChatItemArchiveImporter(
*/
private fun ContentValues.addPaymentNotification(chatItem: ChatItem, chatRecipientId: RecipientId) {
val paymentNotification = chatItem.paymentNotification!!
if (chatItem.paymentNotification.amountMob.isNullOrEmpty()) {
if (paymentNotification.amountMob.isNullOrEmpty()) {
this.addPaymentTombstoneNoAmount()
return
}
val amount = paymentNotification.amountMob?.tryParseMoney() ?: return this.addPaymentTombstoneNoAmount()
val fee = paymentNotification.feeMob?.tryParseMoney() ?: return this.addPaymentTombstoneNoAmount()
if (chatItem.paymentNotification.transactionDetails?.failedTransaction != null) {
if (paymentNotification.transactionDetails?.failedTransaction != null) {
this.addFailedPaymentNotification(chatItem, amount, fee, chatRecipientId)
return
}
this.addPaymentTombstoneNoMetadata(chatItem.paymentNotification)
this.addPaymentTombstoneNoMetadata(paymentNotification)
}
private fun PaymentNotification.TransactionDetails.MobileCoinTxoIdentification.toLocal(): PaymentMetaData {
@@ -1062,13 +1092,15 @@ class ChatItemArchiveImporter(
put(MessageTable.QUOTE_ID, MessageTable.QUOTE_TARGET_MISSING_ID)
put(MessageTable.QUOTE_AUTHOR, toRecipientId.serialize())
if (directStoryReply.emoji != null) {
put(MessageTable.BODY, directStoryReply.emoji)
val emoji = directStoryReply.emoji
if (emoji != null) {
put(MessageTable.BODY, emoji)
}
if (directStoryReply.textReply != null) {
put(MessageTable.BODY, directStoryReply.textReply.text?.body)
put(MessageTable.MESSAGE_RANGES, directStoryReply.textReply.text?.bodyRanges?.toLocalBodyRanges()?.encode())
val textReply = directStoryReply.textReply
if (textReply != null) {
put(MessageTable.BODY, textReply.text?.body)
put(MessageTable.MESSAGE_RANGES, textReply.text?.bodyRanges?.toLocalBodyRanges()?.encode())
}
}
@@ -1126,11 +1158,9 @@ class ChatItemArchiveImporter(
}
private fun ContentValues.addNetworkFailures(chatItem: ChatItem, importState: ImportState) {
if (chatItem.outgoing == null) {
return
}
val outgoing = chatItem.outgoing ?: return
val networkFailures = chatItem.outgoing.sendStatus
val networkFailures = outgoing.sendStatus
.filter { status -> status.failed?.reason == SendStatus.Failed.FailureReason.NETWORK }
.mapNotNull { status -> importState.remoteToLocalRecipientId[status.recipientId] }
.map { recipientId -> NetworkFailure(recipientId) }
@@ -1142,11 +1172,9 @@ class ChatItemArchiveImporter(
}
private fun ContentValues.addIdentityKeyMismatches(chatItem: ChatItem, importState: ImportState) {
if (chatItem.outgoing == null) {
return
}
val outgoing = chatItem.outgoing ?: return
val mismatches = chatItem.outgoing.sendStatus
val mismatches = outgoing.sendStatus
.filter { status -> status.failed?.reason == SendStatus.Failed.FailureReason.IDENTITY_KEY_MISMATCH }
.mapNotNull { status -> importState.remoteToLocalRecipientId[status.recipientId] }
.map { recipientId -> IdentityKeyMismatch(recipientId, null) } // TODO We probably want the actual identity key in this status situation?
@@ -1166,8 +1194,8 @@ class ChatItemArchiveImporter(
ranges = this.filter { includeMentions || it.mentionAci == null }.map { bodyRange ->
BodyRangeList.BodyRange(
mentionUuid = bodyRange.mentionAci?.let { UuidUtil.fromByteString(it) }?.toString(),
style = bodyRange.style?.let {
when (bodyRange.style) {
style = bodyRange.style?.let { style ->
when (style) {
BodyRange.Style.BOLD -> BodyRangeList.BodyRange.Style.BOLD
BodyRange.Style.ITALIC -> BodyRangeList.BodyRange.Style.ITALIC
BodyRange.Style.MONOSPACE -> BodyRangeList.BodyRange.Style.MONOSPACE
@@ -1217,13 +1245,11 @@ class ChatItemArchiveImporter(
return@mapNotNull thumbnail
}
if (attachment.contentType == null) {
return@mapNotNull null
}
val contentType = attachment.contentType ?: return@mapNotNull null
return@mapNotNull PointerAttachment.forPointer(
quotedAttachment = DataMessage.Quote.QuotedAttachment(
contentType = attachment.contentType,
contentType = contentType,
fileName = attachment.fileName,
thumbnail = null
)
@@ -1259,7 +1285,8 @@ class ChatItemArchiveImporter(
}
private fun MessageAttachment.toLocalAttachment(quote: Boolean = false, quoteTargetContentType: String? = null, contentType: String? = pointer?.contentType): Attachment? {
return pointer?.toLocalAttachment(
val pointer = this.pointer ?: return null
return pointer.toLocalAttachment(
voiceNote = flag == MessageAttachment.Flag.VOICE_MESSAGE,
borderless = flag == MessageAttachment.Flag.BORDERLESS,
gif = flag == MessageAttachment.Flag.GIF,

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.backup.v2.importer
import androidx.core.content.contentValuesOf
import org.signal.archive.proto.Contact
import org.signal.core.models.ServiceId.ACI
import org.signal.core.models.ServiceId.PNI
import org.signal.core.util.Base64
@@ -14,7 +15,6 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.toInt
import org.signal.core.util.update
import org.thoughtcrime.securesms.backup.v2.ImportSkips
import org.thoughtcrime.securesms.backup.v2.proto.Contact
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.database.IdentityTable
import org.thoughtcrime.securesms.database.RecipientTable
@@ -70,11 +70,12 @@ object ContactArchiveImporter {
RecipientTable.KEY_TRANSPARENCY_DATA to contact.keyTransparencyData?.toByteArray()
)
val notRegistered = contact.notRegistered
if (contact.registered != null) {
values.put(RecipientTable.UNREGISTERED_TIMESTAMP, 0L)
values.put(RecipientTable.REGISTERED, RecipientTable.RegisteredState.REGISTERED.id)
} else if (contact.notRegistered != null) {
values.put(RecipientTable.UNREGISTERED_TIMESTAMP, contact.notRegistered.unregisteredTimestamp)
} else if (notRegistered != null) {
values.put(RecipientTable.UNREGISTERED_TIMESTAMP, notRegistered.unregisteredTimestamp)
values.put(RecipientTable.REGISTERED, RecipientTable.RegisteredState.NOT_REGISTERED.id)
}
@@ -84,12 +85,13 @@ object ContactArchiveImporter {
.where("${RecipientTable.ID} = ?", id)
.run()
if (contact.identityKey != null && (aci != null || pni != null)) {
val identityKey = contact.identityKey
if (identityKey != null && (aci != null || pni != null)) {
SignalDatabase.writableDatabase
.insertInto(IdentityTable.TABLE_NAME)
.values(
IdentityTable.ADDRESS to (aci ?: pni).toString(),
IdentityTable.IDENTITY_KEY to Base64.encodeWithPadding(contact.identityKey.toByteArray()),
IdentityTable.IDENTITY_KEY to Base64.encodeWithPadding(identityKey.toByteArray()),
IdentityTable.VERIFIED to contact.identityState.toLocal().toInt()
)
.run(SQLiteDatabase.CONFLICT_REPLACE)

View File

@@ -5,11 +5,11 @@
package org.thoughtcrime.securesms.backup.v2.importer
import org.signal.archive.proto.DistributionList
import org.signal.archive.proto.DistributionListItem
import org.signal.core.util.UuidUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.backup.v2.ImportState
import org.thoughtcrime.securesms.backup.v2.proto.DistributionList
import org.thoughtcrime.securesms.backup.v2.proto.DistributionListItem
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListPrivacyMode
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -23,13 +23,14 @@ object DistributionListArchiveImporter {
private val TAG = Log.tag(DistributionListArchiveImporter.javaClass)
fun import(dlistItem: DistributionListItem, importState: ImportState): RecipientId? {
if (dlistItem.deletionTimestamp != null && dlistItem.deletionTimestamp > 0) {
val deletionTimestamp = dlistItem.deletionTimestamp
if (deletionTimestamp != null && deletionTimestamp > 0) {
val dlistId = SignalDatabase.distributionLists.createList(
name = "",
members = emptyList(),
distributionId = DistributionId.from(UuidUtil.fromByteString(dlistItem.distributionId)),
allowsReplies = false,
deletionTimestamp = dlistItem.deletionTimestamp,
deletionTimestamp = deletionTimestamp,
storageId = null,
privacyMode = DistributionListPrivacyMode.ONLY_WITH
)!!

View File

@@ -6,6 +6,7 @@
package org.thoughtcrime.securesms.backup.v2.importer
import android.content.ContentValues
import org.signal.archive.proto.Group
import org.signal.core.models.ServiceId
import org.signal.core.util.Base64
import org.signal.core.util.toInt
@@ -21,7 +22,6 @@ import org.signal.storageservice.storage.protos.groups.local.DecryptedRequesting
import org.signal.storageservice.storage.protos.groups.local.DecryptedTimer
import org.signal.storageservice.storage.protos.groups.local.EnabledState
import org.thoughtcrime.securesms.backup.v2.ArchiveGroup
import org.thoughtcrime.securesms.backup.v2.proto.Group
import org.thoughtcrime.securesms.backup.v2.util.toLocal
import org.thoughtcrime.securesms.conversation.colors.AvatarColorHash
import org.thoughtcrime.securesms.database.GroupTable
@@ -45,10 +45,11 @@ object GroupArchiveImporter {
val groupId = GroupId.v2(masterKey)
val operations = AppDependencies.groupsV2Operations.forGroup(GroupSecretParams.deriveFromMasterKey(masterKey))
val decryptedState = if (group.snapshot == null) {
val snapshot = group.snapshot
val decryptedState = if (snapshot == null) {
DecryptedGroup(revision = GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
} else {
group.snapshot.toLocal(operations)
snapshot.toLocal(operations)
}
val values = ContentValues().apply {
@@ -84,12 +85,13 @@ private fun Group.StorySendMode.toLocal(): GroupTable.ShowAsStoryState {
}
private fun Group.MemberPendingProfileKey.toLocal(operations: GroupsV2Operations.GroupOperations): DecryptedPendingMember {
val m = member!!
return DecryptedPendingMember(
serviceIdBytes = member!!.userId,
role = member.role.toLocal(),
serviceIdBytes = m.userId,
role = m.role.toLocal(),
addedByAci = addedByUserId,
timestamp = timestamp,
serviceIdCipherText = operations.encryptServiceId(ServiceId.Companion.parseOrNull(member.userId))
serviceIdCipherText = operations.encryptServiceId(ServiceId.Companion.parseOrNull(m.userId))
)
}
@@ -162,6 +164,7 @@ private fun Group.GroupSnapshot.toLocal(operations: GroupsV2Operations.GroupOper
description = this.description?.descriptionText ?: "",
isAnnouncementGroup = if (this.announcements_only) EnabledState.ENABLED else EnabledState.DISABLED,
bannedMembers = this.members_banned.map { it.toLocal() },
terminated = this.terminated,
isPlaceholderGroup = isPlaceholder
)
}

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