Improve implementation and testing on PNP contact merging.

This commit is contained in:
Greyson Parrelli
2022-06-29 15:29:58 -04:00
committed by Cody Henthorne
parent c64be82710
commit 61ce39b5b6
8 changed files with 1455 additions and 74 deletions

View File

@@ -0,0 +1,215 @@
package org.thoughtcrime.securesms.database
import app.cash.exhaustive.Exhaustive
import org.thoughtcrime.securesms.database.model.RecipientRecord
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
/**
* Encapsulates data around processing a tuple of user data into a user entry in [RecipientDatabase].
* Also lets you apply a list of [PnpOperation]s to get what the resulting dataset would be.
*/
data class PnpDataSet(
val e164: String?,
val pni: PNI?,
val aci: ACI?,
val byE164: RecipientId?,
val byPniSid: RecipientId?,
val byPniOnly: RecipientId?,
val byAciSid: RecipientId?,
val e164Record: RecipientRecord? = null,
val pniSidRecord: RecipientRecord? = null,
val aciSidRecord: RecipientRecord? = null
) {
/**
* @return The common id if all non-null ids are equal, or null if all are null or at least one non-null pair doesn't match.
*/
val commonId: RecipientId? = findCommonId(listOf(byE164, byPniSid, byPniOnly, byAciSid))
fun MutableSet<RecipientRecord>.replace(recipientId: RecipientId, update: (RecipientRecord) -> RecipientRecord) {
val toUpdate = this.first { it.id == recipientId }
this -= toUpdate
this += update(toUpdate)
}
/**
* 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 {
if (operations.isEmpty()) {
return this
}
val records: MutableSet<RecipientRecord> = listOfNotNull(e164Record, pniSidRecord, aciSidRecord).toMutableSet()
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) }
}
is PnpOperation.RemovePni -> {
records.replace(operation.recipientId) { record ->
record.copy(
pni = null,
serviceId = if (record.sidIsPni()) {
null
} else {
record.serviceId
}
)
}
}
is PnpOperation.SetAci -> {
records.replace(operation.recipientId) { it.copy(serviceId = operation.aci) }
}
is PnpOperation.SetE164 -> {
records.replace(operation.recipientId) { it.copy(e164 = operation.e164) }
}
is PnpOperation.SetPni -> {
records.replace(operation.recipientId) { record ->
record.copy(
pni = operation.pni,
serviceId = if (record.sidIsPni()) {
operation.pni
} else {
record.serviceId ?: operation.pni
}
)
}
}
is PnpOperation.Merge -> {
val primary: RecipientRecord = records.first { it.id == operation.primaryId }
val secondary: RecipientRecord = records.first { it.id == operation.secondaryId }
records.replace(primary.id) { _ ->
primary.copy(
e164 = primary.e164 ?: secondary.e164,
pni = primary.pni ?: secondary.pni,
serviceId = primary.serviceId ?: secondary.serviceId
)
}
records -= secondary
}
is PnpOperation.SessionSwitchoverInsert -> Unit
is PnpOperation.ChangeNumberInsert -> Unit
}
}
val newE164Record = if (e164 != null) records.firstOrNull { it.e164 == e164 } else null
val newPniSidRecord = if (pni != null) records.firstOrNull { it.serviceId == pni } else null
val newAciSidRecord = if (aci != null) records.firstOrNull { it.serviceId == aci } else null
return PnpDataSet(
e164 = e164,
pni = pni,
aci = aci,
byE164 = newE164Record?.id,
byPniSid = newPniSidRecord?.id,
byPniOnly = byPniOnly,
byAciSid = newAciSidRecord?.id,
e164Record = newE164Record,
pniSidRecord = newPniSidRecord,
aciSidRecord = newAciSidRecord
)
}
companion object {
private fun findCommonId(ids: List<RecipientId?>): RecipientId? {
val nonNull = ids.filterNotNull()
return when {
nonNull.isEmpty() -> null
nonNull.all { it == nonNull[0] } -> nonNull[0]
else -> null
}
}
}
}
/**
* Represents a set of actions that need to be applied to incorporate a tuple of user data
* into [RecipientDatabase].
*/
data class PnpChangeSet(
val id: PnpIdResolver,
val operations: List<PnpOperation> = emptyList()
)
sealed class PnpIdResolver {
data class PnpNoopId(
val recipientId: RecipientId
) : PnpIdResolver()
data class PnpInsert(
val e164: String?,
val pni: PNI?,
val aci: ACI?
) : PnpIdResolver()
}
/**
* An operation that needs to be performed on the [RecipientDatabase] as part of merging in new user data.
* 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()
data class RemoveE164(
val recipientId: RecipientId
) : PnpOperation()
data class RemovePni(
val recipientId: RecipientId
) : PnpOperation()
data class SetE164(
val recipientId: RecipientId,
val e164: String
) : PnpOperation()
data class SetPni(
val recipientId: RecipientId,
val pni: PNI
) : PnpOperation()
data class SetAci(
val recipientId: RecipientId,
val aci: ACI
) : PnpOperation()
/**
* Merge two rows into one. Prefer data in the primary row when there's conflicts. Delete the secondary row afterwards.
*/
data class Merge(
val primaryId: RecipientId,
val secondaryId: RecipientId
) : PnpOperation()
data class SessionSwitchoverInsert(
val recipientId: RecipientId
) : PnpOperation()
data class ChangeNumberInsert(
val recipientId: RecipientId,
val oldE164: String,
val newE164: String
) : PnpOperation()
}

View File

@@ -7,6 +7,7 @@ import android.net.Uri
import android.text.TextUtils
import androidx.annotation.VisibleForTesting
import androidx.core.content.contentValuesOf
import app.cash.exhaustive.Exhaustive
import com.google.protobuf.ByteString
import com.google.protobuf.InvalidProtocolBufferException
import net.zetetic.database.sqlcipher.SQLiteConstraintException
@@ -54,7 +55,6 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.notificationProfiles
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.reactions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.runPostSuccessfulTransaction
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.storySends
@@ -105,6 +105,7 @@ 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
@@ -2179,43 +2180,388 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
@VisibleForTesting
fun processCdsV2Result(e164: String, pni: PNI, aci: ACI?): RecipientId {
val byE164: RecipientId? = getByE164(e164).orElse(null)
val byPni: RecipientId? = getByServiceId(pni).orElse(null)
val byPniOnly: RecipientId? = getByPni(pni).orElse(null)
val byAci: RecipientId? = aci?.let { getByServiceId(it).orElse(null) }
val result = processPnpTupleToChangeSet(e164, pni, aci, pniVerified = false)
val commonId: RecipientId? = listOf(byE164, byPni, byPniOnly, byAci).commonId()
val allRequiredDbFields: List<RecipientId?> = if (aci != null) listOf(byE164, byAci, byPniOnly) else listOf(byE164, byPni, byPniOnly)
val allRequiredDbFieldPopulated: Boolean = allRequiredDbFields.all { it != null }
// All ID's agree and the database is up-to-date
if (commonId != null && allRequiredDbFieldPopulated) {
return commonId
val id: RecipientId = when (result.id) {
is PnpIdResolver.PnpNoopId -> {
result.id.recipientId
}
is PnpIdResolver.PnpInsert -> {
val id: Long = writableDatabase.insert(TABLE_NAME, null, buildContentValuesForCdsInsert(result.id.e164, result.id.pni, result.id.aci))
RecipientId.from(id)
}
}
// All ID's agree but we need to update the database
if (commonId != null && !allRequiredDbFieldPopulated) {
writableDatabase
.update(TABLE_NAME)
.values(
PHONE to e164,
SERVICE_ID to (aci ?: pni).toString(),
PNI_COLUMN to pni.toString(),
REGISTERED to RegisteredState.REGISTERED.id,
STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey())
for (operation in result.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.Merge -> {
// TODO [pnp]
error("Not yet implemented")
}
is PnpOperation.SessionSwitchoverInsert -> {
// TODO [pnp]
error("Not yet implemented")
}
is PnpOperation.ChangeNumberInsert -> {
// TODO [pnp]
error("Not yet implemented")
}
is PnpOperation.RemoveE164 -> {
// TODO [pnp]
error("Not yet implemented")
}
is PnpOperation.RemovePni -> {
// TODO [pnp]
error("Not yet implemented")
}
is PnpOperation.SetAci -> {
// TODO [pnp]
error("Not yet implemented")
}
is PnpOperation.SetE164 -> {
// TODO [pnp]
error("Not yet implemented")
}
is PnpOperation.SetPni -> {
// TODO [pnp]
error("Not yet implemented")
}
}
}
return id
}
/**
* Takes a tuple of (e164, pni, aci) and converts that into a list of changes that would need to be made to
* merge that data into our database.
*
* The database will be read, but not written to, during this function.
* It is assumed that we are in a transaction.
*/
@VisibleForTesting
fun processPnpTupleToChangeSet(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean): 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!")
val partialData = PnpDataSet(
e164 = e164,
pni = pni,
aci = aci,
byE164 = e164?.let { getByE164(it).orElse(null) },
byPniSid = pni?.let { getByServiceId(it).orElse(null) },
byPniOnly = pni?.let { getByPni(it).orElse(null) },
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 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))
}
// 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()
if (e164 != null && record.e164 != e164) {
operations += PnpOperation.SetE164(
recipientId = partialData.commonId,
e164 = e164
)
.where("$ID = ?", commonId)
.run()
return commonId
}
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 (e164 != null && record.e164 != null && record.e164 != e164) {
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
)
}
// Nothing matches
if (byE164 == null && byPni == null && byAci == null) {
val id: Long = writableDatabase.insert(TABLE_NAME, null, buildContentValuesForCdsInsert(e164, pni, aci))
return RecipientId.from(id)
if (partialData.byE164 == null && partialData.byPniSid == null && partialData.byAciSid == null) {
return PnpChangeSet(
id = PnpIdResolver.PnpInsert(
e164 = e164,
pni = pni,
aci = aci
)
)
}
throw NotImplementedError("Handle cases where IDs map to different individuals")
// TODO pni only record?
// At this point, we know that records have been found for at least two of the fields,
// and that there are at least two unique IDs among the records.
//
// In other words, *some* sort of merging of data must now occur.
// 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.
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)
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))
val finalData: PnpDataSet = fullData.perform(operations)
val primaryId: RecipientId = listOfNotNull(finalData.byAciSid, finalData.byE164, finalData.byPniSid).first()
if (finalData.byAciSid == null && aci != null) {
operations += PnpOperation.SetAci(
recipientId = primaryId,
aci = aci
)
}
if (finalData.byE164 == null && e164 != null) {
operations += PnpOperation.SetE164(
recipientId = primaryId,
e164 = e164
)
}
if (finalData.byPniSid == null && finalData.byPniOnly == null && pni != null) {
operations += PnpOperation.SetPni(
recipientId = primaryId,
pni = pni
)
}
return PnpChangeSet(
id = PnpIdResolver.PnpNoopId(primaryId),
operations = operations
)
}
private fun processPossibleE164PniSidMerge(pni: PNI?, pniVerified: Boolean, data: PnpDataSet): 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
val operations: MutableList<PnpOperation> = mutableListOf()
// The PNI record only has a single identifier. We know we must merge.
if (data.pniSidRecord.sidOnly(pni)) {
if (data.e164Record.pni != null) {
operations += PnpOperation.RemovePni(data.byE164)
}
operations += PnpOperation.Merge(
primaryId = data.byE164,
secondaryId = data.byPniSid
)
// TODO: Possible session switchover?
} else {
Preconditions.checkState(!data.pniSidRecord.pniAndAci())
Preconditions.checkState(data.pniSidRecord.e164 != null)
operations += PnpOperation.RemovePni(data.byPniSid)
operations += PnpOperation.SetPni(
recipientId = data.byE164,
pni = pni
)
if (!pniVerified && sessions.hasAnySessionFor(data.pniSidRecord.serviceId.toString())) {
operations += PnpOperation.SessionSwitchoverInsert(data.byPniSid)
}
if (!pniVerified && data.e164Record.serviceId != null && data.e164Record.sidIsPni() && sessions.hasAnySessionFor(data.e164Record.serviceId.toString())) {
operations += PnpOperation.SessionSwitchoverInsert(data.byE164)
}
}
return operations
}
private fun processPossiblePniSidAciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet): 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()
}
// We have found records for both the PNI and ACI, and they're different
val operations: MutableList<PnpOperation> = mutableListOf()
// The PNI record only has a single identifier. We know we must merge.
if (data.pniSidRecord.sidOnly(pni)) {
if (data.aciSidRecord.pni != null) {
operations += PnpOperation.RemovePni(data.byAciSid)
}
operations += PnpOperation.Merge(
primaryId = data.byAciSid,
secondaryId = data.byPniSid
)
} 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.
if (data.aciSidRecord.pni != null) {
operations += PnpOperation.RemovePni(data.byAciSid)
}
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
operations += PnpOperation.RemoveE164(data.byAciSid)
}
operations += PnpOperation.Merge(
primaryId = data.byAciSid,
secondaryId = data.byPniSid
)
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
operations += PnpOperation.ChangeNumberInsert(
recipientId = data.byAciSid,
oldE164 = data.aciSidRecord.e164,
newE164 = e164!!
)
}
} else {
Preconditions.checkState(data.pniSidRecord.e164 != null && data.pniSidRecord.e164 != e164)
operations += PnpOperation.RemovePni(data.byPniSid)
operations += PnpOperation.Update(
recipientId = data.byAciSid,
e164 = e164,
pni = pni,
aci = ACI.from(data.aciSidRecord.serviceId)
)
}
return operations
}
private fun processPossibleE164AciSidMerge(e164: String?, pni: PNI?, aci: ACI?, data: PnpDataSet): 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()
}
// We have found records for both the E164 and ACI, and they're different
val operations: MutableList<PnpOperation> = mutableListOf()
// The PNI record only has a single identifier. We know we must merge.
if (data.e164Record.e164Only()) {
// TODO high trust
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
operations += PnpOperation.RemoveE164(data.byAciSid)
}
operations += PnpOperation.Merge(
primaryId = data.byAciSid,
secondaryId = data.byE164
)
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
operations += PnpOperation.ChangeNumberInsert(
recipientId = data.byAciSid,
oldE164 = data.aciSidRecord.e164,
newE164 = e164
)
}
} 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.
if (data.aciSidRecord.pni != null) {
operations += PnpOperation.RemovePni(data.byAciSid)
}
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
operations += PnpOperation.RemoveE164(data.byAciSid)
}
operations += PnpOperation.Merge(
primaryId = data.byAciSid,
secondaryId = data.byE164
)
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
operations += PnpOperation.ChangeNumberInsert(
recipientId = data.byAciSid,
oldE164 = data.aciSidRecord.e164,
newE164 = e164!!
)
}
} else {
operations += PnpOperation.RemoveE164(data.byE164)
operations += PnpOperation.SetE164(
recipientId = data.byAciSid,
e164 = e164
)
if (data.aciSidRecord.e164 != null && data.aciSidRecord.e164 != e164) {
operations += PnpOperation.ChangeNumberInsert(
recipientId = data.byAciSid,
oldE164 = data.aciSidRecord.e164,
newE164 = e164
)
}
}
return operations
}
fun getUninvitedRecipientsForInsights(): List<RecipientId> {
@@ -2939,16 +3285,18 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
return values
}
private fun buildContentValuesForCdsInsert(e164: String, pni: PNI, aci: ACI?): ContentValues {
val serviceId: ServiceId = aci ?: pni
return ContentValues().apply {
put(PHONE, e164)
put(SERVICE_ID, serviceId.toString())
put(PNI_COLUMN, pni.toString())
put(REGISTERED, RegisteredState.REGISTERED.id)
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
put(AVATAR_COLOR, AvatarColor.random().serialize())
}
private fun buildContentValuesForCdsInsert(e164: String?, pni: PNI?, aci: ACI?): ContentValues {
Preconditions.checkArgument(pni != null || aci != null, "Must provide a serviceId!")
val serviceId: ServiceId = aci ?: pni!!
return contentValuesOf(
PHONE to e164,
SERVICE_ID to serviceId.toString(),
PNI_COLUMN to pni.toString(),
REGISTERED to RegisteredState.REGISTERED.id,
STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()),
AVATAR_COLOR to AvatarColor.random().serialize()
)
}
private fun getValuesForStorageContact(contact: SignalContactRecord, isInsert: Boolean): ContentValues {
@@ -3026,22 +3374,6 @@ open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) :
}
}
/**
* @return The common id if all non-null ids are equal, or null if all are null or at least one non-null pair doesn't match.
*/
private fun Collection<RecipientId?>.commonId(): RecipientId? {
val nonNull = this.filterNotNull()
if (nonNull.isEmpty()) {
return null
}
return if (nonNull.all { it.equals(nonNull[0]) }) {
nonNull[0]
} else {
null
}
}
/**
* Should only be used for debugging! A very destructive action that clears all known serviceIds.
*/

View File

@@ -7,6 +7,7 @@ import org.signal.core.util.logging.Log
import org.signal.core.util.requireInt
import org.signal.core.util.requireNonNullBlob
import org.signal.core.util.requireNonNullString
import org.signal.core.util.select
import org.signal.libsignal.protocol.SignalProtocolAddress
import org.signal.libsignal.protocol.state.SessionRecord
import org.whispersystems.signalservice.api.push.ServiceId
@@ -195,5 +196,19 @@ class SessionDatabase(context: Context, databaseHelper: SignalDatabase) : Databa
}
}
/**
* @return True if a session exists with this address for _any_ of your identities.
*/
fun hasAnySessionFor(addressName: String): Boolean {
readableDatabase
.select("1")
.from(TABLE_NAME)
.where("$ADDRESS = ?", addressName)
.run()
.use { cursor ->
return cursor.moveToFirst()
}
}
class SessionRow(val address: String, val deviceId: Int, val record: SessionRecord)
}

View File

@@ -90,6 +90,22 @@ data class RecipientRecord(
return if (defaultSubscriptionId != -1) Optional.of(defaultSubscriptionId) else Optional.empty()
}
fun e164Only(): Boolean {
return this.e164 != null && this.serviceId == null
}
fun sidOnly(sid: ServiceId): Boolean {
return this.e164 == null && this.serviceId == sid && (this.pni == null || this.pni == sid)
}
fun sidIsPni(): Boolean {
return this.serviceId != null && this.pni != null && this.serviceId == this.pni
}
fun pniAndAci(): Boolean {
return this.serviceId != null && this.pni != null && this.serviceId != this.pni
}
/**
* A bundle of data that's only necessary when syncing to storage service, not for a
* [Recipient].