Add support for rendering session switchover events.

main
Greyson Parrelli 2023-02-07 11:26:57 -05:00
rodzic 03c68375db
commit 9e056e5dd0
11 zmienionych plików z 229 dodań i 878 usunięć

Wyświetl plik

@ -43,10 +43,7 @@
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
<value />
</option>
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />

Wyświetl plik

@ -346,6 +346,32 @@ class RecipientTableTest_getAndPossiblyMerge {
expectThreadMergeEvent(E164_A)
}
test("merge, e164 & pni & aci, all provided, no threads") {
given(E164_A, null, null, createThread = false)
given(null, PNI_A, null, createThread = false)
given(null, null, ACI_A, createThread = false)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
test("merge, e164 & pni & aci, all provided, pni session no threads") {
given(E164_A, null, null, createThread = false)
given(null, PNI_A, null, createThread = true, pniSession = true)
given(null, null, ACI_A, createThread = false)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectSessionSwitchoverEvent(E164_A)
}
test("merge, e164 & pni, no aci provided") {
given(E164_A, null, null)
given(null, PNI_A, null)
@ -382,7 +408,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expectThreadMergeEvent("")
}
test("merge, e164 & pni, aci provided, existing pni session") {
test("merge, e164 & pni, aci provided, existing pni session, thread merge shadows") {
given(E164_A, null, null)
given(null, PNI_A, null, pniSession = true)
@ -392,6 +418,17 @@ class RecipientTableTest_getAndPossiblyMerge {
expectDeleted()
expectThreadMergeEvent("")
}
test("merge, e164 & pni, aci provided, existing pni session, no thread merge") {
given(E164_A, null, null, createThread = true)
given(null, PNI_A, null, createThread = false, pniSession = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectDeleted()
expectSessionSwitchoverEvent(E164_A)
}
@ -407,7 +444,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expectThreadMergeEvent("")
}
test("merge, e164+pni & aci") {
test("merge, e164+pni & aci, no pni session") {
given(E164_A, PNI_A, null)
given(null, null, ACI_A)
@ -419,6 +456,52 @@ class RecipientTableTest_getAndPossiblyMerge {
expectThreadMergeEvent(E164_A)
}
test("merge, e164+pni & aci, pni session, thread merge shadows") {
given(E164_A, PNI_A, null, pniSession = true)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
}
test("merge, e164+pni & aci, pni session, no thread merge") {
given(E164_A, PNI_A, null, createThread = true, pniSession = true)
given(null, null, ACI_A, createThread = false)
process(E164_A, PNI_A, ACI_A)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectSessionSwitchoverEvent(E164_A)
}
test("merge, e164+pni & aci, pni session, no thread merge, pni verified") {
given(E164_A, PNI_A, null, createThread = true, pniSession = true)
given(null, null, ACI_A, createThread = false)
process(E164_A, PNI_A, ACI_A, pniVerified = true)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
}
test("merge, e164+pni & aci, pni session, pni verified") {
given(E164_A, PNI_A, null, pniSession = true)
given(null, null, ACI_A)
process(E164_A, PNI_A, ACI_A, pniVerified = true)
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
}
test("merge, e164+pni & e164+pni+aci, change number") {
given(E164_A, PNI_A, null)
given(E164_B, PNI_B, ACI_A)
@ -758,8 +841,8 @@ class RecipientTableTest_getAndPossiblyMerge {
return id
}
fun process(e164: String?, pni: PNI?, aci: ACI?, changeSelf: Boolean = false) {
outputRecipientId = SignalDatabase.recipients.getAndPossiblyMerge(serviceId = aci ?: pni, pni = pni, e164 = e164, pniVerified = false, changeSelf = changeSelf)
fun process(e164: String?, pni: PNI?, aci: ACI?, changeSelf: Boolean = false, pniVerified: Boolean = false) {
outputRecipientId = SignalDatabase.recipients.getAndPossiblyMerge(serviceId = aci ?: pni, pni = pni, e164 = e164, pniVerified = pniVerified, changeSelf = changeSelf)
generatedIds += outputRecipientId
}

Wyświetl plik

@ -1,843 +0,0 @@
package org.thoughtcrime.securesms.database
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.testing.SignalDatabaseRule
import org.thoughtcrime.securesms.util.Util
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import java.lang.AssertionError
import java.lang.IllegalStateException
import java.util.UUID
@RunWith(AndroidJUnit4::class)
class RecipientTableTest_processPnpTupleToChangeSet {
@Rule
@JvmField
val databaseRule = SignalDatabaseRule(deleteAllThreadsOnEachRun = false)
private lateinit var db: RecipientTable
@Before
fun setup() {
db = SignalDatabase.recipients
}
@Test
fun noMatch_e164Only() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, null, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, null, null)
),
changeSet
)
}
@Test
fun noMatch_e164AndPni() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, null, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, null)
),
changeSet
)
}
@Test
fun noMatch_aciOnly() {
val changeSet = db.processPnpTupleToChangeSet(null, null, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(null, null, ACI_A)
),
changeSet
)
}
@Test(expected = IllegalStateException::class)
fun noMatch_noData() {
db.processPnpTupleToChangeSet(null, null, null, pniVerified = false)
}
@Test
fun noMatch_allFields() {
val changeSet = db.processPnpTupleToChangeSet(E164_A, PNI_A, ACI_A, pniVerified = false)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpInsert(E164_A, PNI_A, ACI_A)
),
changeSet
)
}
@Test
fun fullMatch() {
val result = applyAndAssert(
Input(E164_A, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id)
),
result.changeSet
)
}
@Test
fun onlyE164Matches() {
val result = applyAndAssert(
Input(E164_A, null, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null, pniSession = true),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.id, E164_A)
)
),
result.changeSet
)
}
@Test
fun onlyE164Matches_pniChanges_noAciProvided_noPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_B, null),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun e164AndPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(E164_A, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id, E164_A)
)
),
result.changeSet
)
}
@Test
fun e164AndAciMatches() {
val result = applyAndAssert(
Input(E164_A, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_noExistingSession() {
val result = applyAndAssert(
Input(null, PNI_A, null),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession() {
val result = applyAndAssert(
Input(null, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id, E164_A)
)
),
result.changeSet
)
}
@Test
fun onlyPniMatches_existingPniSession_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, null, pniSession = true),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
),
PnpOperation.SessionSwitchoverInsert(result.id, E164_A)
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches() {
val result = applyAndAssert(
Input(null, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A)
)
),
result.changeSet
)
}
@Test
fun pniAndAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, PNI_A, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches() {
val result = applyAndAssert(
Input(null, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A)
)
),
result.changeSet
)
}
@Test
fun onlyAciMatches_changeNumber() {
val result = applyAndAssert(
Input(E164_B, null, ACI_A),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null),
Input(null, null, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.thirdId),
operations = linkedSetOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.Merge(
primaryId = result.thirdId,
secondaryId = result.firstId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null)
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = linkedSetOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniOnly_aciProvidedButNoAciRecord() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(null, PNI_A, null)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = linkedSetOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
),
PnpOperation.SetAci(
recipientId = result.firstId,
aci = ACI_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164Only_pniAndE164_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, null, null),
Input(E164_B, PNI_A, null)
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = linkedSetOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(
recipientId = result.firstId,
pni = PNI_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_pniOnly_noAciProvided() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(null, PNI_A, null)
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = linkedSetOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_noSessions() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null),
Input(E164_B, PNI_A, null)
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = linkedSetOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPni_noAciProvided_sessionsExist() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_B, null, pniSession = true),
Input(E164_B, PNI_A, null, pniSession = true)
),
Update(E164_A, PNI_A, null),
Output(E164_A, PNI_A, null)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = linkedSetOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.secondId, E164_A),
PnpOperation.SessionSwitchoverInsert(result.firstId, E164_A)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(null, null, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = linkedSetOf(
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164() {
val result = applyAndAssert(
listOf(
Input(E164_B, PNI_A, null),
Input(null, null, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = linkedSetOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.SetPni(
recipientId = result.secondId,
pni = PNI_A
),
PnpOperation.SetE164(
recipientId = result.secondId,
e164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_aciOnly_e164RecordHasSeparateE164_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_B, PNI_A, null),
Input(E164_C, null, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = linkedSetOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.SetPni(
recipientId = result.secondId,
pni = PNI_A
),
PnpOperation.SetE164(
recipientId = result.secondId,
e164 = E164_A
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_C,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164AndPniAndAci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, PNI_B, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = linkedSetOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
@Test
fun merge_e164AndPni_e164Aci_changeNumber() {
val result = applyAndAssert(
listOf(
Input(E164_A, PNI_A, null),
Input(E164_B, null, ACI_A)
),
Update(E164_A, PNI_A, ACI_A),
Output(E164_A, PNI_A, ACI_A)
)
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = linkedSetOf(
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
),
PnpOperation.ChangeNumberInsert(
recipientId = result.secondId,
oldE164 = E164_B,
newE164 = E164_A
)
)
),
result.changeSet
)
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val id: Long = SignalDatabase.rawDatabase.insert(
RecipientTable.TABLE_NAME,
null,
contentValuesOf(
RecipientTable.PHONE to e164,
RecipientTable.SERVICE_ID to (aci ?: pni)?.toString(),
RecipientTable.PNI_COLUMN to pni?.toString(),
RecipientTable.REGISTERED to RecipientTable.RegisteredState.REGISTERED.id
)
)
return RecipientId.from(id)
}
private fun insertMockSessionFor(account: ServiceId, address: ServiceId) {
SignalDatabase.rawDatabase.insert(
SessionTable.TABLE_NAME,
null,
contentValuesOf(
SessionTable.ACCOUNT_ID to account.toString(),
SessionTable.ADDRESS to address.toString(),
SessionTable.DEVICE to 1,
SessionTable.RECORD to Util.getSecretBytes(32)
)
)
}
data class Input(val e164: String?, val pni: PNI?, val aci: ACI?, val pniSession: Boolean = false, val aciSession: Boolean = false)
data class Update(val e164: String?, val pni: PNI?, val aci: ACI?, val pniVerified: Boolean = false)
data class Output(val e164: String?, val pni: PNI?, val aci: ACI?)
data class PnpMatchResult(val ids: List<RecipientId>, val changeSet: PnpChangeSet) {
val id
get() = if (ids.size == 1) {
ids[0]
} else {
throw IllegalStateException("There are multiple IDs, but you assumed 1!")
}
val firstId
get() = ids[0]
val secondId
get() = ids[1]
val thirdId
get() = ids[2]
}
private fun applyAndAssert(input: Input, update: Update, output: Output): PnpMatchResult {
return applyAndAssert(listOf(input), update, output)
}
/**
* Helper method that will call insert your recipients, call [RecipientTable.processPnpTupleToChangeSet] with your params,
* and then verify your output matches what you expect.
*
* It results the inserted ID's and changeset for additional verification.
*
* But basically this is here to make the tests more readable. It gives you a clear list of:
* - input
* - update
* - output
*
* that you can spot check easily.
*
* Important: The output will only include records that contain fields from the input. That means
* for:
*
* Input: E164_B, PNI_A, null
* Update: E164_A, PNI_A, null
*
* You will get:
* Output: E164_A, PNI_A, null
*
* Even though there was an update that will also result in the row (E164_B, null, null)
*/
private fun applyAndAssert(input: List<Input>, update: Update, output: Output): PnpMatchResult {
val ids = input.map { insert(it.e164, it.pni, it.aci) }
input
.filter { it.pniSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.pni!!) }
input
.filter { it.aciSession }
.forEach { insertMockSessionFor(databaseRule.localAci, it.aci!!) }
val byE164 = update.e164?.let { db.getByE164(it).orElse(null) }
val byPniSid = update.pni?.let { db.getByServiceId(it).orElse(null) }
val byAciSid = update.aci?.let { db.getByServiceId(it).orElse(null) }
val data = PnpDataSet(
e164 = update.e164,
pni = update.pni,
aci = update.aci,
byE164 = byE164,
byPniSid = byPniSid,
byPniOnly = update.pni?.let { db.getByPni(it).orElse(null) },
byAciSid = byAciSid,
e164Record = byE164?.let { db.getRecord(it) },
pniSidRecord = byPniSid?.let { db.getRecord(it) },
aciSidRecord = byAciSid?.let { db.getRecord(it) }
)
val changeSet = db.processPnpTupleToChangeSet(update.e164, update.pni, update.aci, pniVerified = update.pniVerified)
val finalData = data.perform(changeSet.operations)
val finalRecords = setOfNotNull(finalData.e164Record, finalData.pniSidRecord, finalData.aciSidRecord)
assertEquals("There's still multiple records in the resulting record set! $finalRecords", 1, finalRecords.size)
finalRecords.firstOrNull { record -> record.e164 == output.e164 && record.pni == output.pni && record.serviceId == (output.aci ?: output.pni) }
?: throw AssertionError("Expected output was not found in the result set! Expected: $output")
return PnpMatchResult(
ids = ids,
changeSet = changeSet
)
}
companion object {
val ACI_A = ACI.from(UUID.fromString("3436efbe-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("8de7f691-0b60-4a68-9cd9-ed2f8453f9ed"))
val PNI_A = PNI.from(UUID.fromString("154b8d92-c960-4f6c-8385-671ad2ffb999"))
val PNI_B = PNI.from(UUID.fromString("ba92b1fb-cd55-40bf-adda-c35a85375533"))
const val E164_A = "+12221234567"
const val E164_B = "+13331234567"
const val E164_C = "+14441234567"
}
}

Wyświetl plik

@ -22,7 +22,6 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager
import org.whispersystems.signalservice.api.push.TrustStore
import org.whispersystems.signalservice.internal.configuration.SignalCdnUrl
import org.whispersystems.signalservice.internal.configuration.SignalCdsiUrl
import org.whispersystems.signalservice.internal.configuration.SignalContactDiscoveryUrl
import org.whispersystems.signalservice.internal.configuration.SignalKeyBackupServiceUrl
import org.whispersystems.signalservice.internal.configuration.SignalServiceConfiguration
import org.whispersystems.signalservice.internal.configuration.SignalServiceUrl
@ -66,15 +65,13 @@ class InstrumentationApplicationDependencyProvider(application: Application, def
0 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
2 to arrayOf(SignalCdnUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT))
),
arrayOf(SignalContactDiscoveryUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
arrayOf(SignalKeyBackupServiceUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
arrayOf(SignalStorageUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
arrayOf(SignalCdsiUrl(baseUrl, "localhost", serviceTrustStore, ConnectionSpec.CLEARTEXT)),
emptyList(),
Optional.of(SignalServiceNetworkAccess.DNS),
Optional.empty(),
Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS),
true
Base64.decode(BuildConfig.ZKGROUP_SERVER_PUBLIC_PARAMS)
)
serviceNetworkAccessMock = mock {

Wyświetl plik

@ -9,6 +9,8 @@ import androidx.lifecycle.ViewModelProvider
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.Hex
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.isAbsent
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.thoughtcrime.securesms.MainActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
@ -220,7 +222,7 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
sectionHeaderPref(DSLSettingsText.from("PNP"))
clickPref(
title = DSLSettingsText.from("Split contact"),
title = DSLSettingsText.from("Split and create threads"),
summary = DSLSettingsText.from("Splits this contact into two recipients and two threads so that you can test merging them together. This will remain the 'primary' recipient."),
onClick = {
MaterialAlertDialogBuilder(requireContext())
@ -257,6 +259,41 @@ class InternalConversationSettingsFragment : DSLSettingsFragment(
.show()
}
)
clickPref(
title = DSLSettingsText.from("Split without creating threads"),
summary = DSLSettingsText.from("Splits this contact into two recipients so you can test merging them together. This will become the PNI-based recipient. Another recipient will be made with this ACI and profile key. Doing a CDS refresh should allow you to see a Session Switchover Event, as long as you had a session with this PNI."),
isEnabled = FeatureFlags.phoneNumberPrivacy(),
onClick = {
MaterialAlertDialogBuilder(requireContext())
.setTitle("Are you sure?")
.setNegativeButton(android.R.string.cancel) { d, _ -> d.dismiss() }
.setPositiveButton(android.R.string.ok) { _, _ ->
if (recipient.pni.isAbsent()) {
Toast.makeText(context, "Recipient doesn't have a PNI! Can't split.", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
if (recipient.serviceId.isAbsent()) {
Toast.makeText(context, "Recipient doesn't have a serviceId! Can't split.", Toast.LENGTH_SHORT).show()
return@setPositiveButton
}
SignalDatabase.recipients.debugRemoveAci(recipient.id)
val aciRecipientId: RecipientId = SignalDatabase.recipients.getAndPossiblyMergePnpVerified(recipient.requireServiceId(), null, null)
recipient.profileKey?.let { profileKey ->
SignalDatabase.recipients.setProfileKey(aciRecipientId, ProfileKey(profileKey))
}
SignalDatabase.recipients.debugClearProfileData(recipient.id)
Toast.makeText(context, "Done! Split the ACI and profile key off into $aciRecipientId", Toast.LENGTH_SHORT).show()
}
.show()
}
)
}
}

Wyświetl plik

@ -22,7 +22,6 @@ import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
@ -73,6 +72,7 @@ import org.thoughtcrime.securesms.database.model.UpdateDescription;
import org.thoughtcrime.securesms.glide.GlideLiveDataTarget;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@ -85,7 +85,6 @@ import org.thoughtcrime.securesms.util.SpanUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@ -637,6 +636,14 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
return emphasisAdded(context, "", defaultTint);
} else if (MessageTypes.isBadDecryptType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_delivery_issue), defaultTint);
} else if (MessageTypes.isThreadMergeType(thread.getType())) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_history_has_been_merged), defaultTint);
} else if (MessageTypes.isSessionSwitchoverType(thread.getType())) {
if (thread.getRecipient().getE164().isPresent()) {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_s_belongs_to_s, PhoneNumberFormatter.prettyPrint(thread.getRecipient().requireE164()), thread.getRecipient().getDisplayName(context)), defaultTint);
} else {
return emphasisAdded(context, context.getString(R.string.ThreadRecord_safety_number_changed), defaultTint);
}
} else {
ThreadTable.Extra extra = thread.getExtra();
if (extra != null && extra.isViewOnce()) {

Wyświetl plik

@ -2406,6 +2406,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
@VisibleForTesting
fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, inputPni: PNI?): RecipientId {
var hadThreadMerge = false
for (operation in changeSet.operations) {
@Exhaustive
when (operation) {
@ -2465,16 +2466,21 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
.run()
}
is PnpOperation.Merge -> {
merge(operation.primaryId, operation.secondaryId, inputPni)
val mergeResult: MergeResult = merge(operation.primaryId, operation.secondaryId, inputPni)
hadThreadMerge = hadThreadMerge || mergeResult.neededThreadMerge
}
is PnpOperation.SessionSwitchoverInsert -> {
val threadId: Long? = threads.getThreadIdFor(operation.recipientId)
if (threadId != null) {
val event = SessionSwitchoverEvent
.newBuilder()
.setE164(operation.e164 ?: "")
.build()
SignalDatabase.messages.insertSessionSwitchoverEvent(operation.recipientId, threadId, event)
if (hadThreadMerge) {
Log.d(TAG, "Skipping SSE insert because we already had a thread merge event.")
} else {
val threadId: Long? = threads.getThreadIdFor(operation.recipientId)
if (threadId != null) {
val event = SessionSwitchoverEvent
.newBuilder()
.setE164(operation.e164 ?: "")
.build()
SignalDatabase.messages.insertSessionSwitchoverEvent(operation.recipientId, threadId, event)
}
}
}
is PnpOperation.ChangeNumberInsert -> {
@ -2622,6 +2628,14 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
)
}
if (!pniVerified && fullData.pniSidRecord != null && finalData.aciSidRecord != null && sessions.hasAnySessionFor(fullData.pniSidRecord.serviceId.toString())) {
breadCrumbs += "FinalUpdateSSE"
operations += PnpOperation.SessionSwitchoverInsert(
recipientId = primaryId,
e164 = finalData.e164
)
}
return PnpChangeSet(
id = PnpIdResolver.PnpNoopId(primaryId),
operations = operations,
@ -3644,7 +3658,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
* Merges one ACI recipient with an E164 recipient. It is assumed that the E164 recipient does
* *not* have an ACI.
*/
private fun merge(primaryId: RecipientId, secondaryId: RecipientId, newPni: PNI? = null): RecipientId {
private fun merge(primaryId: RecipientId, secondaryId: RecipientId, newPni: PNI? = null): MergeResult {
ensureInTransaction()
val db = writableDatabase
val primaryRecord = getRecord(primaryId)
@ -3656,7 +3670,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
// Threads
val threadMerge = threads.merge(primaryId, secondaryId)
val threadMerge: ThreadTable.MergeResult = threads.merge(primaryId, secondaryId)
threads.setLastScrolled(threadMerge.threadId, 0)
threads.update(threadMerge.threadId, false, false)
@ -3721,7 +3735,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(primaryId))
return primaryId
return MergeResult(
finalId = primaryId,
neededThreadMerge = threadMerge.neededMerge
)
}
private fun ensureInTransaction() {
@ -3845,6 +3863,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
* get them back through CDS).
*/
fun debugClearServiceIds(recipientId: RecipientId? = null) {
check(FeatureFlags.internalUser())
writableDatabase
.update(TABLE_NAME)
.values(
@ -3868,6 +3888,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
* Should only be used for debugging! A very destructive action that clears all known profile keys and credentials.
*/
fun debugClearProfileData(recipientId: RecipientId? = null) {
check(FeatureFlags.internalUser())
writableDatabase
.update(TABLE_NAME)
.values(
@ -3896,6 +3918,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
* Should only be used for debugging! Clears the E164 and PNI from a recipient.
*/
fun debugClearE164AndPni(recipientId: RecipientId) {
check(FeatureFlags.internalUser())
writableDatabase
.update(TABLE_NAME)
.values(
@ -3905,7 +3929,28 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
.where(ID_WHERE, recipientId)
.run()
Recipient.live(recipientId).refresh()
ApplicationDependencies.getRecipientCache().clear()
RecipientId.clearCache()
}
/**
* Should only be used for debugging! Clears the ACI from a contact.
* Only works if the recipient has a PNI.
*/
fun debugRemoveAci(recipientId: RecipientId) {
check(FeatureFlags.internalUser())
writableDatabase.execSQL(
"""
UPDATE $TABLE_NAME
SET $SERVICE_ID = $PNI_COLUMN
WHERE $ID = ? AND $PNI_COLUMN NOT NULL
""".toSingleLine(),
SqlUtil.buildArgs(recipientId)
)
ApplicationDependencies.getRecipientCache().clear()
RecipientId.clearCache()
}
fun getRecord(context: Context, cursor: Cursor): RecipientRecord {
@ -4131,6 +4176,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
)
}
private data class MergeResult(
val finalId: RecipientId,
val neededThreadMerge: Boolean
)
inner class BulkOperationsHandle internal constructor(private val database: SQLiteDatabase) {
private val pendingRecipients: MutableSet<RecipientId> = mutableSetOf()

Wyświetl plik

@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent;
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.emoji.JumboEmoji;
@ -235,6 +236,18 @@ public abstract class MessageRecord extends DisplayRecord {
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
} else if (isSessionSwitchoverEventType()) {
try {
SessionSwitchoverEvent event = SessionSwitchoverEvent.parseFrom(Base64.decodeOrThrow(getBody()));
if (event.getE164().isEmpty()) {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_your_safety_number_with_s_has_changed, r.getDisplayName(context)), R.drawable.ic_update_safety_number_16);
} else {
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_belongs_to_s, PhoneNumberFormatter.prettyPrint(r.requireE164()), r.getDisplayName(context)), R.drawable.ic_update_info_16);
}
} catch (InvalidProtocolBufferException e) {
throw new AssertionError(e);
}
} else if (isSmsExportType()) {
int messageResource = SignalStore.misc().getSmsExportPhase().isSmsSupported() ? R.string.MessageRecord__you_will_no_longer_be_able_to_send_sms_messages_from_signal_soon
: R.string.MessageRecord__you_can_no_longer_send_sms_messages_in_signal;
@ -551,6 +564,10 @@ public abstract class MessageRecord extends DisplayRecord {
return MessageTypes.isThreadMergeType(type);
}
public boolean isSessionSwitchoverEventType() {
return MessageTypes.isSessionSwitchoverType(type);
}
public boolean isSmsExportType() {
return MessageTypes.isSmsExport(type);
}
@ -575,7 +592,7 @@ public abstract class MessageRecord extends DisplayRecord {
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() ||
isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType() ||
isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType() || isSessionSwitchoverEventType() ||
isPaymentsRequestToActivate() || isPaymentsActivated();
}

Wyświetl plik

@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.messages.MessageDecryptionUtil;
import org.thoughtcrime.securesms.messages.MessageDecryptionUtil.DecryptionResult;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.notifications.NotificationIds;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.transport.RetryLaterException;
import org.thoughtcrime.securesms.util.FeatureFlags;
@ -33,7 +32,6 @@ import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
import org.whispersystems.signalservice.api.messages.SignalServicePniSignatureMessage;
import org.whispersystems.signalservice.api.push.PNI;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import java.util.LinkedList;
@ -114,10 +112,6 @@ public final class PushDecryptMessageJob extends BaseJob {
if (result.getContent().getSenderKeyDistributionMessage().isPresent()) {
handleSenderKeyDistributionMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getSenderKeyDistributionMessage().get());
}
result.getContent();
if (envelope.hasReportingToken()) {
SignalDatabase.recipients().setReportingToken(RecipientId.from(result.getContent().getSender()), envelope.getReportingToken());
}
if (FeatureFlags.phoneNumberPrivacy() && result.getContent().getPniSignatureMessage().isPresent()) {
handlePniSignatureMessage(result.getContent().getSender(), result.getContent().getSenderDevice(), result.getContent().getPniSignatureMessage().get());
@ -125,6 +119,10 @@ public final class PushDecryptMessageJob extends BaseJob {
Log.w(TAG, "Ignoring PNI signature because the feature flag is disabled!");
}
if (envelope.hasReportingToken()) {
SignalDatabase.recipients().setReportingToken(RecipientId.from(result.getContent().getSender()), envelope.getReportingToken());
}
jobs.add(new PushProcessMessageJob(result.getContent(), smsMessageId, envelope.getTimestamp()));
} else if (result.getException() != null && result.getState() != MessageState.NOOP) {
jobs.add(new PushProcessMessageJob(result.getState(), result.getException(), smsMessageId, envelope.getTimestamp()));

Wyświetl plik

@ -1042,7 +1042,11 @@ public class Recipient {
}
public @NonNull UnidentifiedAccessMode getUnidentifiedAccessMode() {
return unidentifiedAccessMode;
if (getPni().isPresent() && getPni().equals(getServiceId())) {
return UnidentifiedAccessMode.DISABLED;
} else {
return unidentifiedAccessMode;
}
}
public @Nullable ChatWallpaper getWallpaper() {

Wyświetl plik

@ -1962,6 +1962,10 @@
<string name="ThreadRecord_payment">Payment</string>
<!-- Displayed in the conversation list when your only message in a conversation is a scheduled send. -->
<string name="ThreadRecord_scheduled_message">Scheduled message</string>
<!-- Displayed in the conversation list when your message history has been merged -->
<string name="ThreadRecord_message_history_has_been_merged">Your message history has been merged</string>
<!-- Displayed in the conversation list when identities have been merged. The first placeholder is a phone number, and the second is a person's name -->
<string name="ThreadRecord_s_belongs_to_s">%1$s belongs to %2$s</string>
<!-- UpdateApkReadyListener -->
<string name="UpdateApkReadyListener_Signal_update">Signal update</string>