Use the PNP merging function for everything.

This commit is contained in:
Greyson Parrelli
2022-08-09 18:36:04 -04:00
committed by GitHub
parent e83a4692c5
commit f004b72ba2
8 changed files with 430 additions and 212 deletions

View File

@@ -47,15 +47,6 @@ data class PnpDataSet(
for (operation in operations) {
@Exhaustive
when (operation) {
is PnpOperation.Update -> {
records.replace(operation.recipientId) { record ->
record.copy(
e164 = operation.e164,
pni = operation.pni,
serviceId = operation.aci ?: operation.pni
)
}
}
is PnpOperation.RemoveE164 -> {
records.replace(operation.recipientId) { it.copy(e164 = null) }
}
@@ -145,8 +136,28 @@ data class PnpDataSet(
*/
data class PnpChangeSet(
val id: PnpIdResolver,
val operations: List<PnpOperation> = emptyList()
)
val operations: List<PnpOperation> = emptyList(),
val breadCrumbs: List<String> = emptyList()
) {
// We want to exclude breadcrumbs from equality for testing purposes
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as PnpChangeSet
if (id != other.id) return false
if (operations != other.operations) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + operations.hashCode()
return result
}
}
sealed class PnpIdResolver {
data class PnpNoopId(
@@ -165,33 +176,28 @@ sealed class PnpIdResolver {
* Lets us describe various situations as a series of operations, making code clearer and tests easier.
*/
sealed class PnpOperation {
data class Update(
val recipientId: RecipientId,
val e164: String?,
val pni: PNI?,
val aci: ACI?
) : PnpOperation()
abstract val recipientId: RecipientId
data class RemoveE164(
val recipientId: RecipientId
override val recipientId: RecipientId
) : PnpOperation()
data class RemovePni(
val recipientId: RecipientId
override val recipientId: RecipientId
) : PnpOperation()
data class SetE164(
val recipientId: RecipientId,
override val recipientId: RecipientId,
val e164: String
) : PnpOperation()
data class SetPni(
val recipientId: RecipientId,
override val recipientId: RecipientId,
val pni: PNI
) : PnpOperation()
data class SetAci(
val recipientId: RecipientId,
override val recipientId: RecipientId,
val aci: ACI
) : PnpOperation()
@@ -201,14 +207,17 @@ sealed class PnpOperation {
data class Merge(
val primaryId: RecipientId,
val secondaryId: RecipientId
) : PnpOperation()
) : PnpOperation() {
override val recipientId: RecipientId
get() = throw UnsupportedOperationException()
}
data class SessionSwitchoverInsert(
val recipientId: RecipientId
override val recipientId: RecipientId
) : PnpOperation()
data class ChangeNumberInsert(
val recipientId: RecipientId,
override val recipientId: RecipientId,
val oldE164: String,
val newE164: String
) : PnpOperation()

View File

@@ -106,7 +106,6 @@ import org.whispersystems.signalservice.api.storage.SignalContactRecord
import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
import org.whispersystems.signalservice.api.storage.StorageId
import org.whispersystems.signalservice.api.util.Preconditions
import java.io.Closeable
import java.io.IOException
import java.util.Arrays
@@ -421,7 +420,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
@JvmOverloads
fun getAndPossiblyMerge(serviceId: ServiceId?, e164: String?, changeSelf: Boolean = false): RecipientId {
return if (FeatureFlags.phoneNumberPrivacy()) {
return if (FeatureFlags.recipientMergeV2()) {
getAndPossiblyMergePnp(serviceId, e164, changeSelf)
} else {
getAndPossiblyMergeLegacy(serviceId, e164, changeSelf)
@@ -525,15 +524,54 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
fun getAndPossiblyMergePnp(serviceId: ServiceId?, e164: String?, changeSelf: Boolean = false): RecipientId {
require(!(serviceId == null && e164 == null)) { "Must provide an ACI or E164!" }
return writableDatabase.withinTransaction {
when {
val db = writableDatabase
var transactionSuccessful = false
lateinit var result: ProcessPnpTupleResult
db.beginTransaction()
try {
result = when {
serviceId is PNI -> processPnpTuple(e164, serviceId, null, pniVerified = false, changeSelf = changeSelf)
serviceId is ACI -> processPnpTuple(e164, null, serviceId, pniVerified = false, changeSelf = changeSelf)
serviceId == null -> processPnpTuple(e164, null, null, pniVerified = false, changeSelf = changeSelf)
getByPni(PNI.from(serviceId.uuid())).isPresent -> processPnpTuple(e164, PNI.from(serviceId.uuid()), null, pniVerified = false, changeSelf = changeSelf)
else -> processPnpTuple(e164, null, ACI.fromNullable(serviceId), pniVerified = false, changeSelf = changeSelf)
}
if (result.operations.isNotEmpty()) {
Log.i(TAG, "[getAndPossiblyMergePnp] BreadCrumbs: ${result.breadCrumbs}, Operations: ${result.operations}")
}
db.setTransactionSuccessful()
transactionSuccessful = true
} finally {
db.endTransaction()
if (transactionSuccessful) {
if (result.affectedIds.isNotEmpty()) {
result.affectedIds.forEach { ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(it) }
RetrieveProfileJob.enqueue(result.affectedIds)
}
if (result.oldIds.isNotEmpty()) {
result.oldIds.forEach { oldId ->
Recipient.live(oldId).refresh(result.finalId)
ApplicationDependencies.getRecipientCache().remap(oldId, result.finalId)
}
}
if (result.affectedIds.isNotEmpty() || result.oldIds.isNotEmpty()) {
StorageSyncHelper.scheduleSyncForDataChange()
RecipientId.clearCache()
}
if (result.changedNumberId != null) {
ApplicationDependencies.getJobManager().add(RecipientChangedNumberJob(result.changedNumberId!!))
}
}
}
return result.finalId
}
fun getAllServiceIdProfileKeyPairs(): Map<ServiceId, ProfileKey> {
@@ -2190,7 +2228,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
db.beginTransaction()
try {
for ((e164, result) in mapping) {
ids += processPnpTuple(e164, result.pni, result.aci, false)
ids += processPnpTuple(e164, result.pni, result.aci, false).finalId
}
db.setTransactionSuccessful()
@@ -2208,26 +2246,49 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
* @return The [RecipientId] of the resulting recipient.
*/
@VisibleForTesting
fun processPnpTuple(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false): RecipientId {
val changeSet = processPnpTupleToChangeSet(e164, pni, aci, pniVerified, changeSelf)
return writePnpChangeSetToDisk(changeSet)
}
fun processPnpTuple(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false, pnpEnabled: Boolean = FeatureFlags.phoneNumberPrivacy()): ProcessPnpTupleResult {
val changeSet: PnpChangeSet = processPnpTupleToChangeSet(e164, pni, aci, pniVerified, changeSelf)
val affectedIds: MutableSet<RecipientId> = mutableSetOf()
val oldIds: MutableSet<RecipientId> = mutableSetOf()
var changedNumberId: RecipientId? = null
@VisibleForTesting
fun writePnpChangeSetToDisk(changeSet: PnpChangeSet): RecipientId {
for (operation in changeSet.operations) {
@Exhaustive
when (operation) {
is PnpOperation.Update -> {
writableDatabase.update(TABLE_NAME)
.values(
PHONE to operation.e164,
SERVICE_ID to (operation.aci ?: operation.pni).toString(),
PNI_COLUMN to operation.pni.toString()
)
.where("$ID = ?", operation.recipientId)
.run()
is PnpOperation.RemoveE164,
is PnpOperation.RemovePni,
is PnpOperation.SetAci,
is PnpOperation.SetE164,
is PnpOperation.SetPni -> {
affectedIds.add(operation.recipientId)
}
is PnpOperation.Merge -> {
oldIds.add(operation.secondaryId)
affectedIds.add(operation.primaryId)
}
is PnpOperation.SessionSwitchoverInsert -> {}
is PnpOperation.ChangeNumberInsert -> changedNumberId = operation.recipientId
}
}
val finalId: RecipientId = writePnpChangeSetToDisk(changeSet, pnpEnabled)
return ProcessPnpTupleResult(
finalId = finalId,
affectedIds = affectedIds,
oldIds = oldIds,
changedNumberId = changedNumberId,
operations = changeSet.operations,
breadCrumbs = changeSet.breadCrumbs
)
}
@VisibleForTesting
fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, pnpEnabled: Boolean): RecipientId {
for (operation in changeSet.operations) {
@Exhaustive
when (operation) {
is PnpOperation.RemoveE164 -> {
writableDatabase
.update(TABLE_NAME)
@@ -2282,26 +2343,34 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
.run()
}
is PnpOperation.Merge -> {
Log.w(TAG, "WARNING: Performing a PNP merge! This operation currently only has a basic implementation only suitable for basic testing!")
val primary = getRecord(operation.primaryId)
val secondary = getRecord(operation.secondaryId)
writableDatabase
.delete(TABLE_NAME)
.where("$ID = ?", operation.secondaryId)
.run()
if (primary.serviceId != null && !primary.sidIsPni() && secondary.e164 != null) {
merge(operation.primaryId, operation.secondaryId)
} else {
if (!pnpEnabled) {
throw AssertionError("This type of merge is not supported in production!")
}
writableDatabase
.update(TABLE_NAME)
.values(
PHONE to (primary.e164 ?: secondary.e164),
PNI_COLUMN to (primary.pni ?: secondary.pni)?.toString(),
SERVICE_ID to (primary.serviceId ?: secondary.serviceId)?.toString(),
REGISTERED to RegisteredState.REGISTERED.id
)
.where("$ID = ?", operation.primaryId)
.run()
Log.w(TAG, "WARNING: Performing an unfinished PNP merge! This operation currently only has a basic implementation only suitable for basic testing!")
writableDatabase
.delete(TABLE_NAME)
.where("$ID = ?", operation.secondaryId)
.run()
writableDatabase
.update(TABLE_NAME)
.values(
PHONE to (primary.e164 ?: secondary.e164),
PNI_COLUMN to (primary.pni ?: secondary.pni)?.toString(),
SERVICE_ID to (primary.serviceId ?: secondary.serviceId)?.toString(),
REGISTERED to RegisteredState.REGISTERED.id
)
.where("$ID = ?", operation.primaryId)
.run()
}
}
is PnpOperation.SessionSwitchoverInsert -> {
// TODO [pnp]
@@ -2334,8 +2403,10 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
*/
@VisibleForTesting
fun processPnpTupleToChangeSet(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false): PnpChangeSet {
Preconditions.checkArgument(e164 != null || pni != null || aci != null, "Must provide at least one field!")
Preconditions.checkArgument(pni == null || e164 != null, "If a PNI is provided, you must also provide an E164!")
check(e164 != null || pni != null || aci != null) { "Must provide at least one field!" }
check(pni == null || e164 != null) { "If a PNI is provided, you must also provide an E164!" }
val breadCrumbs: MutableList<String> = mutableListOf()
val partialData = PnpDataSet(
e164 = e164,
@@ -2347,91 +2418,44 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
byAciSid = aci?.let { getByServiceId(it).orElse(null) }
)
val allRequiredDbFields: List<RecipientId?> = if (aci != null) {
listOf(partialData.byE164, partialData.byAciSid, partialData.byPniOnly)
} else {
listOf(partialData.byE164, partialData.byPniSid, partialData.byPniOnly)
val allRequiredDbFields: MutableList<RecipientId?> = mutableListOf()
if (e164 != null) {
allRequiredDbFields += partialData.byE164
}
if (aci != null) {
allRequiredDbFields += partialData.byAciSid
}
if (pni != null) {
allRequiredDbFields += partialData.byPniOnly
}
if (pni != null && aci == null) {
allRequiredDbFields += partialData.byPniSid
}
val allRequiredDbFieldPopulated: Boolean = allRequiredDbFields.all { it != null }
// All IDs agree and the database is up-to-date
if (partialData.commonId != null && allRequiredDbFieldPopulated) {
return PnpChangeSet(id = PnpIdResolver.PnpNoopId(partialData.commonId))
breadCrumbs.add("CommonIdAndUpToDate")
return PnpChangeSet(id = PnpIdResolver.PnpNoopId(partialData.commonId), breadCrumbs = breadCrumbs)
}
// All ID's agree, but we need to update the database
if (partialData.commonId != null && !allRequiredDbFieldPopulated) {
val record: RecipientRecord = getRecord(partialData.commonId)
val operations: MutableList<PnpOperation> = mutableListOf()
// 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()) {
if (record.e164 == e164) {
operations += PnpOperation.RemoveE164(record.id)
operations += PnpOperation.RemovePni(record.id)
} else if (record.pni == pni) {
operations += PnpOperation.RemovePni(record.id)
}
return PnpChangeSet(
id = PnpIdResolver.PnpInsert(e164, pni, aci),
operations = operations
)
}
var updatedNumber = false
if (e164 != null && record.e164 != e164 && (changeSelf || aci != SignalStore.account().aci)) {
operations += PnpOperation.SetE164(
recipientId = partialData.commonId,
e164 = e164
)
updatedNumber = true
}
if (pni != null && record.pni != pni) {
operations += PnpOperation.SetPni(
recipientId = partialData.commonId,
pni = pni
)
}
if (aci != null && record.serviceId != aci) {
operations += PnpOperation.SetAci(
recipientId = partialData.commonId,
aci = aci
)
}
if (record.e164 != null && updatedNumber) {
operations += PnpOperation.ChangeNumberInsert(
recipientId = partialData.commonId,
oldE164 = record.e164,
newE164 = e164!!
)
}
val newServiceId: ServiceId? = aci ?: pni ?: record.serviceId
if (!pniVerified && record.serviceId != null && record.serviceId != newServiceId && sessions.hasAnySessionFor(record.serviceId.toString())) {
operations += PnpOperation.SessionSwitchoverInsert(partialData.commonId)
}
return PnpChangeSet(
id = PnpIdResolver.PnpNoopId(partialData.commonId),
operations = operations
)
breadCrumbs.add("CommonIdButNeedsUpdate")
return processNonMergePnpUpdate(e164, pni, aci, commonId = partialData.commonId, pniVerified = pniVerified, changeSelf = changeSelf, breadCrumbs = breadCrumbs)
}
// Nothing matches
if (partialData.byE164 == null && partialData.byPniSid == null && partialData.byAciSid == null) {
breadCrumbs += "NothingMatches"
return PnpChangeSet(
id = PnpIdResolver.PnpInsert(
e164 = e164,
pni = pni,
aci = aci
)
),
breadCrumbs = breadCrumbs
)
}
@@ -2444,32 +2468,36 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
// It may be that some data just gets shuffled around, or it may be that
// two or more records get merged into one record, with the others being deleted.
breadCrumbs += "NeedsMerge"
val fullData = partialData.copy(
e164Record = partialData.byE164?.let { getRecord(it) },
pniSidRecord = partialData.byPniSid?.let { getRecord(it) },
aciSidRecord = partialData.byAciSid?.let { getRecord(it) },
)
Preconditions.checkState(fullData.commonId == null)
Preconditions.checkState(listOfNotNull(fullData.byE164, fullData.byPniSid, fullData.byPniOnly, fullData.byAciSid).size >= 2)
check(fullData.commonId == null)
check(listOfNotNull(fullData.byE164, fullData.byPniSid, fullData.byPniOnly, fullData.byAciSid).size >= 2)
val operations: MutableList<PnpOperation> = mutableListOf()
operations += processPossibleE164PniSidMerge(pni, pniVerified, fullData)
operations += processPossiblePniSidAciSidMerge(e164, pni, aci, fullData.perform(operations))
operations += processPossibleE164AciSidMerge(e164, pni, aci, fullData.perform(operations))
operations += processPossibleE164PniSidMerge(pni, pniVerified, fullData, breadCrumbs)
operations += processPossiblePniSidAciSidMerge(e164, pni, aci, fullData.perform(operations), changeSelf, breadCrumbs)
operations += processPossibleE164AciSidMerge(e164, pni, aci, fullData.perform(operations), changeSelf, breadCrumbs)
val finalData: PnpDataSet = fullData.perform(operations)
val primaryId: RecipientId = listOfNotNull(finalData.byAciSid, finalData.byE164, finalData.byPniSid).first()
if (finalData.byAciSid == null && aci != null) {
breadCrumbs += "FinalUpdateAci"
operations += PnpOperation.SetAci(
recipientId = primaryId,
aci = aci
)
}
if (finalData.byE164 == null && e164 != null && (changeSelf || aci != SignalStore.account().aci)) {
if (finalData.byE164 == null && e164 != null && (changeSelf || notSelf(e164, pni, aci))) {
breadCrumbs += "FinalUpdateE164"
operations += PnpOperation.SetE164(
recipientId = primaryId,
e164 = e164
@@ -2477,6 +2505,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
}
if (finalData.byPniSid == null && finalData.byPniOnly == null && pni != null) {
breadCrumbs += "FinalUpdatePni"
operations += PnpOperation.SetPni(
recipientId = primaryId,
pni = pni
@@ -2485,21 +2514,109 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
return PnpChangeSet(
id = PnpIdResolver.PnpNoopId(primaryId),
operations = operations
operations = operations,
breadCrumbs = breadCrumbs
)
}
private fun processPossibleE164PniSidMerge(pni: PNI?, pniVerified: Boolean, data: PnpDataSet): List<PnpOperation> {
private fun notSelf(e164: String?, pni: PNI?, aci: ACI?): Boolean {
return (e164 == null || e164 != SignalStore.account().e164) &&
(pni == null || pni != SignalStore.account().pni) &&
(aci == null || aci != SignalStore.account().aci)
}
private fun isSelf(e164: String?, pni: PNI?, aci: ACI?): Boolean {
return (e164 != null && e164 == SignalStore.account().e164) ||
(pni != null && pni == SignalStore.account().pni) ||
(aci != null && aci == SignalStore.account().aci)
}
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()
// 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()) {
breadCrumbs += "AciDoesNotMatchCommonRecord"
if (record.e164 == e164 && (changeSelf || notSelf(e164, pni, aci))) {
breadCrumbs += "StealingE164"
operations += PnpOperation.RemoveE164(record.id)
operations += PnpOperation.RemovePni(record.id)
} else if (record.pni == pni) {
breadCrumbs += "StealingPni"
operations += PnpOperation.RemovePni(record.id)
}
val insertE164: String? = if (changeSelf || notSelf(e164, pni, aci)) e164 else null
val insertPni: PNI? = if (changeSelf || notSelf(e164, pni, aci)) pni else null
return PnpChangeSet(
id = PnpIdResolver.PnpInsert(insertE164, insertPni, aci),
operations = operations,
breadCrumbs = breadCrumbs
)
}
var updatedNumber = false
if (e164 != null && record.e164 != e164 && (changeSelf || notSelf(e164, pni, aci))) {
operations += PnpOperation.SetE164(
recipientId = commonId,
e164 = e164
)
updatedNumber = true
}
if (pni != null && record.pni != pni) {
operations += PnpOperation.SetPni(
recipientId = commonId,
pni = pni
)
}
if (aci != null && record.serviceId != aci) {
operations += PnpOperation.SetAci(
recipientId = commonId,
aci = aci
)
}
if (record.e164 != null && updatedNumber) {
operations += PnpOperation.ChangeNumberInsert(
recipientId = commonId,
oldE164 = record.e164,
newE164 = e164!!
)
}
val newServiceId: ServiceId? = aci ?: pni ?: record.serviceId
if (!pniVerified && record.serviceId != null && record.serviceId != newServiceId && sessions.hasAnySessionFor(record.serviceId.toString())) {
operations += PnpOperation.SessionSwitchoverInsert(commonId)
}
return PnpChangeSet(
id = PnpIdResolver.PnpNoopId(commonId),
operations = operations,
breadCrumbs = breadCrumbs
)
}
private fun processPossibleE164PniSidMerge(pni: PNI?, pniVerified: Boolean, data: PnpDataSet, breadCrumbs: MutableList<String>): List<PnpOperation> {
if (pni == null || data.byE164 == null || data.byPniSid == null || data.e164Record == null || data.pniSidRecord == null || data.e164Record.id == data.pniSidRecord.id) {
return emptyList()
}
// We have found records for both the E164 and PNI, and they're different
breadCrumbs += "E164PniSidMerge"
val operations: MutableList<PnpOperation> = mutableListOf()
// The PNI record only has a single identifier. We know we must merge.
if (data.pniSidRecord.sidOnly(pni)) {
breadCrumbs += "PniOnly"
if (data.e164Record.pni != null) {
operations += PnpOperation.RemovePni(data.byE164)
}
@@ -2511,8 +2628,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
// TODO: Possible session switchover?
} else {
Preconditions.checkState(!data.pniSidRecord.pniAndAci())
Preconditions.checkState(data.pniSidRecord.e164 != null)
check(!data.pniSidRecord.pniAndAci() && data.pniSidRecord.e164 != null)
breadCrumbs += "PniSidRecordHasE164"
operations += PnpOperation.RemovePni(data.byPniSid)
operations += PnpOperation.SetPni(
@@ -2532,17 +2650,25 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
return operations
}
private fun processPossiblePniSidAciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet): List<PnpOperation> {
private fun processPossiblePniSidAciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet, changeSelf: Boolean, breadCrumbs: MutableList<String>): List<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()
}
if (!changeSelf && isSelf(e164, pni, aci)) {
breadCrumbs += "ChangeSelfPreventsPniSidAciSidMerge"
return emptyList()
}
// We have found records for both the PNI and ACI, and they're different
breadCrumbs += "PniSidAciSidMerge"
val operations: MutableList<PnpOperation> = mutableListOf()
// The PNI record only has a single identifier. We know we must merge.
if (data.pniSidRecord.sidOnly(pni)) {
breadCrumbs += "PniOnly"
if (data.aciSidRecord.pni != null) {
operations += PnpOperation.RemovePni(data.byAciSid)
}
@@ -2554,6 +2680,7 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
} else if (data.pniSidRecord.e164 == e164) {
// The PNI record also has the E164 on it. We're going to be stealing both fields,
// so this is basically a merge with a little bit of extra prep.
breadCrumbs += "PniSidRecordHasMatchingE164"
if (data.aciSidRecord.pni != null) {
operations += PnpOperation.RemovePni(data.byAciSid)
@@ -2576,33 +2703,55 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
)
}
} else {
Preconditions.checkState(data.pniSidRecord.e164 != null && data.pniSidRecord.e164 != e164)
check(data.pniSidRecord.e164 != null && data.pniSidRecord.e164 != e164)
breadCrumbs += "PniSidRecordHasNonMatchingE164"
operations += PnpOperation.RemovePni(data.byPniSid)
operations += PnpOperation.Update(
recipientId = data.byAciSid,
e164 = e164,
pni = pni,
aci = ACI.from(data.aciSidRecord.serviceId)
)
if (data.aciSidRecord.pni != pni) {
operations += PnpOperation.SetPni(
recipientId = data.byAciSid,
pni = pni
)
}
if (e164 != null && data.aciSidRecord.e164 != e164) {
operations += PnpOperation.SetE164(
recipientId = data.byAciSid,
e164 = e164
)
if (data.aciSidRecord.e164 != null) {
operations += PnpOperation.ChangeNumberInsert(
recipientId = data.byAciSid,
oldE164 = data.aciSidRecord.e164,
newE164 = e164
)
}
}
}
return operations
}
private fun processPossibleE164AciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet): List<PnpOperation> {
private fun processPossibleE164AciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet, changeSelf: Boolean, breadCrumbs: MutableList<String>): List<PnpOperation> {
if (e164 == null || aci == null || data.byE164 == null || data.byAciSid == null || data.e164Record == null || data.aciSidRecord == null || data.e164Record.id == data.aciSidRecord.id) {
return emptyList()
}
if (!changeSelf && isSelf(e164, pni, aci)) {
breadCrumbs += "ChangeSelfPreventsE164AciSidMerge"
return emptyList()
}
// We have found records for both the E164 and ACI, and they're different
breadCrumbs += "E164AciSidMerge"
val operations: MutableList<PnpOperation> = mutableListOf()
// The PNI record only has a single identifier. We know we must merge.
// The E164 record only has a single identifier. We know we must merge.
if (data.e164Record.e164Only()) {
// TODO high trust
breadCrumbs += "E164Only"
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
operations += PnpOperation.RemoveE164(data.byAciSid)
@@ -2623,6 +2772,8 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
} else if (data.e164Record.pni != null && data.e164Record.pni == pni) {
// The E164 record also has the PNI on it. We're going to be stealing both fields,
// so this is basically a merge with a little bit of extra prep.
breadCrumbs += "E164RecordHasMatchingPni"
if (data.aciSidRecord.pni != null) {
operations += PnpOperation.RemovePni(data.byAciSid)
}
@@ -2644,6 +2795,9 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
)
}
} else {
check(data.e164Record.pni == null || data.e164Record.pni != pni)
breadCrumbs += "E164RecordHasNonMatchingPni"
operations += PnpOperation.RemoveE164(data.byE164)
operations += PnpOperation.SetE164(
@@ -3332,32 +3486,34 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164))
RemappedRecords.getInstance().addRecipient(byE164, byAci)
val uuidValues = ContentValues().apply {
put(PHONE, e164Record.e164)
put(BLOCKED, e164Record.isBlocked || aciRecord.isBlocked)
put(MESSAGE_RINGTONE, Optional.ofNullable(aciRecord.messageRingtone).or(Optional.ofNullable(e164Record.messageRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null))
put(MESSAGE_VIBRATE, if (aciRecord.messageVibrateState != VibrateState.DEFAULT) aciRecord.messageVibrateState.id else e164Record.messageVibrateState.id)
put(CALL_RINGTONE, Optional.ofNullable(aciRecord.callRingtone).or(Optional.ofNullable(e164Record.callRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null))
put(CALL_VIBRATE, if (aciRecord.callVibrateState != VibrateState.DEFAULT) aciRecord.callVibrateState.id else e164Record.callVibrateState.id)
put(NOTIFICATION_CHANNEL, aciRecord.notificationChannel ?: e164Record.notificationChannel)
put(MUTE_UNTIL, if (aciRecord.muteUntil > 0) aciRecord.muteUntil else e164Record.muteUntil)
put(CHAT_COLORS, Optional.ofNullable(aciRecord.chatColors).or(Optional.ofNullable(e164Record.chatColors)).map { colors: ChatColors? -> colors!!.serialize().toByteArray() }.orElse(null))
put(AVATAR_COLOR, aciRecord.avatarColor.serialize())
put(CUSTOM_CHAT_COLORS_ID, Optional.ofNullable(aciRecord.chatColors).or(Optional.ofNullable(e164Record.chatColors)).map { colors: ChatColors? -> colors!!.id.longValue }.orElse(null))
put(SEEN_INVITE_REMINDER, e164Record.insightsBannerTier.id)
put(DEFAULT_SUBSCRIPTION_ID, e164Record.getDefaultSubscriptionId().orElse(-1))
put(MESSAGE_EXPIRATION_TIME, if (aciRecord.expireMessages > 0) aciRecord.expireMessages else e164Record.expireMessages)
put(REGISTERED, RegisteredState.REGISTERED.id)
put(SYSTEM_GIVEN_NAME, e164Record.systemProfileName.givenName)
put(SYSTEM_FAMILY_NAME, e164Record.systemProfileName.familyName)
put(SYSTEM_JOINED_NAME, e164Record.systemProfileName.toString())
put(SYSTEM_PHOTO_URI, e164Record.systemContactPhotoUri)
put(SYSTEM_PHONE_LABEL, e164Record.systemPhoneLabel)
put(SYSTEM_CONTACT_URI, e164Record.systemContactUri)
put(PROFILE_SHARING, aciRecord.profileSharing || e164Record.profileSharing)
put(CAPABILITIES, max(aciRecord.rawCapabilities, e164Record.rawCapabilities))
put(MENTION_SETTING, if (aciRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) aciRecord.mentionSetting.id else e164Record.mentionSetting.id)
}
// TODO [pnp] We should pass in the PNI involved in the merge and prefer that over either of the ones in the records
val uuidValues = contentValuesOf(
PHONE to e164Record.e164,
PNI_COLUMN to (e164Record.pni ?: aciRecord.pni)?.toString(),
BLOCKED to (e164Record.isBlocked || aciRecord.isBlocked),
MESSAGE_RINGTONE to Optional.ofNullable(aciRecord.messageRingtone).or(Optional.ofNullable(e164Record.messageRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null),
MESSAGE_VIBRATE to if (aciRecord.messageVibrateState != VibrateState.DEFAULT) aciRecord.messageVibrateState.id else e164Record.messageVibrateState.id,
CALL_RINGTONE to Optional.ofNullable(aciRecord.callRingtone).or(Optional.ofNullable(e164Record.callRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null),
CALL_VIBRATE to if (aciRecord.callVibrateState != VibrateState.DEFAULT) aciRecord.callVibrateState.id else e164Record.callVibrateState.id,
NOTIFICATION_CHANNEL to (aciRecord.notificationChannel ?: e164Record.notificationChannel),
MUTE_UNTIL to if (aciRecord.muteUntil > 0) aciRecord.muteUntil else e164Record.muteUntil,
CHAT_COLORS to Optional.ofNullable(aciRecord.chatColors).or(Optional.ofNullable(e164Record.chatColors)).map { colors: ChatColors? -> colors!!.serialize().toByteArray() }.orElse(null),
AVATAR_COLOR to aciRecord.avatarColor.serialize(),
CUSTOM_CHAT_COLORS_ID to Optional.ofNullable(aciRecord.chatColors).or(Optional.ofNullable(e164Record.chatColors)).map { colors: ChatColors? -> colors!!.id.longValue }.orElse(null),
SEEN_INVITE_REMINDER to e164Record.insightsBannerTier.id,
DEFAULT_SUBSCRIPTION_ID to e164Record.getDefaultSubscriptionId().orElse(-1),
MESSAGE_EXPIRATION_TIME to if (aciRecord.expireMessages > 0) aciRecord.expireMessages else e164Record.expireMessages,
REGISTERED to RegisteredState.REGISTERED.id,
SYSTEM_GIVEN_NAME to e164Record.systemProfileName.givenName,
SYSTEM_FAMILY_NAME to e164Record.systemProfileName.familyName,
SYSTEM_JOINED_NAME to e164Record.systemProfileName.toString(),
SYSTEM_PHOTO_URI to e164Record.systemContactPhotoUri,
SYSTEM_PHONE_LABEL to e164Record.systemPhoneLabel,
SYSTEM_CONTACT_URI to e164Record.systemContactUri,
PROFILE_SHARING to (aciRecord.profileSharing || e164Record.profileSharing),
CAPABILITIES to max(aciRecord.rawCapabilities, e164Record.rawCapabilities),
MENTION_SETTING to if (aciRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) aciRecord.mentionSetting.id else e164Record.mentionSetting.id
)
if (aciRecord.profileKey != null) {
updateProfileValuesForMerge(uuidValues, aciRecord)
@@ -4098,4 +4254,13 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
}
}
}
data class ProcessPnpTupleResult(
val finalId: RecipientId,
val affectedIds: Set<RecipientId>,
val oldIds: Set<RecipientId>,
val changedNumberId: RecipientId?,
val operations: List<PnpOperation>,
val breadCrumbs: List<String>,
)
}

View File

@@ -201,7 +201,7 @@ public final class LiveRecipient {
details = RecipientDetails.forIndividual(context, record);
}
Recipient recipient = new Recipient(id, details, true);
Recipient recipient = new Recipient(record.getId(), details, true);
RecipientIdCache.INSTANCE.put(recipient);
return recipient;
}

View File

@@ -100,6 +100,7 @@ public final class FeatureFlags {
private static final String TELECOM_MANUFACTURER_ALLOWLIST = "android.calling.telecomAllowList";
private static final String TELECOM_MODEL_BLOCKLIST = "android.calling.telecomModelBlockList";
private static final String CAMERAX_MODEL_BLOCKLIST = "android.cameraXModelBlockList";
private static final String RECIPIENT_MERGE_V2 = "android.recipientMergeV2";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -152,7 +153,8 @@ public final class FeatureFlags {
GIFT_BADGE_SEND_SUPPORT,
TELECOM_MANUFACTURER_ALLOWLIST,
TELECOM_MODEL_BLOCKLIST,
CAMERAX_MODEL_BLOCKLIST
CAMERAX_MODEL_BLOCKLIST,
RECIPIENT_MERGE_V2
);
@VisibleForTesting
@@ -214,7 +216,8 @@ public final class FeatureFlags {
USE_FCM_FOREGROUND_SERVICE,
TELECOM_MANUFACTURER_ALLOWLIST,
TELECOM_MODEL_BLOCKLIST,
CAMERAX_MODEL_BLOCKLIST
CAMERAX_MODEL_BLOCKLIST,
RECIPIENT_MERGE_V2
);
/**
@@ -535,6 +538,13 @@ public final class FeatureFlags {
return giftBadgeReceiveSupport() && getBoolean(GIFT_BADGE_SEND_SUPPORT, Environment.IS_STAGING);
}
/**
* Whether or not we should use the new recipient merging strategy.
*/
public static boolean recipientMergeV2() {
return getBoolean(RECIPIENT_MERGE_V2, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);