Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068eaff801 | ||
|
|
e0bb3a48c2 | ||
|
|
f2e4881026 | ||
|
|
b605148ac4 | ||
|
|
2b9126d74b | ||
|
|
206f6d84e7 | ||
|
|
01836b3a7c | ||
|
|
e68691c966 | ||
|
|
957f473e77 | ||
|
|
8a023100ea | ||
|
|
5bfdca509c | ||
|
|
9a837254ec | ||
|
|
3f27769d20 | ||
|
|
4f260c2063 | ||
|
|
75df16e842 | ||
|
|
fce6651e26 | ||
|
|
b06783bc90 | ||
|
|
72a1a9b0ff | ||
|
|
5568a14490 | ||
|
|
378ebb00c4 | ||
|
|
c81f40eb74 | ||
|
|
d97bde3959 | ||
|
|
4d301a4f66 | ||
|
|
9941b2d123 | ||
|
|
089d8a50b2 | ||
|
|
eb8ad5218d | ||
|
|
21b1401fc4 | ||
|
|
58ea9a1f48 | ||
|
|
2bb9578ef9 | ||
|
|
c3b8768570 | ||
|
|
94e3dabc20 | ||
|
|
542a820e22 | ||
|
|
8f7cc52255 | ||
|
|
63888f1c99 | ||
|
|
a588522c9b | ||
|
|
7a2eca3bd5 | ||
|
|
a8ba0dccca | ||
|
|
782c83cc4e | ||
|
|
46e6ae915c | ||
|
|
8a887b65a1 | ||
|
|
08491579dd | ||
|
|
25b01a30be | ||
|
|
48374e6950 | ||
|
|
6496f236ea | ||
|
|
e767434c2b | ||
|
|
bb6507a456 | ||
|
|
c3f9e5d972 | ||
|
|
34d87cf6e1 | ||
|
|
e657a4adf3 | ||
|
|
9594599d60 | ||
|
|
a0c0acb8fc | ||
|
|
0896718e5c | ||
|
|
be4bf27ede | ||
|
|
7253aaaa14 | ||
|
|
72cbe61f6c | ||
|
|
78d3db319c | ||
|
|
c7a6c7ad9e | ||
|
|
8bc183b994 | ||
|
|
ef6e5abc17 | ||
|
|
e96e6e8d18 | ||
|
|
cee33a23ac | ||
|
|
c5de7581ee | ||
|
|
5dc626078f | ||
|
|
9de75b3e1f | ||
|
|
f09bf5b14c |
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -313,7 +313,7 @@ class SmsDatabaseTest_collapseJoinRequestEventsIfPossible {
|
||||
timestamp = wallClock,
|
||||
groupId = groupId,
|
||||
update = updateDescription,
|
||||
isGroupAdd = false,
|
||||
isNotifiable = false,
|
||||
serverGuid = null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 209 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 97 KiB |
BIN
app/src/main/assets/emoji/People_10.webp
Normal file
|
After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 213 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 210 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 100 KiB After Width: | Height: | Size: 94 KiB |
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)!!
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||