Insert session switchover events when appropriate.

main
Greyson Parrelli 2023-01-24 20:49:59 -05:00
rodzic 7745ae62ea
commit a7d9bd944b
10 zmienionych plików z 331 dodań i 76 usunięć

Wyświetl plik

@ -1,17 +1,21 @@
package org.thoughtcrime.securesms.database
import android.database.Cursor
import androidx.core.content.contentValuesOf
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.hamcrest.MatcherAssert
import org.hamcrest.Matchers
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.SignalProtocolAddress
@ -23,6 +27,8 @@ import org.thoughtcrime.securesms.database.model.Mention
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.ReactionRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.keyvalue.SignalStore
@ -32,6 +38,9 @@ import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage
import org.thoughtcrime.securesms.sms.IncomingTextMessage
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.FeatureFlagsAccessor
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
@ -46,6 +55,7 @@ class RecipientTableTest_getAndPossiblyMerge {
SignalStore.account().setE164(E164_SELF)
SignalStore.account().setAci(ACI_SELF)
SignalStore.account().setPni(PNI_SELF)
FeatureFlagsAccessor.forceValue(FeatureFlags.PHONE_NUMBER_PRIVACY, true)
}
@Test
@ -115,24 +125,40 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, null, ACI_B)
}
test("e164 and pni matches, all provided, new aci") {
test("e164 and pni matches, all provided, new aci, no pni session") {
given(E164_A, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("e164 and pni matches, all provided, new aci, existing pni session") {
given(E164_A, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectSessionSwitchoverEvent(E164_A)
}
test("e164 and aci matches, all provided, new pni") {
given(E164_A, null, ACI_A)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("pni matches, all provided, new e164 and aci") {
test("pni matches, all provided, new e164 and aci, no pni session") {
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("pni matches, all provided, new e164 and aci, existing pni session") {
given(null, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectSessionSwitchoverEvent(E164_A)
}
test("pni and aci matches, all provided, new e164") {
given(null, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
@ -172,20 +198,42 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, null)
}
test("e164 and pni matches, all provided, no existing session") {
test("e164 matches, e164 and pni provided, pni changes, existing pni session") {
given(E164_A, PNI_B, null, pniSession = true)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expectSessionSwitchoverEvent(E164_A)
}
test("e164 and pni matches, all provided, no pni session") {
given(E164_A, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
test("pni matches, all provided, no existing session") {
test("e164 and pni matches, all provided, existing pni session") {
given(E164_A, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectSessionSwitchoverEvent(E164_A)
}
test("pni matches, all provided, no pni session") {
given(null, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
}
// This test, I could go either way. We decide to change the E164 on the existing row rather than create a new one.
// But it's an "unstable E164->PNI mapping" case, which we don't expect, so as long as there's a user-visible impact that should be fine.
test("pni matches, all provided, existing pni session") {
given(null, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectSessionSwitchoverEvent(E164_A)
}
test("pni matches, no existing pni session, changes number") {
given(E164_B, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
@ -194,8 +242,15 @@ class RecipientTableTest_getAndPossiblyMerge {
expectChangeNumberEvent()
}
// This test, I could go either way. We decide to change the E164 on the existing row rather than create a new one.
// But it's an "unstable E164->PNI mapping" case, which we don't expect, so as long as there's a user-visible impact that should be fine.
test("pni matches, existing pni session, changes number") {
given(E164_B, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectSessionSwitchoverEvent(E164_B)
expectChangeNumberEvent()
}
test("pni and aci matches, change number") {
given(E164_B, PNI_A, ACI_A)
process(E164_A, PNI_A, ACI_A)
@ -220,7 +275,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expectChangeNumberEvent()
}
test("steal, e164+pni & e164+pni, no aci provided, no sessions") {
test("steal, e164+pni & e164+pni, no aci provided, no pni session") {
given(E164_A, PNI_B, null)
given(E164_B, PNI_A, null)
@ -230,6 +285,18 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_B, null, null)
}
test("steal, e164+pni & e164+pni, no aci provided, existing pni session") {
given(E164_A, PNI_B, null, pniSession = true)
given(E164_B, PNI_A, null) // TODO How to handle if this user had a session? They just end up losing the PNI, meaning it would become unregistered, but it could register again later with a different PNI?
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expect(E164_B, null, null)
expectSessionSwitchoverEvent(E164_A)
}
test("steal, e164+pni & aci, e164 record has separate e164") {
given(E164_B, PNI_A, null)
given(null, null, ACI_A)
@ -252,6 +319,19 @@ class RecipientTableTest_getAndPossiblyMerge {
expectChangeNumberEvent()
}
test("steal, e164 & pni+e164, no aci provided") {
val id1 = given(E164_A, null, null)
val id2 = given(E164_B, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, null)
expect(E164_A, PNI_A, null)
expect(E164_B, null, null)
expectSessionSwitchoverEvent(id1, E164_A)
expectSessionSwitchoverEvent(id2, E164_B)
}
test("merge, e164 & pni & aci, all provided") {
given(E164_A, null, null)
given(null, PNI_A, null)
@ -262,6 +342,8 @@ class RecipientTableTest_getAndPossiblyMerge {
expectDeleted()
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
}
test("merge, e164 & pni, no aci provided") {
@ -272,9 +354,11 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, null)
expectDeleted()
expectThreadMergeEvent("")
}
test("merge, e164 & pni, aci provided but no aci record") {
test("merge, e164 & pni, aci provided, no pni session") {
given(E164_A, null, null)
given(null, PNI_A, null)
@ -282,16 +366,33 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, ACI_A)
expectDeleted()
expectThreadMergeEvent("")
}
test("merge, e164 & pni+e164, no aci provided") {
test("merge, e164 & pni, aci provided, no pni session") {
given(E164_A, null, null)
given(E164_B, PNI_A, null)
given(null, PNI_A, null)
process(E164_A, PNI_A, null)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, null)
expect(E164_B, null, null)
expect(E164_A, PNI_A, ACI_A)
expectDeleted()
expectThreadMergeEvent("")
}
test("merge, e164 & pni, aci provided, existing pni session") {
given(E164_A, null, null)
given(null, PNI_A, null, pniSession = true)
process(E164_A, PNI_A, ACI_A)
expect(E164_A, PNI_A, ACI_A)
expectDeleted()
expectThreadMergeEvent("")
expectSessionSwitchoverEvent(E164_A)
}
test("merge, e164+pni & pni, no aci provided") {
@ -302,6 +403,8 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, null)
expectDeleted()
expectThreadMergeEvent("")
}
test("merge, e164+pni & aci") {
@ -312,6 +415,8 @@ class RecipientTableTest_getAndPossiblyMerge {
expectDeleted()
expect(E164_A, PNI_A, ACI_A)
expectThreadMergeEvent(E164_A)
}
test("merge, e164+pni & e164+pni+aci, change number") {
@ -324,6 +429,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
expectThreadMergeEvent(E164_A)
}
test("merge, e164+pni & e164+aci, change number") {
@ -336,6 +442,7 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, PNI_A, ACI_A)
expectChangeNumberEvent()
expectThreadMergeEvent(E164_A)
}
test("merge, e164 & aci") {
@ -346,6 +453,8 @@ class RecipientTableTest_getAndPossiblyMerge {
expectDeleted()
expect(E164_A, null, ACI_A)
expectThreadMergeEvent(E164_A)
}
test("merge, e164 & e164+aci, change number") {
@ -358,6 +467,8 @@ class RecipientTableTest_getAndPossiblyMerge {
expect(E164_A, null, ACI_A)
expectChangeNumberEvent()
expectThreadMergeEvent(E164_A)
}
test("local user, local e164 and aci provided, changeSelf=false, leave e164 alone") {
@ -539,11 +650,11 @@ class RecipientTableTest_getAndPossiblyMerge {
}
private fun getMention(messageId: Long): MentionModel {
SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionTable.TABLE_NAME} WHERE ${MentionTable.MESSAGE_ID} = $messageId").use { cursor ->
return SignalDatabase.rawDatabase.rawQuery("SELECT * FROM ${MentionTable.TABLE_NAME} WHERE ${MentionTable.MESSAGE_ID} = $messageId").use { cursor ->
cursor.moveToFirst()
return MentionModel(
recipientId = RecipientId.from(CursorUtil.requireLong(cursor, MentionTable.RECIPIENT_ID)),
threadId = CursorUtil.requireLong(cursor, MentionTable.THREAD_ID)
MentionModel(
recipientId = RecipientId.from(cursor.requireLong(MentionTable.RECIPIENT_ID)),
threadId = cursor.requireLong(MentionTable.THREAD_ID)
)
}
}
@ -577,6 +688,14 @@ class RecipientTableTest_getAndPossiblyMerge {
if (!test.changeNumberExpected) {
test.expectNoChangeNumberEvent()
}
if (!test.threadMergeExpected) {
test.expectNoThreadMergeEvent()
}
if (!test.sessionSwitchoverExpected) {
test.expectNoSessionSwitchoverEvent()
}
} catch (e: Throwable) {
if (e.javaClass != exception) {
val error = java.lang.AssertionError("[$name] ${e.message}")
@ -594,6 +713,8 @@ class RecipientTableTest_getAndPossiblyMerge {
private lateinit var outputRecipientId: RecipientId
var changeNumberExpected = false
var threadMergeExpected = false
var sessionSwitchoverExpected = false
init {
// Need to delete these first to prevent foreign key crash
@ -616,9 +737,8 @@ class RecipientTableTest_getAndPossiblyMerge {
pni: PNI?,
aci: ACI?,
createThread: Boolean = true,
sms: List<String> = emptyList(),
mms: List<String> = emptyList()
) {
pniSession: Boolean = false
): RecipientId {
val id = insert(e164, pni, aci)
generatedIds += id
if (createThread) {
@ -626,6 +746,16 @@ class RecipientTableTest_getAndPossiblyMerge {
SignalDatabase.threads.getOrCreateThreadIdFor(Recipient.resolved(id))
SignalDatabase.messages.insertMessageInbox(IncomingEncryptedMessage(IncomingTextMessage(id, 1, 0, 0, 0, "", Optional.empty(), 0, false, ""), ""))
}
if (pniSession) {
if (pni == null) {
throw IllegalArgumentException("pniSession = true but pni is null!")
}
SignalDatabase.sessions.store(pni, SignalProtocolAddress(pni.toString(), 1), SessionRecord())
}
return id
}
fun process(e164: String?, pni: PNI?, aci: ACI?, changeSelf: Boolean = false) {
@ -676,6 +806,32 @@ class RecipientTableTest_getAndPossiblyMerge {
changeNumberExpected = false
}
fun expectSessionSwitchoverEvent(e164: String) {
expectSessionSwitchoverEvent(outputRecipientId, e164)
}
fun expectSessionSwitchoverEvent(recipientId: RecipientId, e164: String) {
val event: SessionSwitchoverEvent? = getLatestSessionSwitchoverEvent(recipientId)
assertNotNull(event)
assertEquals(e164, event!!.e164)
sessionSwitchoverExpected = true
}
fun expectNoSessionSwitchoverEvent() {
assertNull(getLatestSessionSwitchoverEvent(outputRecipientId))
}
fun expectThreadMergeEvent(previousE164: String) {
val event: ThreadMergeEvent? = getLatestThreadMergeEvent(outputRecipientId)
assertNotNull(event)
assertEquals(previousE164, event!!.previousE164)
threadMergeExpected = true
}
fun expectNoThreadMergeEvent() {
assertNull(getLatestThreadMergeEvent(outputRecipientId))
}
private fun insert(e164: String?, pni: PNI?, aci: ACI?): RecipientId {
val serviceIdString: String? = (aci ?: pni)?.toString()
val pniString: String? = pni?.toString()
@ -746,6 +902,42 @@ class RecipientTableTest_getAndPossiblyMerge {
}
}
private fun getLatestThreadMergeEvent(recipientId: RecipientId): ThreadMergeEvent? {
return SignalDatabase.rawDatabase
.select(MessageTable.BODY)
.from(MessageTable.TABLE_NAME)
.where("${MessageTable.RECIPIENT_ID} = ? AND ${MessageTable.TYPE} = ?", recipientId, MessageTypes.THREAD_MERGE_TYPE)
.orderBy("${MessageTable.DATE_RECEIVED} DESC")
.limit(1)
.run()
.use { cursor: Cursor ->
if (cursor.moveToFirst()) {
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
ThreadMergeEvent.parseFrom(bytes)
} else {
null
}
}
}
private fun getLatestSessionSwitchoverEvent(recipientId: RecipientId): SessionSwitchoverEvent? {
return SignalDatabase.rawDatabase
.select(MessageTable.BODY)
.from(MessageTable.TABLE_NAME)
.where("${MessageTable.RECIPIENT_ID} = ? AND ${MessageTable.TYPE} = ?", recipientId, MessageTypes.SESSION_SWITCHOVER_TYPE)
.orderBy("${MessageTable.DATE_RECEIVED} DESC")
.limit(1)
.run()
.use { cursor: Cursor ->
if (cursor.moveToFirst()) {
val bytes = Base64.decode(cursor.requireNonNullString(MessageTable.BODY))
SessionSwitchoverEvent.parseFrom(bytes)
} else {
null
}
}
}
companion object {
val ACI_A = ACI.from(UUID.fromString("aaaa0000-5a76-47fa-a98a-7e72c948a82e"))
val ACI_B = ACI.from(UUID.fromString("bbbb0000-0b60-4a68-9cd9-ed2f8453f9ed"))

Wyświetl plik

@ -111,7 +111,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SetAci(result.id, ACI_A)
)
@ -131,9 +131,9 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
PnpOperation.SessionSwitchoverInsert(result.id, E164_A)
)
),
result.changeSet
@ -151,7 +151,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
@ -170,7 +170,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetAci(result.id, ACI_A)
)
),
@ -189,9 +189,9 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
PnpOperation.SessionSwitchoverInsert(result.id, E164_A)
)
),
result.changeSet
@ -209,7 +209,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetPni(result.id, PNI_A)
)
),
@ -228,7 +228,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A)
)
@ -248,10 +248,10 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.SessionSwitchoverInsert(result.id)
PnpOperation.SessionSwitchoverInsert(result.id, E164_A)
)
),
result.changeSet
@ -269,7 +269,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetAci(result.id, ACI_A),
PnpOperation.ChangeNumberInsert(
@ -277,7 +277,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
oldE164 = E164_B,
newE164 = E164_A
),
PnpOperation.SessionSwitchoverInsert(result.id)
PnpOperation.SessionSwitchoverInsert(result.id, E164_A)
)
),
result.changeSet
@ -295,7 +295,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
)
),
@ -314,7 +314,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.ChangeNumberInsert(
recipientId = result.id,
@ -338,7 +338,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A)
)
@ -358,7 +358,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.id),
operations = listOf(
operations = linkedSetOf(
PnpOperation.SetE164(result.id, E164_A),
PnpOperation.SetPni(result.id, PNI_A),
PnpOperation.ChangeNumberInsert(
@ -387,7 +387,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.thirdId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
@ -416,7 +416,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
@ -441,7 +441,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.Merge(
primaryId = result.firstId,
secondaryId = result.secondId
@ -470,7 +470,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(
recipientId = result.firstId,
@ -496,7 +496,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.Merge(
primaryId = result.firstId,
@ -522,7 +522,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A)
)
@ -545,11 +545,11 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.firstId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.SetPni(result.firstId, PNI_A),
PnpOperation.SessionSwitchoverInsert(result.secondId),
PnpOperation.SessionSwitchoverInsert(result.firstId)
PnpOperation.SessionSwitchoverInsert(result.secondId, E164_A),
PnpOperation.SessionSwitchoverInsert(result.firstId, E164_A)
)
),
result.changeSet
@ -570,7 +570,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.Merge(
primaryId = result.secondId,
secondaryId = result.firstId
@ -595,7 +595,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.SetPni(
recipientId = result.secondId,
@ -625,7 +625,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.RemovePni(result.firstId),
PnpOperation.SetPni(
recipientId = result.secondId,
@ -660,7 +660,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.RemovePni(result.secondId),
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
@ -692,7 +692,7 @@ class RecipientTableTest_processPnpTupleToChangeSet {
assertEquals(
PnpChangeSet(
id = PnpIdResolver.PnpNoopId(result.secondId),
operations = listOf(
operations = linkedSetOf(
PnpOperation.RemoveE164(result.secondId),
PnpOperation.Merge(
primaryId = result.secondId,

Wyświetl plik

@ -0,0 +1,11 @@
package org.thoughtcrime.securesms.util;
/**
* A class that allows us to inject feature flags during tests.
*/
public final class FeatureFlagsAccessor {
public static void forceValue(String key, Object value) {
FeatureFlags.FORCED_VALUES.put(FeatureFlags.PHONE_NUMBER_PRIVACY, true);
}
}

Wyświetl plik

@ -73,6 +73,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateD
import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState;
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent;
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.insights.InsightsConstants;
@ -94,6 +95,7 @@ import org.thoughtcrime.securesms.sms.IncomingGroupUpdateMessage;
import org.thoughtcrime.securesms.sms.IncomingTextMessage;
import org.thoughtcrime.securesms.stories.Stories;
import org.thoughtcrime.securesms.util.Base64;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
@ -1125,6 +1127,26 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId);
}
public void insertSessionSwitchoverEvent(@NonNull RecipientId recipientId, long threadId, @NonNull SessionSwitchoverEvent event) {
if (!FeatureFlags.phoneNumberPrivacy()) {
throw new IllegalStateException("Should not occur in a non-PNP world!");
}
ContentValues values = new ContentValues();
values.put(RECIPIENT_ID, recipientId.serialize());
values.put(RECIPIENT_DEVICE_ID, 1);
values.put(DATE_RECEIVED, System.currentTimeMillis());
values.put(DATE_SENT, System.currentTimeMillis());
values.put(READ, 1);
values.put(TYPE, MessageTypes.SESSION_SWITCHOVER_TYPE);
values.put(THREAD_ID, threadId);
values.put(BODY, Base64.encodeBytes(event.toByteArray()));
getWritableDatabase().insert(TABLE_NAME, null, values);
ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId);
}
public void insertSmsExportMessage(@NonNull RecipientId recipientId, long threadId) {
ContentValues values = new ContentValues();
values.put(RECIPIENT_ID, recipientId.serialize());

Wyświetl plik

@ -50,6 +50,7 @@ public interface MessageTypes {
long BOOST_REQUEST_TYPE = 15;
long THREAD_MERGE_TYPE = 16;
long SMS_EXPORT_TYPE = 17;
long SESSION_SWITCHOVER_TYPE = 18;
long BASE_INBOX_TYPE = 20;
long BASE_OUTBOX_TYPE = 21;
@ -207,6 +208,10 @@ public interface MessageTypes {
return (type & BASE_TYPE_MASK) == THREAD_MERGE_TYPE;
}
static boolean isSessionSwitchoverType(long type) {
return (type & BASE_TYPE_MASK) == SESSION_SWITCHOVER_TYPE;
}
static boolean isSecureType(long type) {
return (type & SECURE_MESSAGE_BIT) != 0;
}

Wyświetl plik

@ -37,7 +37,7 @@ data class PnpDataSet(
* Applies the set of operations and returns the resulting dataset.
* Important: This only occurs _in memory_. You must still apply the operations to disk to persist them.
*/
fun perform(operations: List<PnpOperation>): PnpDataSet {
fun perform(operations: LinkedHashSet<PnpOperation>): PnpDataSet {
if (operations.isEmpty()) {
return this
}
@ -136,7 +136,7 @@ data class PnpDataSet(
*/
data class PnpChangeSet(
val id: PnpIdResolver,
val operations: List<PnpOperation> = emptyList(),
val operations: LinkedHashSet<PnpOperation> = linkedSetOf(),
val breadCrumbs: List<String> = emptyList()
) {
// We want to exclude breadcrumbs from equality for testing purposes
@ -213,7 +213,8 @@ sealed class PnpOperation {
}
data class SessionSwitchoverInsert(
override val recipientId: RecipientId
override val recipientId: RecipientId,
val e164: String?
) : PnpOperation()
data class ChangeNumberInsert(

Wyświetl plik

@ -66,6 +66,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor
import org.thoughtcrime.securesms.database.model.databaseprotos.DeviceLastResetTime
import org.thoughtcrime.securesms.database.model.databaseprotos.ExpiringProfileKeyCredentialColumnData
import org.thoughtcrime.securesms.database.model.databaseprotos.RecipientExtras
import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
@ -2367,7 +2368,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
affectedIds = affectedIds,
oldIds = oldIds,
changedNumberId = changedNumberId,
operations = changeSet.operations,
operations = changeSet.operations.toList(),
breadCrumbs = changeSet.breadCrumbs
)
}
@ -2436,8 +2437,14 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
merge(operation.primaryId, operation.secondaryId, inputPni)
}
is PnpOperation.SessionSwitchoverInsert -> {
// TODO [pnp]
Log.w(TAG, "Session switchover events aren't implemented yet!")
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 -> {
if (changeSet.id is PnpIdResolver.PnpNoopId) {
@ -2544,7 +2551,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
check(fullData.commonId == null)
check(listOfNotNull(fullData.byE164, fullData.byPniSid, fullData.byPniOnly, fullData.byAciSid).size >= 2)
val operations: MutableList<PnpOperation> = mutableListOf()
val operations: LinkedHashSet<PnpOperation> = linkedSetOf()
operations += processPossibleE164PniSidMerge(pni, pniVerified, fullData, breadCrumbs)
operations += processPossiblePniSidAciSidMerge(e164, pni, aci, fullData.perform(operations), changeSelf, breadCrumbs)
@ -2559,6 +2566,13 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
recipientId = primaryId,
aci = aci
)
if (!pniVerified && finalData.pni != null && sessions.hasAnySessionFor(finalData.pni.toString())) {
operations += PnpOperation.SessionSwitchoverInsert(
recipientId = primaryId,
e164 = finalData.e164
)
}
}
if (finalData.byE164 == null && e164 != null && (changeSelf || notSelf(e164, pni, aci))) {
@ -2599,7 +2613,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
private fun processNonMergePnpUpdate(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean, commonId: RecipientId, breadCrumbs: MutableList<String>): PnpChangeSet {
val record: RecipientRecord = getRecord(commonId)
val operations: MutableList<PnpOperation> = mutableListOf()
val operations: LinkedHashSet<PnpOperation> = linkedSetOf()
// This is a special case. The ACI passed in doesn't match the common record. We can't change ACIs, so we need to make a new record.
if (aci != null && aci != record.serviceId && record.serviceId != null && !record.sidIsPni()) {
@ -2658,7 +2672,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
val newServiceId: ServiceId? = aci ?: pni ?: record.serviceId
if (!pniVerified && record.serviceId != null && record.serviceId != newServiceId && sessions.hasAnySessionFor(record.serviceId.toString())) {
operations += PnpOperation.SessionSwitchoverInsert(commonId)
operations += PnpOperation.SessionSwitchoverInsert(recipientId = commonId, e164 = record.e164 ?: e164)
}
return PnpChangeSet(
@ -2668,15 +2682,15 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
)
}
private fun processPossibleE164PniSidMerge(pni: PNI?, pniVerified: Boolean, data: PnpDataSet, breadCrumbs: MutableList<String>): List<PnpOperation> {
private fun processPossibleE164PniSidMerge(pni: PNI?, pniVerified: Boolean, data: PnpDataSet, breadCrumbs: MutableList<String>): LinkedHashSet<PnpOperation> {
if (pni == null || data.byE164 == null || data.byPniSid == null || data.e164Record == null || data.pniSidRecord == null || data.e164Record.id == data.pniSidRecord.id) {
return emptyList()
return linkedSetOf()
}
// We have found records for both the E164 and PNI, and they're different
breadCrumbs += "E164PniSidMerge"
val operations: MutableList<PnpOperation> = mutableListOf()
val operations: LinkedHashSet<PnpOperation> = linkedSetOf()
// The PNI record only has a single identifier. We know we must merge.
if (data.pniSidRecord.sidOnly(pni)) {
@ -2704,31 +2718,35 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
)
if (!pniVerified && sessions.hasAnySessionFor(data.pniSidRecord.serviceId.toString())) {
operations += PnpOperation.SessionSwitchoverInsert(data.byPniSid)
operations += PnpOperation.SessionSwitchoverInsert(recipientId = data.byPniSid, e164 = data.pniSidRecord.e164)
if (data.e164Record.serviceId == null || data.e164Record.sidIsPni()) {
operations += PnpOperation.SessionSwitchoverInsert(recipientId = data.byE164, e164 = data.e164Record.e164)
}
}
if (!pniVerified && data.e164Record.serviceId != null && data.e164Record.sidIsPni() && sessions.hasAnySessionFor(data.e164Record.serviceId.toString())) {
operations += PnpOperation.SessionSwitchoverInsert(data.byE164)
operations += PnpOperation.SessionSwitchoverInsert(recipientId = data.byE164, e164 = data.e164)
}
}
return operations
}
private fun processPossiblePniSidAciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet, changeSelf: Boolean, breadCrumbs: MutableList<String>): List<PnpOperation> {
private fun processPossiblePniSidAciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet, changeSelf: Boolean, breadCrumbs: MutableList<String>): LinkedHashSet<PnpOperation> {
if (pni == null || aci == null || data.byPniSid == null || data.byAciSid == null || data.pniSidRecord == null || data.aciSidRecord == null || data.pniSidRecord.id == data.aciSidRecord.id) {
return emptyList()
return linkedSetOf()
}
if (!changeSelf && isSelf(e164, pni, aci)) {
breadCrumbs += "ChangeSelfPreventsPniSidAciSidMerge"
return emptyList()
return linkedSetOf()
}
// We have found records for both the PNI and ACI, and they're different
breadCrumbs += "PniSidAciSidMerge"
val operations: MutableList<PnpOperation> = mutableListOf()
val operations: LinkedHashSet<PnpOperation> = linkedSetOf()
// The PNI record only has a single identifier. We know we must merge.
if (data.pniSidRecord.sidOnly(pni)) {

Wyświetl plik

@ -86,7 +86,7 @@ public final class FeatureFlags {
private static final String USE_HARDWARE_AEC_IF_OLD = "android.calling.useHardwareAecIfOlderThanApi29";
private static final String USE_AEC3 = "android.calling.useAec3";
private static final String PAYMENTS_COUNTRY_BLOCKLIST = "android.payments.blocklist";
private static final String PHONE_NUMBER_PRIVACY = "android.pnp";
public static final String PHONE_NUMBER_PRIVACY = "android.pnp";
private static final String USE_FCM_FOREGROUND_SERVICE = "android.useFcmForegroundService.3";
private static final String STORIES_AUTO_DOWNLOAD_MAXIMUM = "android.stories.autoDownloadMaximum";
private static final String TELECOM_MANUFACTURER_ALLOWLIST = "android.calling.telecomAllowList";

Wyświetl plik

@ -265,4 +265,8 @@ message MessageExportState {
message ThreadMergeEvent {
string previousE164 = 1;
}
}
message SessionSwitchoverEvent {
string e164 = 1;
}

Wyświetl plik

@ -1364,10 +1364,12 @@
<string name="MessageRecord_s_changed_their_phone_number">%1$s changed their phone number.</string>
<!-- Update item message shown in the release channel when someone is already a sustainer so we ask them if they want to boost. -->
<string name="MessageRecord_like_this_new_feature_help_support_signal_with_a_one_time_donation">Like this new feature? Help support Signal with a one-time donation.</string>
<!-- Update item message shown when we merge two threads together -->
<!-- Update item message shown when we merge two threads together. First placeholder is a name, second placeholder is a phone number. -->
<string name="MessageRecord_your_message_history_with_s_and_their_number_s_has_been_merged">Your message history with %1$s and their number %2$s has been merged.</string>
<!-- Update item message shown when we merge two threads together and we don\'t know the phone number of the other thread -->
<!-- Update item message shown when we merge two threads together and we don\'t know the phone number of the other thread. The placeholder is a person's name. -->
<string name="MessageRecord_your_message_history_with_s_and_another_chat_has_been_merged">Your message history with %1$s and another chat that belonged to them has been merged.</string>
<!-- Update item message shown when you find out a phone number belongs to a person you had a conversation with. First placeholder is a phone number, second placeholder is a name. -->
<string name="MessageRecord_s_belongs_to_s">%1$s belongs to %2$s</string>
<!-- Message to notify sender that activate payments request has been sent to the recipient -->
<string name="MessageRecord_you_sent_request">You sent %s a request to activate Payments</string>
<!-- Request message from recipient to activate payments -->