Signal-Android/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.kt

4419 wiersze
160 KiB
Kotlin

package org.thoughtcrime.securesms.database
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
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
import org.signal.core.util.Bitmask
import org.signal.core.util.CursorUtil
import org.signal.core.util.SqlUtil
import org.signal.core.util.exists
import org.signal.core.util.logging.Log
import org.signal.core.util.optionalBlob
import org.signal.core.util.optionalBoolean
import org.signal.core.util.optionalInt
import org.signal.core.util.optionalLong
import org.signal.core.util.optionalString
import org.signal.core.util.or
import org.signal.core.util.readToSet
import org.signal.core.util.requireBlob
import org.signal.core.util.requireBoolean
import org.signal.core.util.requireInt
import org.signal.core.util.requireLong
import org.signal.core.util.requireNonNullString
import org.signal.core.util.requireString
import org.signal.core.util.select
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.libsignal.protocol.IdentityKey
import org.signal.libsignal.protocol.InvalidKeyException
import org.signal.libsignal.zkgroup.InvalidInputException
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
import org.signal.libsignal.zkgroup.profiles.ProfileKey
import org.signal.storageservice.protos.groups.local.DecryptedGroup
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.Badges.toDatabaseBadge
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.color.MaterialColor
import org.thoughtcrime.securesms.color.MaterialColor.UnknownColorException
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColors.Companion.forChatColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors.Id.Companion.forLongValue
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper.getChatColors
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.GroupDatabase.LegacyGroupInsertException
import org.thoughtcrime.securesms.database.GroupDatabase.MissedGroupMigrationInsertException
import org.thoughtcrime.securesms.database.GroupDatabase.ShowAsStoryState
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.groups
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.identities
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.runPostSuccessfulTransaction
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.sessions
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.threads
import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.RecipientRecord
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.BadgeList
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.ThreadMergeEvent
import org.thoughtcrime.securesms.database.model.databaseprotos.Wallpaper
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.groups.BadGroupIdException
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.groups.GroupId.V1
import org.thoughtcrime.securesms.groups.GroupId.V2
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor
import org.thoughtcrime.securesms.jobs.RecipientChangedNumberJob
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageRecordUpdate
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.storage.StorageSyncModels
import org.thoughtcrime.securesms.stories.Stories.isFeatureFlagEnabled
import org.thoughtcrime.securesms.util.Base64
import org.thoughtcrime.securesms.util.FeatureFlags
import org.thoughtcrime.securesms.util.GroupUtil
import org.thoughtcrime.securesms.util.IdentityUtil
import org.thoughtcrime.securesms.util.ProfileUtil
import org.thoughtcrime.securesms.util.Util
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperFactory
import org.thoughtcrime.securesms.wallpaper.WallpaperStorage
import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
import org.whispersystems.signalservice.api.push.ACI
import org.whispersystems.signalservice.api.push.PNI
import org.whispersystems.signalservice.api.push.ServiceId
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import org.whispersystems.signalservice.api.storage.SignalAccountRecord
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.internal.storage.protos.GroupV2Record
import java.io.Closeable
import java.io.IOException
import java.util.Arrays
import java.util.Collections
import java.util.LinkedList
import java.util.Objects
import java.util.Optional
import java.util.concurrent.TimeUnit
import kotlin.math.max
open class RecipientDatabase(context: Context, databaseHelper: SignalDatabase) : Database(context, databaseHelper) {
companion object {
private val TAG = Log.tag(RecipientDatabase::class.java)
private val UNREGISTERED_LIFESPAN: Long = TimeUnit.DAYS.toMillis(30)
const val TABLE_NAME = "recipient"
const val ID = "_id"
const val SERVICE_ID = "uuid"
const val PNI_COLUMN = "pni"
private const val USERNAME = "username"
const val PHONE = "phone"
const val EMAIL = "email"
const val GROUP_ID = "group_id"
const val DISTRIBUTION_LIST_ID = "distribution_list_id"
const val GROUP_TYPE = "group_type"
const val BLOCKED = "blocked"
private const val MESSAGE_RINGTONE = "message_ringtone"
private const val MESSAGE_VIBRATE = "message_vibrate"
private const val CALL_RINGTONE = "call_ringtone"
private const val CALL_VIBRATE = "call_vibrate"
private const val NOTIFICATION_CHANNEL = "notification_channel"
private const val MUTE_UNTIL = "mute_until"
private const val AVATAR_COLOR = "color"
private const val SEEN_INVITE_REMINDER = "seen_invite_reminder"
private const val DEFAULT_SUBSCRIPTION_ID = "default_subscription_id"
private const val MESSAGE_EXPIRATION_TIME = "message_expiration_time"
const val REGISTERED = "registered"
const val SYSTEM_JOINED_NAME = "system_display_name"
const val SYSTEM_FAMILY_NAME = "system_family_name"
const val SYSTEM_GIVEN_NAME = "system_given_name"
private const val SYSTEM_PHOTO_URI = "system_photo_uri"
const val SYSTEM_PHONE_TYPE = "system_phone_type"
const val SYSTEM_PHONE_LABEL = "system_phone_label"
private const val SYSTEM_CONTACT_URI = "system_contact_uri"
private const val SYSTEM_INFO_PENDING = "system_info_pending"
private const val PROFILE_KEY = "profile_key"
const val EXPIRING_PROFILE_KEY_CREDENTIAL = "profile_key_credential"
private const val SIGNAL_PROFILE_AVATAR = "signal_profile_avatar"
const val PROFILE_SHARING = "profile_sharing"
private const val LAST_PROFILE_FETCH = "last_profile_fetch"
private const val UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode"
const val FORCE_SMS_SELECTION = "force_sms_selection"
private const val CAPABILITIES = "capabilities"
const val STORAGE_SERVICE_ID = "storage_service_key"
private const val PROFILE_GIVEN_NAME = "signal_profile_name"
private const val PROFILE_FAMILY_NAME = "profile_family_name"
private const val PROFILE_JOINED_NAME = "profile_joined_name"
private const val MENTION_SETTING = "mention_setting"
private const val STORAGE_PROTO = "storage_proto"
private const val LAST_SESSION_RESET = "last_session_reset"
private const val WALLPAPER = "wallpaper"
private const val WALLPAPER_URI = "wallpaper_file"
const val ABOUT = "about"
const val ABOUT_EMOJI = "about_emoji"
private const val EXTRAS = "extras"
private const val GROUPS_IN_COMMON = "groups_in_common"
private const val CHAT_COLORS = "chat_colors"
private const val CUSTOM_CHAT_COLORS_ID = "custom_chat_colors_id"
private const val BADGES = "badges"
const val SEARCH_PROFILE_NAME = "search_signal_profile"
private const val SORT_NAME = "sort_name"
private const val IDENTITY_STATUS = "identity_status"
private const val IDENTITY_KEY = "identity_key"
private const val NEEDS_PNI_SIGNATURE = "needs_pni_signature"
private const val UNREGISTERED_TIMESTAMP = "unregistered_timestamp"
private const val HIDDEN = "hidden"
@JvmField
val CREATE_TABLE =
"""
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY AUTOINCREMENT,
$SERVICE_ID TEXT UNIQUE DEFAULT NULL,
$USERNAME TEXT UNIQUE DEFAULT NULL,
$PHONE TEXT UNIQUE DEFAULT NULL,
$EMAIL TEXT UNIQUE DEFAULT NULL,
$GROUP_ID TEXT UNIQUE DEFAULT NULL,
$GROUP_TYPE INTEGER DEFAULT ${GroupType.NONE.id},
$BLOCKED INTEGER DEFAULT 0,
$MESSAGE_RINGTONE TEXT DEFAULT NULL,
$MESSAGE_VIBRATE INTEGER DEFAULT ${VibrateState.DEFAULT.id},
$CALL_RINGTONE TEXT DEFAULT NULL,
$CALL_VIBRATE INTEGER DEFAULT ${VibrateState.DEFAULT.id},
$NOTIFICATION_CHANNEL TEXT DEFAULT NULL,
$MUTE_UNTIL INTEGER DEFAULT 0,
$AVATAR_COLOR TEXT DEFAULT NULL,
$SEEN_INVITE_REMINDER INTEGER DEFAULT ${InsightsBannerTier.NO_TIER.id},
$DEFAULT_SUBSCRIPTION_ID INTEGER DEFAULT -1,
$MESSAGE_EXPIRATION_TIME INTEGER DEFAULT 0,
$REGISTERED INTEGER DEFAULT ${RegisteredState.UNKNOWN.id},
$SYSTEM_GIVEN_NAME TEXT DEFAULT NULL,
$SYSTEM_FAMILY_NAME TEXT DEFAULT NULL,
$SYSTEM_JOINED_NAME TEXT DEFAULT NULL,
$SYSTEM_PHOTO_URI TEXT DEFAULT NULL,
$SYSTEM_PHONE_LABEL TEXT DEFAULT NULL,
$SYSTEM_PHONE_TYPE INTEGER DEFAULT -1,
$SYSTEM_CONTACT_URI TEXT DEFAULT NULL,
$SYSTEM_INFO_PENDING INTEGER DEFAULT 0,
$PROFILE_KEY TEXT DEFAULT NULL,
$EXPIRING_PROFILE_KEY_CREDENTIAL TEXT DEFAULT NULL,
$PROFILE_GIVEN_NAME TEXT DEFAULT NULL,
$PROFILE_FAMILY_NAME TEXT DEFAULT NULL,
$PROFILE_JOINED_NAME TEXT DEFAULT NULL,
$SIGNAL_PROFILE_AVATAR TEXT DEFAULT NULL,
$PROFILE_SHARING INTEGER DEFAULT 0,
$LAST_PROFILE_FETCH INTEGER DEFAULT 0,
$UNIDENTIFIED_ACCESS_MODE INTEGER DEFAULT 0,
$FORCE_SMS_SELECTION INTEGER DEFAULT 0,
$STORAGE_SERVICE_ID TEXT UNIQUE DEFAULT NULL,
$MENTION_SETTING INTEGER DEFAULT ${MentionSetting.ALWAYS_NOTIFY.id},
$STORAGE_PROTO TEXT DEFAULT NULL,
$CAPABILITIES INTEGER DEFAULT 0,
$LAST_SESSION_RESET BLOB DEFAULT NULL,
$WALLPAPER BLOB DEFAULT NULL,
$WALLPAPER_URI TEXT DEFAULT NULL,
$ABOUT TEXT DEFAULT NULL,
$ABOUT_EMOJI TEXT DEFAULT NULL,
$EXTRAS BLOB DEFAULT NULL,
$GROUPS_IN_COMMON INTEGER DEFAULT 0,
$CHAT_COLORS BLOB DEFAULT NULL,
$CUSTOM_CHAT_COLORS_ID INTEGER DEFAULT 0,
$BADGES BLOB DEFAULT NULL,
$PNI_COLUMN TEXT DEFAULT NULL,
$DISTRIBUTION_LIST_ID INTEGER DEFAULT NULL,
$NEEDS_PNI_SIGNATURE INTEGER DEFAULT 0,
$UNREGISTERED_TIMESTAMP INTEGER DEFAULT 0,
$HIDDEN INTEGER DEFAULT 0
)
""".trimIndent()
val CREATE_INDEXS = arrayOf(
"CREATE INDEX IF NOT EXISTS recipient_group_type_index ON $TABLE_NAME ($GROUP_TYPE);",
"CREATE UNIQUE INDEX IF NOT EXISTS recipient_pni_index ON $TABLE_NAME ($PNI_COLUMN)",
"CREATE INDEX IF NOT EXISTS recipient_service_id_profile_key ON $TABLE_NAME ($SERVICE_ID, $PROFILE_KEY) WHERE $SERVICE_ID NOT NULL AND $PROFILE_KEY NOT NULL"
)
private val RECIPIENT_PROJECTION: Array<String> = arrayOf(
ID,
SERVICE_ID,
PNI_COLUMN,
USERNAME,
PHONE,
EMAIL,
GROUP_ID,
GROUP_TYPE,
BLOCKED,
MESSAGE_RINGTONE,
CALL_RINGTONE,
MESSAGE_VIBRATE,
CALL_VIBRATE,
MUTE_UNTIL,
AVATAR_COLOR,
SEEN_INVITE_REMINDER,
DEFAULT_SUBSCRIPTION_ID,
MESSAGE_EXPIRATION_TIME,
REGISTERED,
PROFILE_KEY,
EXPIRING_PROFILE_KEY_CREDENTIAL,
SYSTEM_JOINED_NAME,
SYSTEM_GIVEN_NAME,
SYSTEM_FAMILY_NAME,
SYSTEM_PHOTO_URI,
SYSTEM_PHONE_LABEL,
SYSTEM_PHONE_TYPE,
SYSTEM_CONTACT_URI,
PROFILE_GIVEN_NAME,
PROFILE_FAMILY_NAME,
SIGNAL_PROFILE_AVATAR,
PROFILE_SHARING,
LAST_PROFILE_FETCH,
NOTIFICATION_CHANNEL,
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION,
CAPABILITIES,
STORAGE_SERVICE_ID,
MENTION_SETTING,
WALLPAPER,
WALLPAPER_URI,
MENTION_SETTING,
ABOUT,
ABOUT_EMOJI,
EXTRAS,
GROUPS_IN_COMMON,
CHAT_COLORS,
CUSTOM_CHAT_COLORS_ID,
BADGES,
DISTRIBUTION_LIST_ID,
NEEDS_PNI_SIGNATURE,
HIDDEN
)
private val ID_PROJECTION = arrayOf(ID)
private val SEARCH_PROJECTION = arrayOf(
ID,
SYSTEM_JOINED_NAME,
PHONE,
EMAIL,
SYSTEM_PHONE_LABEL,
SYSTEM_PHONE_TYPE,
REGISTERED,
ABOUT,
ABOUT_EMOJI,
EXTRAS,
GROUPS_IN_COMMON,
"COALESCE(NULLIF($PROFILE_JOINED_NAME, ''), NULLIF($PROFILE_GIVEN_NAME, '')) AS $SEARCH_PROFILE_NAME",
"""
LOWER(
COALESCE(
NULLIF($SYSTEM_JOINED_NAME, ''),
NULLIF($SYSTEM_GIVEN_NAME, ''),
NULLIF($PROFILE_JOINED_NAME, ''),
NULLIF($PROFILE_GIVEN_NAME, ''),
NULLIF($USERNAME, '')
)
) AS $SORT_NAME
""".trimIndent()
)
@JvmField
val SEARCH_PROJECTION_NAMES = arrayOf(
ID,
SYSTEM_JOINED_NAME,
PHONE,
EMAIL,
SYSTEM_PHONE_LABEL,
SYSTEM_PHONE_TYPE,
REGISTERED,
ABOUT,
ABOUT_EMOJI,
EXTRAS,
GROUPS_IN_COMMON,
SEARCH_PROFILE_NAME,
SORT_NAME
)
private val TYPED_RECIPIENT_PROJECTION: Array<String> = RECIPIENT_PROJECTION
.map { columnName -> "$TABLE_NAME.$columnName" }
.toTypedArray()
@JvmField
val TYPED_RECIPIENT_PROJECTION_NO_ID: Array<String> = TYPED_RECIPIENT_PROJECTION.copyOfRange(1, TYPED_RECIPIENT_PROJECTION.size)
private val MENTION_SEARCH_PROJECTION = arrayOf(
ID,
"""
REPLACE(
COALESCE(
NULLIF($SYSTEM_JOINED_NAME, ''),
NULLIF($SYSTEM_GIVEN_NAME, ''),
NULLIF($PROFILE_JOINED_NAME, ''),
NULLIF($PROFILE_GIVEN_NAME, ''),
NULLIF($USERNAME, ''),
NULLIF($PHONE, '')
),
' ',
''
) AS $SORT_NAME
""".trimIndent()
)
private val INSIGHTS_INVITEE_LIST =
"""
SELECT $TABLE_NAME.$ID
FROM $TABLE_NAME INNER JOIN ${ThreadDatabase.TABLE_NAME} ON $TABLE_NAME.$ID = ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.RECIPIENT_ID}
WHERE
$TABLE_NAME.$GROUP_ID IS NULL AND
$TABLE_NAME.$REGISTERED = ${RegisteredState.NOT_REGISTERED.id} AND
$TABLE_NAME.$SEEN_INVITE_REMINDER < ${InsightsBannerTier.TIER_TWO.id} AND
${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.HAS_SENT} AND
${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.DATE} > ? AND
$TABLE_NAME.$HIDDEN = 0
ORDER BY ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.DATE} DESC LIMIT 50
"""
}
fun containsPhoneOrUuid(id: String): Boolean {
val query = "$SERVICE_ID = ? OR $PHONE = ?"
val args = arrayOf(id, id)
readableDatabase.query(TABLE_NAME, arrayOf(ID), query, args, null, null, null).use { cursor -> return cursor != null && cursor.moveToFirst() }
}
fun getByE164(e164: String): Optional<RecipientId> {
return getByColumn(PHONE, e164)
}
fun getByEmail(email: String): Optional<RecipientId> {
return getByColumn(EMAIL, email)
}
fun getByGroupId(groupId: GroupId): Optional<RecipientId> {
return getByColumn(GROUP_ID, groupId.toString())
}
fun getByServiceId(serviceId: ServiceId): Optional<RecipientId> {
return getByColumn(SERVICE_ID, serviceId.toString())
}
/**
* Will return a recipient matching the PNI, but only in the explicit [PNI_COLUMN]. This should only be checked in conjunction with [getByServiceId] as a way
* to avoid creating a recipient we already merged.
*/
fun getByPni(pni: PNI): Optional<RecipientId> {
return getByColumn(PNI_COLUMN, pni.toString())
}
fun getByUsername(username: String): Optional<RecipientId> {
return getByColumn(USERNAME, username)
}
fun isAssociated(serviceId: ServiceId, pni: PNI): Boolean {
return readableDatabase.exists(TABLE_NAME, "$SERVICE_ID = ? AND $PNI_COLUMN = ?", serviceId.toString(), pni.toString())
}
@JvmOverloads
fun getAndPossiblyMerge(serviceId: ServiceId?, e164: String?, changeSelf: Boolean = false): RecipientId {
require(!(serviceId == null && e164 == null)) { "Must provide an ACI or E164!" }
return getAndPossiblyMerge(serviceId = serviceId, pni = null, e164 = e164, pniVerified = false, changeSelf = changeSelf)
}
/**
* Gets and merges a (serviceId, pni, e164) tuple, doing merges/updates as needed, and giving you back the final RecipientId.
* It is assumed that the tuple is verified. Do not give this method an untrusted association.
*/
fun getAndPossiblyMergePnpVerified(serviceId: ServiceId?, pni: PNI?, e164: String?): RecipientId {
if (!FeatureFlags.phoneNumberPrivacy()) {
throw AssertionError()
}
return getAndPossiblyMerge(serviceId = serviceId, pni = pni, e164 = e164, pniVerified = true, changeSelf = false)
}
private fun getAndPossiblyMerge(serviceId: ServiceId?, pni: PNI?, e164: String?, pniVerified: Boolean = false, changeSelf: Boolean = false): RecipientId {
require(!(serviceId == null && e164 == null)) { "Must provide an ACI or E164!" }
if ((serviceId is PNI) && pni != null && serviceId != pni) {
throw AssertionError("Provided two non-matching PNIs! serviceId: $serviceId, pni: $pni")
}
val db = writableDatabase
var transactionSuccessful = false
lateinit var result: ProcessPnpTupleResult
db.beginTransaction()
try {
result = when {
serviceId is ACI -> processPnpTuple(e164 = e164, pni = pni, aci = serviceId, pniVerified = pniVerified, changeSelf = changeSelf)
serviceId is PNI -> processPnpTuple(e164 = e164, pni = serviceId, aci = null, pniVerified = pniVerified, changeSelf = changeSelf)
serviceId == null -> processPnpTuple(e164 = e164, pni = pni, aci = null, pniVerified = pniVerified, changeSelf = changeSelf)
serviceId == pni -> processPnpTuple(e164 = e164, pni = pni, aci = null, pniVerified = pniVerified, changeSelf = changeSelf)
pni != null -> processPnpTuple(e164 = e164, pni = pni, aci = ACI.from(serviceId.uuid()), pniVerified = pniVerified, changeSelf = changeSelf)
getByPni(PNI.from(serviceId.uuid())).isPresent -> processPnpTuple(e164 = e164, pni = PNI.from(serviceId.uuid()), aci = null, pniVerified = pniVerified, changeSelf = changeSelf)
else -> processPnpTuple(e164 = e164, pni = pni, aci = ACI.fromNullable(serviceId), pniVerified = pniVerified, changeSelf = changeSelf)
}
if (result.operations.isNotEmpty()) {
Log.i(TAG, "[getAndPossiblyMergePnp] ($serviceId, $pni, $e164) 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> {
val serviceIdToProfileKey: MutableMap<ServiceId, ProfileKey> = mutableMapOf()
readableDatabase
.select(SERVICE_ID, PROFILE_KEY)
.from(TABLE_NAME)
.where("$SERVICE_ID NOT NULL AND $PROFILE_KEY NOT NULL")
.run()
.use { cursor ->
while (cursor.moveToNext()) {
val serviceId: ServiceId? = ServiceId.parseOrNull(cursor.requireString(SERVICE_ID))
val profileKey: ProfileKey? = ProfileKeyUtil.profileKeyOrNull(cursor.requireString(PROFILE_KEY))
if (serviceId != null && profileKey != null) {
serviceIdToProfileKey[serviceId] = profileKey
}
}
}
return serviceIdToProfileKey
}
private fun fetchRecipient(serviceId: ServiceId?, e164: String?, changeSelf: Boolean): RecipientFetch {
val byE164 = e164?.let { getByE164(it) } ?: Optional.empty()
val byAci = serviceId?.let { getByServiceId(it) } ?: Optional.empty()
var logs = LogBundle(
bySid = byAci.map { id -> RecipientLogDetails(id = id) }.orElse(null),
byE164 = byE164.map { id -> RecipientLogDetails(id = id) }.orElse(null),
label = "L0"
)
if (byAci.isPresent && byE164.isPresent && byAci.get() == byE164.get()) {
return RecipientFetch.Match(byAci.get(), logs.label("L0"))
}
if (byAci.isPresent && byE164.isAbsent()) {
val aciRecord: RecipientRecord = getRecord(byAci.get())
logs = logs.copy(bySid = aciRecord.toLogDetails())
if (e164 != null && (changeSelf || serviceId != SignalStore.account().aci)) {
val changedNumber: RecipientId? = if (aciRecord.e164 != null && aciRecord.e164 != e164) aciRecord.id else null
return RecipientFetch.MatchAndUpdateE164(byAci.get(), e164, changedNumber, logs.label("L1"))
} else if (e164 == null) {
return RecipientFetch.Match(byAci.get(), logs.label("L2"))
} else {
return RecipientFetch.Match(byAci.get(), logs.label("L3"))
}
}
if (byAci.isAbsent() && byE164.isPresent) {
val e164Record: RecipientRecord = getRecord(byE164.get())
logs = logs.copy(byE164 = e164Record.toLogDetails())
if (serviceId != null && e164Record.serviceId == null) {
return RecipientFetch.MatchAndUpdateAci(byE164.get(), serviceId, logs.label("L4"))
} else if (serviceId != null && e164Record.serviceId != SignalStore.account().aci) {
return RecipientFetch.InsertAndReassignE164(serviceId, e164, byE164.get(), logs.label("L5"))
} else if (serviceId != null) {
return RecipientFetch.Insert(serviceId, null, logs.label("L6"))
} else {
return RecipientFetch.Match(byE164.get(), logs.label("L7"))
}
}
if (byAci.isAbsent() && byE164.isAbsent()) {
return RecipientFetch.Insert(serviceId, e164, logs.label("L8"))
}
require(byAci.isPresent && byE164.isPresent && byAci.get() != byE164.get()) { "Assumed conditions at this point." }
val aciRecord: RecipientRecord = getRecord(byAci.get())
val e164Record: RecipientRecord = getRecord(byE164.get())
logs = logs.copy(bySid = aciRecord.toLogDetails(), byE164 = e164Record.toLogDetails())
if (e164Record.serviceId == null) {
val changedNumber: RecipientId? = if (aciRecord.e164 != null) aciRecord.id else null
return RecipientFetch.MatchAndMerge(sidId = byAci.get(), e164Id = byE164.get(), changedNumber = changedNumber, logs.label("L9"))
} else {
if (e164Record.serviceId != SignalStore.account().aci) {
val changedNumber: RecipientId? = if (aciRecord.e164 != null) aciRecord.id else null
return RecipientFetch.MatchAndReassignE164(id = byAci.get(), e164Id = byE164.get(), e164 = e164!!, changedNumber = changedNumber, logs.label("L10"))
} else {
return RecipientFetch.Match(byAci.get(), logs.label("L11"))
}
}
}
fun getOrInsertFromServiceId(serviceId: ServiceId): RecipientId {
return getOrInsertByColumn(SERVICE_ID, serviceId.toString()).recipientId
}
fun getOrInsertFromE164(e164: String): RecipientId {
return getOrInsertByColumn(PHONE, e164).recipientId
}
fun getOrInsertFromEmail(email: String): RecipientId {
return getOrInsertByColumn(EMAIL, email).recipientId
}
@JvmOverloads
fun getOrInsertFromDistributionListId(distributionListId: DistributionListId, storageId: ByteArray? = null): RecipientId {
return getOrInsertByColumn(
DISTRIBUTION_LIST_ID,
distributionListId.serialize(),
ContentValues().apply {
put(GROUP_TYPE, GroupType.DISTRIBUTION_LIST.id)
put(DISTRIBUTION_LIST_ID, distributionListId.serialize())
put(STORAGE_SERVICE_ID, Base64.encodeBytes(storageId ?: StorageSyncHelper.generateKey()))
put(PROFILE_SHARING, 1)
}
).recipientId
}
fun getDistributionListRecipientIds(): List<RecipientId> {
val recipientIds = mutableListOf<RecipientId>()
readableDatabase.query(TABLE_NAME, arrayOf(ID), "$DISTRIBUTION_LIST_ID is not NULL", null, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
recipientIds.add(RecipientId.from(CursorUtil.requireLong(cursor, ID)))
}
}
return recipientIds
}
fun getOrInsertFromGroupId(groupId: GroupId): RecipientId {
var existing = getByGroupId(groupId)
if (existing.isPresent) {
return existing.get()
} else if (groupId.isV1 && groups.groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
throw LegacyGroupInsertException(groupId)
} else if (groupId.isV2 && groups.getGroupV1ByExpectedV2(groupId.requireV2()).isPresent) {
throw MissedGroupMigrationInsertException(groupId)
} else {
val values = ContentValues().apply {
put(GROUP_ID, groupId.toString())
put(AVATAR_COLOR, AvatarColor.random().serialize())
}
val id = writableDatabase.insert(TABLE_NAME, null, values)
if (id < 0) {
existing = getByColumn(GROUP_ID, groupId.toString())
if (existing.isPresent) {
return existing.get()
} else if (groupId.isV1 && groups.groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
throw LegacyGroupInsertException(groupId)
} else if (groupId.isV2 && groups.getGroupV1ByExpectedV2(groupId.requireV2()).isPresent) {
throw MissedGroupMigrationInsertException(groupId)
} else {
throw AssertionError("Failed to insert recipient!")
}
} else {
val groupUpdates = ContentValues().apply {
if (groupId.isMms) {
put(GROUP_TYPE, GroupType.MMS.id)
} else {
if (groupId.isV2) {
put(GROUP_TYPE, GroupType.SIGNAL_V2.id)
} else {
put(GROUP_TYPE, GroupType.SIGNAL_V1.id)
}
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
}
}
val recipientId = RecipientId.from(id)
update(recipientId, groupUpdates)
return recipientId
}
}
}
/**
* See [Recipient.externalPossiblyMigratedGroup].
*/
fun getOrInsertFromPossiblyMigratedGroupId(groupId: GroupId): RecipientId {
val db = writableDatabase
db.beginTransaction()
try {
val existing = getByColumn(GROUP_ID, groupId.toString())
if (existing.isPresent) {
db.setTransactionSuccessful()
return existing.get()
}
if (groupId.isV1) {
val v2 = getByGroupId(groupId.requireV1().deriveV2MigrationGroupId())
if (v2.isPresent) {
db.setTransactionSuccessful()
return v2.get()
}
}
if (groupId.isV2) {
val v1 = groups.getGroupV1ByExpectedV2(groupId.requireV2())
if (v1.isPresent) {
db.setTransactionSuccessful()
return v1.get().recipientId
}
}
val id = getOrInsertFromGroupId(groupId)
db.setTransactionSuccessful()
return id
} finally {
db.endTransaction()
}
}
/**
* Only call once to create initial release channel recipient.
*/
fun insertReleaseChannelRecipient(): RecipientId {
val values = ContentValues().apply {
put(AVATAR_COLOR, AvatarColor.random().serialize())
}
val id = writableDatabase.insert(TABLE_NAME, null, values)
if (id < 0) {
throw AssertionError("Failed to insert recipient!")
} else {
return GetOrInsertResult(RecipientId.from(id), true).recipientId
}
}
fun getBlocked(): Cursor {
return readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$BLOCKED = 1", null, null, null, null)
}
fun readerForBlocked(cursor: Cursor): RecipientReader {
return RecipientReader(cursor)
}
fun getRecipientsWithNotificationChannels(): RecipientReader {
val cursor = readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$NOTIFICATION_CHANNEL NOT NULL", null, null, null, null)
return RecipientReader(cursor)
}
fun getRecord(id: RecipientId): RecipientRecord {
val query = "$ID = ?"
val args = arrayOf(id.serialize())
readableDatabase.query(TABLE_NAME, RECIPIENT_PROJECTION, query, args, null, null, null).use { cursor ->
return if (cursor != null && cursor.moveToNext()) {
getRecord(context, cursor)
} else {
val remapped = RemappedRecords.getInstance().getRecipient(id)
if (remapped.isPresent) {
Log.w(TAG, "Missing recipient for $id, but found it in the remapped records as ${remapped.get()}")
getRecord(remapped.get())
} else {
throw MissingRecipientException(id)
}
}
}
}
fun getRecordForSync(id: RecipientId): RecipientRecord? {
val query = "$TABLE_NAME.$ID = ?"
val args = arrayOf(id.serialize())
val recordForSync = getRecordForSync(query, args)
if (recordForSync.isEmpty()) {
return null
}
if (recordForSync.size > 1) {
throw AssertionError()
}
return recordForSync[0]
}
fun getByStorageId(storageId: ByteArray): RecipientRecord? {
val result = getRecordForSync("$TABLE_NAME.$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeBytes(storageId)))
return if (result.isNotEmpty()) {
result[0]
} else null
}
fun markNeedsSyncWithoutRefresh(recipientIds: Collection<RecipientId>) {
val db = writableDatabase
db.beginTransaction()
try {
for (recipientId in recipientIds) {
rotateStorageId(recipientId)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
fun markNeedsSync(recipientIds: Collection<RecipientId>) {
writableDatabase
.withinTransaction {
for (recipientId in recipientIds) {
markNeedsSync(recipientId)
}
}
}
fun markNeedsSync(recipientId: RecipientId) {
rotateStorageId(recipientId)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
}
fun markAllSystemContactsNeedsSync() {
writableDatabase.withinTransaction { db ->
db
.select(ID)
.from(TABLE_NAME)
.where("$SYSTEM_CONTACT_URI NOT NULL")
.run()
.use { cursor ->
while (cursor.moveToNext()) {
rotateStorageId(RecipientId.from(cursor.requireLong(ID)))
}
}
}
}
fun applyStorageIdUpdates(storageIds: Map<RecipientId, StorageId>) {
val db = writableDatabase
db.beginTransaction()
try {
val query = "$ID = ?"
for ((key, value) in storageIds) {
val values = ContentValues().apply {
put(STORAGE_SERVICE_ID, Base64.encodeBytes(value.raw))
}
db.update(TABLE_NAME, values, query, arrayOf(key.serialize()))
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
for (id in storageIds.keys) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun applyStorageSyncContactInsert(insert: SignalContactRecord) {
val db = writableDatabase
val threadDatabase = threads
val values = getValuesForStorageContact(insert, true)
val id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE)
val recipientId: RecipientId
if (id < 0) {
Log.w(TAG, "[applyStorageSyncContactInsert] Failed to insert. Possibly merging.")
if (FeatureFlags.phoneNumberPrivacy()) {
recipientId = getAndPossiblyMergePnpVerified(if (insert.serviceId.isValid) insert.serviceId else null, insert.pni.orElse(null), insert.number.orElse(null))
} else {
recipientId = getAndPossiblyMerge(if (insert.serviceId.isValid) insert.serviceId else null, insert.number.orElse(null))
}
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId))
} else {
recipientId = RecipientId.from(id)
}
if (insert.identityKey.isPresent && insert.serviceId.isValid) {
try {
val identityKey = IdentityKey(insert.identityKey.get(), 0)
identities.updateIdentityAfterSync(insert.serviceId.toString(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.identityState))
} catch (e: InvalidKeyException) {
Log.w(TAG, "Failed to process identity key during insert! Skipping.", e)
}
}
updateExtras(recipientId) {
it.setHideStory(insert.shouldHideStory())
}
threadDatabase.applyStorageSyncUpdate(recipientId, insert)
}
fun applyStorageSyncContactUpdate(update: StorageRecordUpdate<SignalContactRecord>) {
val db = writableDatabase
val identityStore = ApplicationDependencies.getProtocolStore().aci().identities()
val values = getValuesForStorageContact(update.new, false)
try {
val updateCount = db.update(TABLE_NAME, values, "$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeBytes(update.old.id.raw)))
if (updateCount < 1) {
throw AssertionError("Had an update, but it didn't match any rows!")
}
} catch (e: SQLiteConstraintException) {
Log.w(TAG, "[applyStorageSyncContactUpdate] Failed to update a user by storageId.")
var recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeBytes(update.old.id.raw)).get()
Log.w(TAG, "[applyStorageSyncContactUpdate] Found user $recipientId. Possibly merging.")
if (FeatureFlags.phoneNumberPrivacy()) {
recipientId = getAndPossiblyMergePnpVerified(if (update.new.serviceId.isValid) update.new.serviceId else null, update.new.pni.orElse(null), update.new.number.orElse(null))
} else {
recipientId = getAndPossiblyMerge(if (update.new.serviceId.isValid) update.new.serviceId else null, update.new.number.orElse(null))
}
Log.w(TAG, "[applyStorageSyncContactUpdate] Merged into $recipientId")
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId))
}
val recipientId = getByStorageKeyOrThrow(update.new.id.raw)
if (StorageSyncHelper.profileKeyChanged(update)) {
val clearValues = ContentValues(1).apply {
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
}
db.update(TABLE_NAME, clearValues, ID_WHERE, SqlUtil.buildArgs(recipientId))
}
try {
val oldIdentityRecord = identityStore.getIdentityRecord(recipientId)
if (update.new.identityKey.isPresent && update.new.serviceId.isValid) {
val identityKey = IdentityKey(update.new.identityKey.get(), 0)
identities.updateIdentityAfterSync(update.new.serviceId.toString(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.new.identityState))
}
val newIdentityRecord = identityStore.getIdentityRecord(recipientId)
if (newIdentityRecord.isPresent && newIdentityRecord.get().verifiedStatus == VerifiedStatus.VERIFIED && (!oldIdentityRecord.isPresent || oldIdentityRecord.get().verifiedStatus != VerifiedStatus.VERIFIED)) {
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true)
} else if (newIdentityRecord.isPresent && newIdentityRecord.get().verifiedStatus != VerifiedStatus.VERIFIED && oldIdentityRecord.isPresent && oldIdentityRecord.get().verifiedStatus == VerifiedStatus.VERIFIED) {
IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true)
}
} catch (e: InvalidKeyException) {
Log.w(TAG, "Failed to process identity key during update! Skipping.", e)
}
updateExtras(recipientId) {
it.setHideStory(update.new.shouldHideStory())
}
threads.applyStorageSyncUpdate(recipientId, update.new)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
}
fun applyStorageSyncGroupV1Insert(insert: SignalGroupV1Record) {
val id = writableDatabase.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert, true))
val recipientId = RecipientId.from(id)
threads.applyStorageSyncUpdate(recipientId, insert)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
}
fun applyStorageSyncGroupV1Update(update: StorageRecordUpdate<SignalGroupV1Record>) {
val values = getValuesForStorageGroupV1(update.new, false)
val updateCount = writableDatabase.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", arrayOf(Base64.encodeBytes(update.old.id.raw)))
if (updateCount < 1) {
throw AssertionError("Had an update, but it didn't match any rows!")
}
val recipient = Recipient.externalGroupExact(GroupId.v1orThrow(update.old.groupId))
threads.applyStorageSyncUpdate(recipient.id, update.new)
recipient.live().refresh()
}
fun applyStorageSyncGroupV2Insert(insert: SignalGroupV2Record) {
val masterKey = insert.masterKeyOrThrow
val groupId = GroupId.v2(masterKey)
val values = getValuesForStorageGroupV2(insert, true)
writableDatabase.insertOrThrow(TABLE_NAME, null, values)
val recipient = Recipient.externalGroupExact(groupId)
Log.i(TAG, "Creating restore placeholder for $groupId")
groups.create(
masterKey,
DecryptedGroup.newBuilder()
.setRevision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
.build()
)
groups.setShowAsStoryState(groupId, insert.storySendMode.toShowAsStoryState())
updateExtras(recipient.id) {
it.setHideStory(insert.shouldHideStory())
}
Log.i(TAG, "Scheduling request for latest group info for $groupId")
ApplicationDependencies.getJobManager().add(RequestGroupV2InfoJob(groupId))
threads.applyStorageSyncUpdate(recipient.id, insert)
recipient.live().refresh()
}
fun applyStorageSyncGroupV2Update(update: StorageRecordUpdate<SignalGroupV2Record>) {
val values = getValuesForStorageGroupV2(update.new, false)
val updateCount = writableDatabase.update(TABLE_NAME, values, "$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeBytes(update.old.id.raw)))
if (updateCount < 1) {
throw AssertionError("Had an update, but it didn't match any rows!")
}
val masterKey = update.old.masterKeyOrThrow
val groupId = GroupId.v2(masterKey)
val recipient = Recipient.externalGroupExact(groupId)
updateExtras(recipient.id) {
it.setHideStory(update.new.shouldHideStory())
}
groups.setShowAsStoryState(groupId, update.new.storySendMode.toShowAsStoryState())
threads.applyStorageSyncUpdate(recipient.id, update.new)
recipient.live().refresh()
}
fun applyStorageSyncAccountUpdate(update: StorageRecordUpdate<SignalAccountRecord>) {
val profileName = ProfileName.fromParts(update.new.givenName.orElse(null), update.new.familyName.orElse(null))
val localKey = ProfileKeyUtil.profileKeyOptional(update.old.profileKey.orElse(null))
val remoteKey = ProfileKeyUtil.profileKeyOptional(update.new.profileKey.orElse(null))
val profileKey: String? = remoteKey.or(localKey).map { obj: ProfileKey -> obj.serialize() }.map { source: ByteArray? -> Base64.encodeBytes(source!!) }.orElse(null)
if (!remoteKey.isPresent) {
Log.w(TAG, "Got an empty profile key while applying an account record update! The parsed local key is ${if (localKey.isPresent) "present" else "not present"}. The raw local key is ${if (update.old.profileKey.isPresent) "present" else "not present"}. The resulting key is ${if (profileKey != null) "present" else "not present"}.")
}
val values = ContentValues().apply {
put(PROFILE_GIVEN_NAME, profileName.givenName)
put(PROFILE_FAMILY_NAME, profileName.familyName)
put(PROFILE_JOINED_NAME, profileName.toString())
if (profileKey != null) {
put(PROFILE_KEY, profileKey)
} else {
Log.w(TAG, "Avoided attempt to apply null profile key in account record update!")
}
put(STORAGE_SERVICE_ID, Base64.encodeBytes(update.new.id.raw))
if (update.new.hasUnknownFields()) {
put(STORAGE_PROTO, Base64.encodeBytes(Objects.requireNonNull(update.new.serializeUnknownFields())))
} else {
putNull(STORAGE_PROTO)
}
}
val updateCount = writableDatabase.update(TABLE_NAME, values, "$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeBytes(update.old.id.raw)))
if (updateCount < 1) {
throw AssertionError("Account update didn't match any rows!")
}
if (remoteKey != localKey) {
Log.i(TAG, "Our own profile key was changed during a storage sync.", Throwable())
runPostSuccessfulTransaction { ProfileUtil.handleSelfProfileKeyChange() }
}
threads.applyStorageSyncUpdate(Recipient.self().id, update.new)
Recipient.self().live().refresh()
}
/**
* Removes storageIds from unregistered recipients who were unregistered more than [UNREGISTERED_LIFESPAN] ago.
* @return The number of rows affected.
*/
fun removeStorageIdsFromOldUnregisteredRecipients(now: Long): Int {
return writableDatabase
.update(TABLE_NAME)
.values(STORAGE_SERVICE_ID to null)
.where("$STORAGE_SERVICE_ID NOT NULL AND $UNREGISTERED_TIMESTAMP > 0 AND $UNREGISTERED_TIMESTAMP < ?", now - UNREGISTERED_LIFESPAN)
.run()
}
/**
* Removes storageIds from unregistered contacts that have storageIds in the provided collection.
* @return The number of updated rows.
*/
fun removeStorageIdsFromLocalOnlyUnregisteredRecipients(storageIds: Collection<StorageId>): Int {
val values = contentValuesOf(STORAGE_SERVICE_ID to null)
var updated = 0
SqlUtil.buildCollectionQuery(STORAGE_SERVICE_ID, storageIds.map { Base64.encodeBytes(it.raw) }, "$UNREGISTERED_TIMESTAMP > 0 AND")
.forEach {
updated += writableDatabase.update(TABLE_NAME, values, it.where, it.whereArgs)
}
return updated
}
/**
* Takes a mapping of old->new phone numbers and updates the table to match.
* Intended to be used to handle changing number formats.
*/
fun rewritePhoneNumbers(mapping: Map<String, String>) {
if (mapping.isEmpty()) return
Log.i(TAG, "Rewriting ${mapping.size} phone numbers.")
writableDatabase.withinTransaction {
for ((originalE164, updatedE164) in mapping) {
writableDatabase.update(TABLE_NAME)
.values(PHONE to updatedE164)
.where("$PHONE = ?", originalE164)
.run(SQLiteDatabase.CONFLICT_IGNORE)
}
}
}
private fun getByStorageKeyOrThrow(storageKey: ByteArray): RecipientId {
val query = "$STORAGE_SERVICE_ID = ?"
val args = arrayOf(Base64.encodeBytes(storageKey))
readableDatabase.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null).use { cursor ->
return if (cursor != null && cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
RecipientId.from(id)
} else {
throw AssertionError("No recipient with that storage key!")
}
}
}
private fun GroupV2Record.StorySendMode.toShowAsStoryState(): ShowAsStoryState {
return when (this) {
GroupV2Record.StorySendMode.DEFAULT -> ShowAsStoryState.IF_ACTIVE
GroupV2Record.StorySendMode.DISABLED -> ShowAsStoryState.NEVER
GroupV2Record.StorySendMode.ENABLED -> ShowAsStoryState.ALWAYS
GroupV2Record.StorySendMode.UNRECOGNIZED -> ShowAsStoryState.IF_ACTIVE
}
}
private fun getRecordForSync(query: String?, args: Array<String>?): List<RecipientRecord> {
val table =
"""
$TABLE_NAME LEFT OUTER JOIN ${IdentityDatabase.TABLE_NAME} ON $TABLE_NAME.$SERVICE_ID = ${IdentityDatabase.TABLE_NAME}.${IdentityDatabase.ADDRESS}
LEFT OUTER JOIN ${GroupDatabase.TABLE_NAME} ON $TABLE_NAME.$GROUP_ID = ${GroupDatabase.TABLE_NAME}.${GroupDatabase.GROUP_ID}
LEFT OUTER JOIN ${ThreadDatabase.TABLE_NAME} ON $TABLE_NAME.$ID = ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.RECIPIENT_ID}
""".trimIndent()
val out: MutableList<RecipientRecord> = ArrayList()
val columns: Array<String> = TYPED_RECIPIENT_PROJECTION + arrayOf(
"$TABLE_NAME.$STORAGE_PROTO",
"$TABLE_NAME.$UNREGISTERED_TIMESTAMP",
"${GroupDatabase.TABLE_NAME}.${GroupDatabase.V2_MASTER_KEY}",
"${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.ARCHIVED}",
"${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.READ}",
"${IdentityDatabase.TABLE_NAME}.${IdentityDatabase.VERIFIED} AS $IDENTITY_STATUS",
"${IdentityDatabase.TABLE_NAME}.${IdentityDatabase.IDENTITY_KEY} AS $IDENTITY_KEY"
)
readableDatabase.query(table, columns, query, args, "$TABLE_NAME.$ID", null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
out.add(getRecord(context, cursor))
}
}
return out
}
/**
* @return All storage ids for ContactRecords, excluding the ones that need to be deleted.
*/
fun getContactStorageSyncIds(): List<StorageId> {
return ArrayList(getContactStorageSyncIdsMap().values)
}
/**
* @return All storage IDs for synced records, excluding the ones that need to be deleted.
*/
fun getContactStorageSyncIdsMap(): Map<RecipientId, StorageId> {
val (inPart, args) = if (isFeatureFlagEnabled()) {
"(?, ?)" to SqlUtil.buildArgs(GroupType.NONE.id, Recipient.self().id, GroupType.SIGNAL_V1.id, GroupType.DISTRIBUTION_LIST.id)
} else {
"(?)" to SqlUtil.buildArgs(GroupType.NONE.id, Recipient.self().id, GroupType.SIGNAL_V1.id)
}
val query = """
$STORAGE_SERVICE_ID NOT NULL AND (
($GROUP_TYPE = ? AND $SERVICE_ID NOT NULL AND $ID != ?)
OR
$GROUP_TYPE IN $inPart
)
""".trimIndent()
val out: MutableMap<RecipientId, StorageId> = HashMap()
readableDatabase.query(TABLE_NAME, arrayOf(ID, STORAGE_SERVICE_ID, GROUP_TYPE), query, args, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val id = RecipientId.from(cursor.requireLong(ID))
val encodedKey = cursor.requireNonNullString(STORAGE_SERVICE_ID)
val groupType = GroupType.fromId(cursor.requireInt(GROUP_TYPE))
val key = Base64.decodeOrThrow(encodedKey)
when (groupType) {
GroupType.NONE -> out[id] = StorageId.forContact(key)
GroupType.SIGNAL_V1 -> out[id] = StorageId.forGroupV1(key)
GroupType.DISTRIBUTION_LIST -> out[id] = StorageId.forStoryDistributionList(key)
else -> throw AssertionError()
}
}
}
for (id in groups.allGroupV2Ids) {
val recipient = Recipient.externalGroupExact(id!!)
val recipientId = recipient.id
val existing: RecipientRecord = getRecordForSync(recipientId) ?: throw AssertionError()
val key = existing.storageId ?: throw AssertionError()
out[recipientId] = StorageId.forGroupV2(key)
}
return out
}
/**
* Given a collection of [RecipientId]s, this will do an efficient bulk query to find all matching E164s.
* If one cannot be found, no error thrown, it will just be omitted.
*/
fun getE164sForIds(ids: Collection<RecipientId>): Set<String> {
val queries: List<SqlUtil.Query> = SqlUtil.buildCustomCollectionQuery(
"$ID = ?",
ids.map { arrayOf(it.serialize()) }.toList()
)
val out: MutableSet<String> = mutableSetOf()
for (query in queries) {
readableDatabase.query(TABLE_NAME, arrayOf(PHONE), query.where, query.whereArgs, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
val e164: String? = cursor.requireString(PHONE)
if (e164 != null) {
out.add(e164)
}
}
}
}
return out
}
/**
* @param clearInfoForMissingContacts If true, this will clear any saved contact details for any recipient that hasn't been updated
* by the time finish() is called. Basically this should be true for full syncs and false for
* partial syncs.
*/
fun beginBulkSystemContactUpdate(clearInfoForMissingContacts: Boolean): BulkOperationsHandle {
writableDatabase.beginTransaction()
if (clearInfoForMissingContacts) {
writableDatabase
.update(TABLE_NAME)
.values(SYSTEM_INFO_PENDING to 1)
.where("$SYSTEM_CONTACT_URI NOT NULL")
.run()
}
return BulkOperationsHandle(writableDatabase)
}
fun onUpdatedChatColors(chatColors: ChatColors) {
val where = "$CUSTOM_CHAT_COLORS_ID = ?"
val args = SqlUtil.buildArgs(chatColors.id.longValue)
val updated: MutableList<RecipientId> = LinkedList()
readableDatabase.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
updated.add(RecipientId.from(cursor.requireLong(ID)))
}
}
if (updated.isEmpty()) {
Log.d(TAG, "No recipients utilizing updated chat color.")
} else {
val values = ContentValues(2).apply {
put(CHAT_COLORS, chatColors.serialize().toByteArray())
put(CUSTOM_CHAT_COLORS_ID, chatColors.id.longValue)
}
writableDatabase.update(TABLE_NAME, values, where, args)
for (recipientId in updated) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
}
}
}
fun onDeletedChatColors(chatColors: ChatColors) {
val where = "$CUSTOM_CHAT_COLORS_ID = ?"
val args = SqlUtil.buildArgs(chatColors.id.longValue)
val updated: MutableList<RecipientId> = LinkedList()
readableDatabase.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
updated.add(RecipientId.from(cursor.requireLong(ID)))
}
}
if (updated.isEmpty()) {
Log.d(TAG, "No recipients utilizing deleted chat color.")
} else {
val values = ContentValues(2).apply {
put(CHAT_COLORS, null as ByteArray?)
put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue)
}
writableDatabase.update(TABLE_NAME, values, where, args)
for (recipientId in updated) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
}
}
}
fun getColorUsageCount(chatColorsId: ChatColors.Id): Int {
val where = "$CUSTOM_CHAT_COLORS_ID = ?"
val args = SqlUtil.buildArgs(chatColorsId.longValue)
readableDatabase.query(TABLE_NAME, arrayOf("COUNT(*)"), where, args, null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
cursor.getInt(0)
} else {
0
}
}
}
fun clearAllColors() {
val database = writableDatabase
val where = "$CUSTOM_CHAT_COLORS_ID != ?"
val args = SqlUtil.buildArgs(ChatColors.Id.NotSet.longValue)
val toUpdate: MutableList<RecipientId> = LinkedList()
database.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
toUpdate.add(RecipientId.from(cursor.requireLong(ID)))
}
}
if (toUpdate.isEmpty()) {
return
}
val values = ContentValues().apply {
put(CHAT_COLORS, null as ByteArray?)
put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue)
}
database.update(TABLE_NAME, values, where, args)
for (id in toUpdate) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun clearColor(id: RecipientId) {
val values = ContentValues().apply {
put(CHAT_COLORS, null as ByteArray?)
put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue)
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setColor(id: RecipientId, color: ChatColors) {
val values = ContentValues().apply {
put(CHAT_COLORS, color.serialize().toByteArray())
put(CUSTOM_CHAT_COLORS_ID, color.id.longValue)
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setDefaultSubscriptionId(id: RecipientId, defaultSubscriptionId: Int) {
val values = ContentValues().apply {
put(DEFAULT_SUBSCRIPTION_ID, defaultSubscriptionId)
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setForceSmsSelection(id: RecipientId, forceSmsSelection: Boolean) {
val contentValues = ContentValues(1).apply {
put(FORCE_SMS_SELECTION, if (forceSmsSelection) 1 else 0)
}
if (update(id, contentValues)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setBlocked(id: RecipientId, blocked: Boolean) {
val values = ContentValues().apply {
put(BLOCKED, if (blocked) 1 else 0)
}
if (update(id, values)) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setMessageRingtone(id: RecipientId, notification: Uri?) {
val values = ContentValues().apply {
put(MESSAGE_RINGTONE, notification?.toString())
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setCallRingtone(id: RecipientId, ringtone: Uri?) {
val values = ContentValues().apply {
put(CALL_RINGTONE, ringtone?.toString())
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setMessageVibrate(id: RecipientId, enabled: VibrateState) {
val values = ContentValues().apply {
put(MESSAGE_VIBRATE, enabled.id)
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setCallVibrate(id: RecipientId, enabled: VibrateState) {
val values = ContentValues().apply {
put(CALL_VIBRATE, enabled.id)
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setMuted(id: RecipientId, until: Long) {
val values = ContentValues().apply {
put(MUTE_UNTIL, until)
}
if (update(id, values)) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
StorageSyncHelper.scheduleSyncForDataChange()
}
fun setMuted(ids: Collection<RecipientId>, until: Long) {
val db = writableDatabase
db.beginTransaction()
try {
val query = SqlUtil.buildSingleCollectionQuery(ID, ids)
val values = ContentValues().apply {
put(MUTE_UNTIL, until)
}
db.update(TABLE_NAME, values, query.where, query.whereArgs)
for (id in ids) {
rotateStorageId(id)
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
for (id in ids) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
StorageSyncHelper.scheduleSyncForDataChange()
}
fun setSeenFirstInviteReminder(id: RecipientId) {
setInsightsBannerTier(id, InsightsBannerTier.TIER_ONE)
}
fun setSeenSecondInviteReminder(id: RecipientId) {
setInsightsBannerTier(id, InsightsBannerTier.TIER_TWO)
}
fun setHasSentInvite(id: RecipientId) {
setSeenSecondInviteReminder(id)
}
private fun setInsightsBannerTier(id: RecipientId, insightsBannerTier: InsightsBannerTier) {
val query = "$ID = ? AND $SEEN_INVITE_REMINDER < ?"
val args = arrayOf(id.serialize(), insightsBannerTier.toString())
val values = ContentValues(1).apply {
put(SEEN_INVITE_REMINDER, insightsBannerTier.id)
}
writableDatabase.update(TABLE_NAME, values, query, args)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
fun setExpireMessages(id: RecipientId, expiration: Int) {
val values = ContentValues(1).apply {
put(MESSAGE_EXPIRATION_TIME, expiration)
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setUnidentifiedAccessMode(id: RecipientId, unidentifiedAccessMode: UnidentifiedAccessMode) {
val values = ContentValues(1).apply {
put(UNIDENTIFIED_ACCESS_MODE, unidentifiedAccessMode.mode)
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setLastSessionResetTime(id: RecipientId, lastResetTime: DeviceLastResetTime) {
val values = ContentValues(1).apply {
put(LAST_SESSION_RESET, lastResetTime.toByteArray())
}
update(id, values)
}
fun getLastSessionResetTimes(id: RecipientId): DeviceLastResetTime {
readableDatabase.query(TABLE_NAME, arrayOf(LAST_SESSION_RESET), ID_WHERE, SqlUtil.buildArgs(id), null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
return try {
val serialized = cursor.requireBlob(LAST_SESSION_RESET)
if (serialized != null) {
DeviceLastResetTime.parseFrom(serialized)
} else {
DeviceLastResetTime.newBuilder().build()
}
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, e)
DeviceLastResetTime.newBuilder().build()
}
}
}
return DeviceLastResetTime.newBuilder().build()
}
fun setBadges(id: RecipientId, badges: List<Badge>) {
val badgeListBuilder = BadgeList.newBuilder()
for (badge in badges) {
badgeListBuilder.addBadges(toDatabaseBadge(badge))
}
val values = ContentValues(1).apply {
put(BADGES, badgeListBuilder.build().toByteArray())
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setCapabilities(id: RecipientId, capabilities: SignalServiceProfile.Capabilities) {
var value: Long = 0
value = Bitmask.update(value, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGv1Migration).serialize().toLong())
value = Bitmask.update(value, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isSenderKey).serialize().toLong())
value = Bitmask.update(value, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isAnnouncementGroup).serialize().toLong())
value = Bitmask.update(value, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isChangeNumber).serialize().toLong())
value = Bitmask.update(value, Capabilities.STORIES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isStories).serialize().toLong())
value = Bitmask.update(value, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isGiftBadges).serialize().toLong())
value = Bitmask.update(value, Capabilities.PNP, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPnp).serialize().toLong())
val values = ContentValues(1).apply {
put(CAPABILITIES, value)
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setMentionSetting(id: RecipientId, mentionSetting: MentionSetting) {
val values = ContentValues().apply {
put(MENTION_SETTING, mentionSetting.id)
}
if (update(id, values)) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
/**
* Updates the profile key.
*
* If it changes, it clears out the profile key credential and resets the unidentified access mode.
* @return true iff changed.
*/
fun setProfileKey(id: RecipientId, profileKey: ProfileKey): Boolean {
val selection = "$ID = ?"
val args = arrayOf(id.serialize())
val encodedProfileKey = Base64.encodeBytes(profileKey.serialize())
val valuesToCompare = ContentValues(1).apply {
put(PROFILE_KEY, encodedProfileKey)
}
val valuesToSet = ContentValues(3).apply {
put(PROFILE_KEY, encodedProfileKey)
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.mode)
}
val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare)
if (update(updateQuery, valuesToSet)) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
if (id == Recipient.self().id) {
Log.i(TAG, "Our own profile key was changed.", Throwable())
runPostSuccessfulTransaction { ProfileUtil.handleSelfProfileKeyChange() }
}
return true
}
return false
}
/**
* Sets the profile key iff currently null.
*
* If it sets it, it also clears out the profile key credential and resets the unidentified access mode.
* @return true iff changed.
*/
fun setProfileKeyIfAbsent(id: RecipientId, profileKey: ProfileKey): Boolean {
val selection = "$ID = ? AND $PROFILE_KEY is NULL"
val args = arrayOf(id.serialize())
val valuesToSet = ContentValues(3).apply {
put(PROFILE_KEY, Base64.encodeBytes(profileKey.serialize()))
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
put(UNIDENTIFIED_ACCESS_MODE, UnidentifiedAccessMode.UNKNOWN.mode)
}
if (writableDatabase.update(TABLE_NAME, valuesToSet, selection, args) > 0) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
return true
} else {
return false
}
}
/**
* Updates the profile key credential as long as the profile key matches.
*/
fun setProfileKeyCredential(
id: RecipientId,
profileKey: ProfileKey,
expiringProfileKeyCredential: ExpiringProfileKeyCredential
): Boolean {
val selection = "$ID = ? AND $PROFILE_KEY = ?"
val args = arrayOf(id.serialize(), Base64.encodeBytes(profileKey.serialize()))
val columnData = ExpiringProfileKeyCredentialColumnData.newBuilder()
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
.setExpiringProfileKeyCredential(ByteString.copyFrom(expiringProfileKeyCredential.serialize()))
.build()
val values = ContentValues(1).apply {
put(EXPIRING_PROFILE_KEY_CREDENTIAL, Base64.encodeBytes(columnData.toByteArray()))
}
val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values)
val updated = update(updateQuery, values)
if (updated) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
return updated
}
fun clearProfileKeyCredential(id: RecipientId) {
val values = ContentValues(1)
values.putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
/**
* Fills in gaps (nulls) in profile key knowledge from new profile keys.
*
*
* If from authoritative source, this will overwrite local, otherwise it will only write to the
* database if missing.
*/
fun persistProfileKeySet(profileKeySet: ProfileKeySet): Set<RecipientId> {
val profileKeys = profileKeySet.profileKeys
val authoritativeProfileKeys = profileKeySet.authoritativeProfileKeys
val totalKeys = profileKeys.size + authoritativeProfileKeys.size
if (totalKeys == 0) {
return emptySet()
}
Log.i(TAG, "Persisting $totalKeys Profile keys, ${authoritativeProfileKeys.size} of which are authoritative")
val updated = HashSet<RecipientId>(totalKeys)
val selfId = Recipient.self().id
for ((key, value) in profileKeys) {
val recipientId = getOrInsertFromServiceId(key)
if (setProfileKeyIfAbsent(recipientId, value)) {
Log.i(TAG, "Learned new profile key")
updated.add(recipientId)
}
}
for ((key, value) in authoritativeProfileKeys) {
val recipientId = getOrInsertFromServiceId(key)
if (selfId == recipientId) {
Log.i(TAG, "Seen authoritative update for self")
if (value != ProfileKeyUtil.getSelfProfileKey()) {
Log.w(TAG, "Seen authoritative update for self that didn't match local, scheduling storage sync")
StorageSyncHelper.scheduleSyncForDataChange()
}
} else {
Log.i(TAG, "Profile key from owner $recipientId")
if (setProfileKey(recipientId, value)) {
Log.i(TAG, "Learned new profile key from owner")
updated.add(recipientId)
}
}
}
return updated
}
fun getSimilarRecipientIds(recipient: Recipient): List<RecipientId> {
val projection = SqlUtil.buildArgs(ID, "COALESCE(NULLIF($SYSTEM_JOINED_NAME, ''), NULLIF($PROFILE_JOINED_NAME, '')) AS checked_name")
val where = "checked_name = ? AND $HIDDEN = ?"
val arguments = SqlUtil.buildArgs(recipient.profileName.toString(), 0)
readableDatabase.query(TABLE_NAME, projection, where, arguments, null, null, null).use { cursor ->
if (cursor == null || cursor.count == 0) {
return emptyList()
}
val results: MutableList<RecipientId> = ArrayList(cursor.count)
while (cursor.moveToNext()) {
results.add(RecipientId.from(cursor.requireLong(ID)))
}
return results
}
}
fun setSystemContactName(id: RecipientId, systemContactName: String) {
val values = ContentValues().apply {
put(SYSTEM_JOINED_NAME, systemContactName)
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun setProfileName(id: RecipientId, profileName: ProfileName) {
val contentValues = ContentValues(1).apply {
put(PROFILE_GIVEN_NAME, profileName.givenName)
put(PROFILE_FAMILY_NAME, profileName.familyName)
put(PROFILE_JOINED_NAME, profileName.toString())
}
if (update(id, contentValues)) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
fun setProfileAvatar(id: RecipientId, profileAvatar: String?) {
val contentValues = ContentValues(1).apply {
put(SIGNAL_PROFILE_AVATAR, profileAvatar)
}
if (update(id, contentValues)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
if (id == Recipient.self().id) {
rotateStorageId(id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
}
fun setAbout(id: RecipientId, about: String?, emoji: String?) {
val contentValues = ContentValues().apply {
put(ABOUT, about)
put(ABOUT_EMOJI, emoji)
}
if (update(id, contentValues)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun markHidden(id: RecipientId) {
val contentValues = contentValuesOf(
HIDDEN to 1,
PROFILE_SHARING to 0
)
val updated = writableDatabase.update(TABLE_NAME, contentValues, "$ID_WHERE AND $GROUP_TYPE = ?", SqlUtil.buildArgs(id, GroupType.NONE.id)) > 0
if (updated) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
} else {
Log.w(TAG, "Failed to hide recipient $id")
}
}
fun setProfileSharing(id: RecipientId, enabled: Boolean) {
val contentValues = ContentValues(1).apply {
put(PROFILE_SHARING, if (enabled) 1 else 0)
}
if (enabled) {
contentValues.put(HIDDEN, 0)
}
val profiledUpdated = update(id, contentValues)
if (profiledUpdated && enabled) {
val group = groups.getGroup(id)
if (group.isPresent) {
setHasGroupsInCommon(group.get().members)
}
}
if (profiledUpdated) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
fun setNotificationChannel(id: RecipientId, notificationChannel: String?) {
val contentValues = ContentValues(1).apply {
put(NOTIFICATION_CHANNEL, notificationChannel)
}
if (update(id, contentValues)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun resetAllWallpaper() {
val database = writableDatabase
val selection = SqlUtil.buildArgs(ID, WALLPAPER_URI)
val where = "$WALLPAPER IS NOT NULL"
val idWithWallpaper: MutableList<Pair<RecipientId, String?>> = LinkedList()
database.beginTransaction()
try {
database.query(TABLE_NAME, selection, where, null, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
idWithWallpaper.add(
Pair(
RecipientId.from(cursor.requireInt(ID).toLong()),
cursor.optionalString(WALLPAPER_URI).orElse(null)
)
)
}
}
if (idWithWallpaper.isEmpty()) {
return
}
val values = ContentValues(2).apply {
putNull(WALLPAPER_URI)
putNull(WALLPAPER)
}
val rowsUpdated = database.update(TABLE_NAME, values, where, null)
if (rowsUpdated == idWithWallpaper.size) {
for (pair in idWithWallpaper) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(pair.first)
if (pair.second != null) {
WallpaperStorage.onWallpaperDeselected(context, Uri.parse(pair.second))
}
}
} else {
throw AssertionError("expected " + idWithWallpaper.size + " but got " + rowsUpdated)
}
} finally {
database.setTransactionSuccessful()
database.endTransaction()
}
}
fun setWallpaper(id: RecipientId, chatWallpaper: ChatWallpaper?) {
setWallpaper(id, chatWallpaper?.serialize())
}
private fun setWallpaper(id: RecipientId, wallpaper: Wallpaper?) {
val existingWallpaperUri = getWallpaperUri(id)
val values = ContentValues().apply {
put(WALLPAPER, wallpaper?.toByteArray())
if (wallpaper != null && wallpaper.hasFile()) {
put(WALLPAPER_URI, wallpaper.file.uri)
} else {
putNull(WALLPAPER_URI)
}
}
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
if (existingWallpaperUri != null) {
WallpaperStorage.onWallpaperDeselected(context, existingWallpaperUri)
}
}
fun setDimWallpaperInDarkTheme(id: RecipientId, enabled: Boolean) {
val wallpaper = getWallpaper(id) ?: throw IllegalStateException("No wallpaper set for $id")
val updated = wallpaper.toBuilder()
.setDimLevelInDarkTheme(if (enabled) ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME else 0f)
.build()
setWallpaper(id, updated)
}
private fun getWallpaper(id: RecipientId): Wallpaper? {
readableDatabase.query(TABLE_NAME, arrayOf(WALLPAPER), ID_WHERE, SqlUtil.buildArgs(id), null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
val raw = cursor.requireBlob(WALLPAPER)
return if (raw != null) {
try {
Wallpaper.parseFrom(raw)
} catch (e: InvalidProtocolBufferException) {
null
}
} else {
null
}
}
}
return null
}
private fun getWallpaperUri(id: RecipientId): Uri? {
val wallpaper = getWallpaper(id)
return if (wallpaper != null && wallpaper.hasFile()) {
Uri.parse(wallpaper.file.uri)
} else {
null
}
}
fun getWallpaperUriUsageCount(uri: Uri): Int {
val query = "$WALLPAPER_URI = ?"
val args = SqlUtil.buildArgs(uri)
readableDatabase.query(TABLE_NAME, arrayOf("COUNT(*)"), query, args, null, null, null).use { cursor ->
if (cursor.moveToFirst()) {
return cursor.getInt(0)
}
}
return 0
}
/**
* @return True if setting the phone number resulted in changed recipientId, otherwise false.
*/
fun setPhoneNumber(id: RecipientId, e164: String): Boolean {
val db = writableDatabase
db.beginTransaction()
return try {
setPhoneNumberOrThrow(id, e164)
db.setTransactionSuccessful()
false
} catch (e: SQLiteConstraintException) {
Log.w(TAG, "[setPhoneNumber] Hit a conflict when trying to update $id. Possibly merging.")
val existing: RecipientRecord = getRecord(id)
val newId = getAndPossiblyMerge(existing.serviceId, e164)
Log.w(TAG, "[setPhoneNumber] Resulting id: $newId")
db.setTransactionSuccessful()
newId != existing.id
} finally {
db.endTransaction()
}
}
private fun removePhoneNumber(recipientId: RecipientId) {
val values = ContentValues().apply {
putNull(PHONE)
putNull(PNI_COLUMN)
}
if (update(recipientId, values)) {
rotateStorageId(recipientId)
}
}
/**
* Should only use if you are confident that this will not result in any contact merging.
*/
@Throws(SQLiteConstraintException::class)
fun setPhoneNumberOrThrow(id: RecipientId, e164: String) {
val contentValues = ContentValues(1).apply {
put(PHONE, e164)
}
if (update(id, contentValues)) {
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
@Throws(SQLiteConstraintException::class)
fun setPhoneNumberOrThrowSilent(id: RecipientId, e164: String) {
val contentValues = ContentValues(1).apply {
put(PHONE, e164)
}
if (update(id, contentValues)) {
rotateStorageId(id)
}
}
/**
* Associates the provided IDs together. The assumption here is that all of the IDs correspond to the local user and have been verified.
*/
fun linkIdsForSelf(aci: ACI, pni: PNI, e164: String) {
getAndPossiblyMerge(serviceId = aci, pni = pni, e164 = e164, changeSelf = true, pniVerified = true)
}
/**
* Does *not* handle clearing the recipient cache. It is assumed the caller handles this.
*/
fun updateSelfPhone(e164: String, pni: PNI) {
val db = writableDatabase
db.beginTransaction()
try {
val id = Recipient.self().id
val newId = getAndPossiblyMerge(serviceId = SignalStore.account().requireAci(), pni = pni, e164 = e164, pniVerified = true, changeSelf = true)
if (id == newId) {
Log.i(TAG, "[updateSelfPhone] Phone updated for self")
} else {
throw AssertionError("[updateSelfPhone] Self recipient id changed when updating phone. old: $id new: $newId")
}
db
.update(TABLE_NAME)
.values(NEEDS_PNI_SIGNATURE to 0)
.run()
SignalDatabase.pendingPniSignatureMessages.deleteAll()
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
fun setUsername(id: RecipientId, username: String?) {
if (username != null) {
val existingUsername = getByUsername(username)
if (existingUsername.isPresent && id != existingUsername.get()) {
Log.i(TAG, "Username was previously thought to be owned by " + existingUsername.get() + ". Clearing their username.")
setUsername(existingUsername.get(), null)
}
}
val contentValues = ContentValues(1).apply {
put(USERNAME, username)
}
if (update(id, contentValues)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
fun setHideStory(id: RecipientId, hideStory: Boolean) {
updateExtras(id) { it.setHideStory(hideStory) }
StorageSyncHelper.scheduleSyncForDataChange()
}
fun updateLastStoryViewTimestamp(id: RecipientId) {
updateExtras(id) { it.setLastStoryView(System.currentTimeMillis()) }
}
fun clearUsernameIfExists(username: String) {
val existingUsername = getByUsername(username)
if (existingUsername.isPresent) {
setUsername(existingUsername.get(), null)
}
}
fun getAllE164s(): Set<String> {
val results: MutableSet<String> = HashSet()
readableDatabase.query(TABLE_NAME, arrayOf(PHONE), null, null, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val number = cursor.getString(cursor.getColumnIndexOrThrow(PHONE))
if (!TextUtils.isEmpty(number)) {
results.add(number)
}
}
}
return results
}
/**
* Gives you all of the recipientIds of possibly-registered users (i.e. REGISTERED or UNKNOWN) that can be found by the set of
* provided E164s.
*/
fun getAllPossiblyRegisteredByE164(e164s: Set<String>): Set<RecipientId> {
val results: MutableSet<RecipientId> = mutableSetOf()
val queries: List<SqlUtil.Query> = SqlUtil.buildCollectionQuery(PHONE, e164s)
for (query in queries) {
readableDatabase.query(TABLE_NAME, arrayOf(ID, REGISTERED), query.where, query.whereArgs, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
if (RegisteredState.fromId(cursor.requireInt(REGISTERED)) != RegisteredState.NOT_REGISTERED) {
results += RecipientId.from(cursor.requireLong(ID))
}
}
}
}
return results
}
fun setPni(id: RecipientId, pni: PNI) {
writableDatabase
.update(TABLE_NAME)
.values(SERVICE_ID to pni.toString())
.where("$ID = ? AND ($SERVICE_ID IS NULL OR $SERVICE_ID = $PNI_COLUMN)", id)
.run()
writableDatabase
.update(TABLE_NAME)
.values(PNI_COLUMN to pni.toString())
.where("$ID = ?", id)
.run()
}
/**
* @return True if setting the UUID resulted in changed recipientId, otherwise false.
*/
fun markRegistered(id: RecipientId, serviceId: ServiceId): Boolean {
val db = writableDatabase
db.beginTransaction()
try {
markRegisteredOrThrow(id, serviceId)
db.setTransactionSuccessful()
return false
} catch (e: SQLiteConstraintException) {
Log.w(TAG, "[markRegistered] Hit a conflict when trying to update $id. Possibly merging.")
val existing = getRecord(id)
val newId = getAndPossiblyMerge(serviceId, existing.e164)
Log.w(TAG, "[markRegistered] Merged into $newId")
db.setTransactionSuccessful()
return newId != existing.id
} finally {
db.endTransaction()
}
}
/**
* Should only use if you are confident that this shouldn't result in any contact merging.
*/
fun markRegisteredOrThrow(id: RecipientId, serviceId: ServiceId) {
val contentValues = contentValuesOf(
REGISTERED to RegisteredState.REGISTERED.id,
SERVICE_ID to serviceId.toString().lowercase(),
UNREGISTERED_TIMESTAMP to 0
)
if (update(id, contentValues)) {
setStorageIdIfNotSet(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun markUnregistered(id: RecipientId) {
val contentValues = contentValuesOf(
REGISTERED to RegisteredState.NOT_REGISTERED.id,
STORAGE_SERVICE_ID to null,
UNREGISTERED_TIMESTAMP to System.currentTimeMillis()
)
if (update(id, contentValues)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun bulkUpdatedRegisteredStatus(registered: Map<RecipientId, ServiceId?>, unregistered: Collection<RecipientId>) {
writableDatabase.withinTransaction { db ->
val registeredWithServiceId: Set<RecipientId> = getRegisteredWithServiceIds()
val needsMarkRegistered: Map<RecipientId, ServiceId?> = registered - registeredWithServiceId
for ((recipientId, serviceId) in needsMarkRegistered) {
val values = ContentValues().apply {
put(REGISTERED, RegisteredState.REGISTERED.id)
put(UNREGISTERED_TIMESTAMP, 0)
if (serviceId != null) {
put(SERVICE_ID, serviceId.toString().lowercase())
}
}
try {
if (update(recipientId, values)) {
setStorageIdIfNotSet(recipientId)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
}
} catch (e: SQLiteConstraintException) {
Log.w(TAG, "[bulkUpdateRegisteredStatus] Hit a conflict when trying to update $recipientId. Possibly merging.")
val e164 = getRecord(recipientId).e164
val newId = getAndPossiblyMerge(serviceId, e164)
Log.w(TAG, "[bulkUpdateRegisteredStatus] Merged into $newId")
}
}
for (id in unregistered) {
val values = contentValuesOf(
REGISTERED to RegisteredState.NOT_REGISTERED.id,
UNREGISTERED_TIMESTAMP to System.currentTimeMillis()
)
if (update(id, values)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
}
}
/**
* Handles inserts the (e164, UUID) pairs, which could result in merges. Does not mark users as
* registered.
*
* @return A mapping of (RecipientId, UUID)
*/
fun bulkProcessCdsResult(mapping: Map<String, ACI?>): Map<RecipientId, ACI?> {
val db = writableDatabase
val aciMap: MutableMap<RecipientId, ACI?> = mutableMapOf()
db.beginTransaction()
try {
for ((e164, aci) in mapping) {
var aciEntry = if (aci != null) getByServiceId(aci) else Optional.empty()
if (aciEntry.isPresent) {
val idChanged = setPhoneNumber(aciEntry.get(), e164)
if (idChanged) {
aciEntry = getByServiceId(aci!!)
}
}
val id = if (aciEntry.isPresent) aciEntry.get() else getOrInsertFromE164(e164)
aciMap[id] = aci
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
return aciMap
}
/**
* Processes CDSv2 results, merging recipients as necessary. Does not mark users as
* registered.
*
* Important: This is under active development and is not suitable for actual use.
*
* @return A set of [RecipientId]s that were updated/inserted.
*/
fun bulkProcessCdsV2Result(mapping: Map<String, CdsV2Result>): Set<RecipientId> {
val ids: MutableSet<RecipientId> = mutableSetOf()
val db = writableDatabase
db.beginTransaction()
try {
for ((e164, result) in mapping) {
ids += processPnpTuple(e164, result.pni, result.aci, false).finalId
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
return ids
}
fun bulkUpdatedRegisteredStatusV2(registered: Set<RecipientId>, unregistered: Collection<RecipientId>) {
writableDatabase.withinTransaction {
val registeredValues = contentValuesOf(
REGISTERED to RegisteredState.REGISTERED.id,
UNREGISTERED_TIMESTAMP to 0
)
for (id in registered) {
if (update(id, registeredValues)) {
setStorageIdIfNotSet(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
val unregisteredValues = contentValuesOf(
REGISTERED to RegisteredState.NOT_REGISTERED.id,
STORAGE_SERVICE_ID to null,
UNREGISTERED_TIMESTAMP to System.currentTimeMillis()
)
for (id in unregistered) {
if (update(id, unregisteredValues)) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
}
}
/**
* Takes a tuple of (e164, pni, aci) and incorporates it into our database.
* It is assumed that we are in a transaction.
*
* @return The [RecipientId] of the resulting recipient.
*/
@VisibleForTesting
fun processPnpTuple(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false): ProcessPnpTupleResult {
val changeSet: PnpChangeSet = processPnpTupleToChangeSet(e164, pni, aci, pniVerified, changeSelf)
val affectedIds: MutableSet<RecipientId> = mutableSetOf()
val oldIds: MutableSet<RecipientId> = mutableSetOf()
var changedNumberId: RecipientId? = null
for (operation in changeSet.operations) {
@Exhaustive
when (operation) {
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, pni)
return ProcessPnpTupleResult(
finalId = finalId,
affectedIds = affectedIds,
oldIds = oldIds,
changedNumberId = changedNumberId,
operations = changeSet.operations,
breadCrumbs = changeSet.breadCrumbs
)
}
@VisibleForTesting
fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, inputPni: PNI?): RecipientId {
for (operation in changeSet.operations) {
@Exhaustive
when (operation) {
is PnpOperation.RemoveE164 -> {
writableDatabase
.update(TABLE_NAME)
.values(PHONE to null)
.where("$ID = ?", operation.recipientId)
.run()
}
is PnpOperation.RemovePni -> {
writableDatabase
.update(TABLE_NAME)
.values(SERVICE_ID to null)
.where("$ID = ? AND $SERVICE_ID NOT NULL AND $SERVICE_ID = $PNI_COLUMN", operation.recipientId)
.run()
writableDatabase
.update(TABLE_NAME)
.values(PNI_COLUMN to null)
.where("$ID = ?", operation.recipientId)
.run()
}
is PnpOperation.SetAci -> {
writableDatabase
.update(TABLE_NAME)
.values(
SERVICE_ID to operation.aci.toString(),
REGISTERED to RegisteredState.REGISTERED.id,
UNREGISTERED_TIMESTAMP to 0
)
.where("$ID = ?", operation.recipientId)
.run()
}
is PnpOperation.SetE164 -> {
writableDatabase
.update(TABLE_NAME)
.values(PHONE to operation.e164)
.where("$ID = ?", operation.recipientId)
.run()
}
is PnpOperation.SetPni -> {
writableDatabase
.update(TABLE_NAME)
.values(SERVICE_ID to operation.pni.toString())
.where("$ID = ? AND ($SERVICE_ID IS NULL OR $SERVICE_ID = $PNI_COLUMN)", operation.recipientId)
.run()
writableDatabase
.update(TABLE_NAME)
.values(
PNI_COLUMN to operation.pni.toString(),
REGISTERED to RegisteredState.REGISTERED.id,
UNREGISTERED_TIMESTAMP to 0
)
.where("$ID = ?", operation.recipientId)
.run()
}
is PnpOperation.Merge -> {
merge(operation.primaryId, operation.secondaryId, inputPni)
}
is PnpOperation.SessionSwitchoverInsert -> {
// TODO [pnp]
Log.w(TAG, "Session switchover events aren't implemented yet!")
}
is PnpOperation.ChangeNumberInsert -> {
// TODO [pnp]
Log.w(TAG, "Change number inserts aren't implemented yet!")
}
}
}
return when (changeSet.id) {
is PnpIdResolver.PnpNoopId -> {
changeSet.id.recipientId
}
is PnpIdResolver.PnpInsert -> {
val id: Long = writableDatabase.insert(TABLE_NAME, null, buildContentValuesForPnpInsert(changeSet.id.e164, changeSet.id.pni, changeSet.id.aci))
RecipientId.from(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, changeSelf: Boolean = false): PnpChangeSet {
check(e164 != null || pni != null || aci != null) { "Must provide at least one field!" }
val breadCrumbs: MutableList<String> = mutableListOf()
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: 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) {
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) {
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
)
}
// 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.
breadCrumbs += "NeedsMerge"
val fullData = partialData.copy(
e164Record = partialData.byE164?.let { getRecord(it) },
pniSidRecord = partialData.byPniSid?.let { getRecord(it) },
aciSidRecord = partialData.byAciSid?.let { getRecord(it) },
)
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, 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 || notSelf(e164, pni, aci))) {
breadCrumbs += "FinalUpdateE164"
operations += PnpOperation.SetE164(
recipientId = primaryId,
e164 = e164
)
}
if (finalData.byPniSid == null && finalData.byPniOnly == null && pni != null) {
breadCrumbs += "FinalUpdatePni"
operations += PnpOperation.SetPni(
recipientId = primaryId,
pni = pni
)
}
return PnpChangeSet(
id = PnpIdResolver.PnpNoopId(primaryId),
operations = operations,
breadCrumbs = breadCrumbs
)
}
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)
}
operations += PnpOperation.Merge(
primaryId = data.byE164,
secondaryId = data.byPniSid
)
// TODO: Possible session switchover?
} else {
check(!data.pniSidRecord.pniAndAci() && data.pniSidRecord.e164 != null)
breadCrumbs += "PniSidRecordHasE164"
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, 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)
}
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.
breadCrumbs += "PniSidRecordHasMatchingE164"
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 {
check(data.pniSidRecord.e164 != null && data.pniSidRecord.e164 != e164)
breadCrumbs += "PniSidRecordHasNonMatchingE164"
operations += PnpOperation.RemovePni(data.byPniSid)
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, 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 E164 record only has a single identifier. We know we must merge.
if (data.e164Record.e164Only()) {
breadCrumbs += "E164Only"
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.
breadCrumbs += "E164RecordHasMatchingPni"
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 {
check(data.e164Record.pni == null || data.e164Record.pni != pni)
breadCrumbs += "E164RecordHasNonMatchingPni"
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> {
val results: MutableList<RecipientId> = LinkedList()
val args = arrayOf((System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31)).toString())
readableDatabase.rawQuery(INSIGHTS_INVITEE_LIST, args).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))))
}
}
return results
}
fun getRegistered(): List<RecipientId> {
val results: MutableList<RecipientId> = LinkedList()
readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$REGISTERED = ? and $HIDDEN = ?", arrayOf("1", "0"), null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))))
}
}
return results
}
fun getRegisteredWithServiceIds(): Set<RecipientId> {
return readableDatabase
.select(ID)
.from(TABLE_NAME)
.where("$REGISTERED = ? and $HIDDEN = ? AND $SERVICE_ID NOT NULL", 1, 0)
.run()
.readToSet { cursor ->
RecipientId.from(cursor.requireLong(ID))
}
}
fun getSystemContacts(): List<RecipientId> {
val results: MutableList<RecipientId> = LinkedList()
readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$SYSTEM_JOINED_NAME IS NOT NULL AND $SYSTEM_JOINED_NAME != \"\"", null, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))))
}
}
return results
}
fun getRegisteredE164s(): Set<String> {
return readableDatabase
.select(PHONE)
.from(TABLE_NAME)
.where("$REGISTERED = ? and $HIDDEN = ? AND $PHONE NOT NULL", 1, 0)
.run()
.readToSet { cursor ->
cursor.requireNonNullString(PHONE)
}
}
/**
* We no longer automatically generate a chat color. This method is used only
* in the case of a legacy migration and otherwise should not be called.
*/
@Deprecated("")
fun updateSystemContactColors() {
val db = readableDatabase
val updates: MutableMap<RecipientId, ChatColors> = HashMap()
db.beginTransaction()
try {
db.query(TABLE_NAME, arrayOf(ID, "color", CHAT_COLORS, CUSTOM_CHAT_COLORS_ID, SYSTEM_JOINED_NAME), "$SYSTEM_JOINED_NAME IS NOT NULL AND $SYSTEM_JOINED_NAME != \"\"", null, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
val id = cursor.requireLong(ID)
val serializedColor = cursor.requireString("color")
val customChatColorsId = cursor.requireLong(CUSTOM_CHAT_COLORS_ID)
val serializedChatColors = cursor.requireBlob(CHAT_COLORS)
var chatColors: ChatColors? = if (serializedChatColors != null) {
try {
forChatColor(forLongValue(customChatColorsId), ChatColor.parseFrom(serializedChatColors))
} catch (e: InvalidProtocolBufferException) {
null
}
} else {
null
}
if (chatColors != null) {
return
}
chatColors = if (serializedColor != null) {
try {
getChatColors(MaterialColor.fromSerialized(serializedColor))
} catch (e: UnknownColorException) {
return
}
} else {
return
}
val contentValues = ContentValues().apply {
put(CHAT_COLORS, chatColors.serialize().toByteArray())
put(CUSTOM_CHAT_COLORS_ID, chatColors.id.longValue)
}
db.update(TABLE_NAME, contentValues, "$ID = ?", arrayOf(id.toString()))
updates[RecipientId.from(id)] = chatColors
}
}
} finally {
db.setTransactionSuccessful()
db.endTransaction()
updates.entries.forEach { ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(it.key) }
}
}
fun getSignalContacts(includeSelf: Boolean): Cursor? {
return getSignalContacts(includeSelf, "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $USERNAME, $PHONE")
}
fun getSignalContactsCount(includeSelf: Boolean): Int {
return getSignalContacts(includeSelf)?.count ?: 0
}
fun getSignalContacts(includeSelf: Boolean, orderBy: String? = null): Cursor? {
val searchSelection = ContactSearchSelection.Builder()
.withRegistered(true)
.withGroups(false)
.excludeId(if (includeSelf) null else Recipient.self().id)
.build()
val selection = searchSelection.where
val args = searchSelection.args
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
}
fun querySignalContacts(inputQuery: String, includeSelf: Boolean): Cursor? {
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
val searchSelection = ContactSearchSelection.Builder()
.withRegistered(true)
.withGroups(false)
.excludeId(if (includeSelf) null else Recipient.self().id)
.withSearchQuery(query)
.build()
val selection = searchSelection.where
val args = searchSelection.args
val orderBy = "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $PHONE"
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
}
fun querySignalContactLetterHeaders(inputQuery: String, includeSelf: Boolean): Map<RecipientId, String> {
val searchSelection = ContactSearchSelection.Builder()
.withRegistered(true)
.withGroups(false)
.excludeId(if (includeSelf) null else Recipient.self().id)
.withSearchQuery(inputQuery)
.build()
return readableDatabase.query(
"""
SELECT
_id,
UPPER(SUBSTR($SORT_NAME, 0, 2)) AS letter_header
FROM (
SELECT ${SEARCH_PROJECTION.joinToString(", ")}
FROM recipient
WHERE ${searchSelection.where}
ORDER BY $SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $PHONE
)
GROUP BY letter_header
""".trimIndent(),
searchSelection.args
).use { cursor ->
if (cursor.count == 0) {
emptyMap()
} else {
val resultsMap = mutableMapOf<RecipientId, String>()
while (cursor.moveToNext()) {
cursor.requireString("letter_header")?.let {
resultsMap[RecipientId.from(cursor.requireLong(ID))] = it
}
}
resultsMap
}
}
}
fun getNonSignalContacts(): Cursor? {
val searchSelection = ContactSearchSelection.Builder().withNonRegistered(true)
.withGroups(false)
.build()
val selection = searchSelection.where
val args = searchSelection.args
val orderBy = "$SYSTEM_JOINED_NAME, $PHONE"
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
}
fun queryNonSignalContacts(inputQuery: String): Cursor? {
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
val searchSelection = ContactSearchSelection.Builder()
.withNonRegistered(true)
.withGroups(false)
.withSearchQuery(query)
.build()
val selection = searchSelection.where
val args = searchSelection.args
val orderBy = "$SYSTEM_JOINED_NAME, $PHONE"
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
}
fun getNonGroupContacts(includeSelf: Boolean): Cursor? {
val searchSelection = ContactSearchSelection.Builder()
.withRegistered(true)
.withNonRegistered(true)
.withGroups(false)
.excludeId(if (includeSelf) null else Recipient.self().id)
.build()
val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + PHONE
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, searchSelection.where, searchSelection.args, null, null, orderBy)
}
fun queryNonGroupContacts(inputQuery: String, includeSelf: Boolean): Cursor? {
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
val searchSelection = ContactSearchSelection.Builder()
.withRegistered(true)
.withNonRegistered(true)
.withGroups(false)
.excludeId(if (includeSelf) null else Recipient.self().id)
.withSearchQuery(query)
.build()
val selection = searchSelection.where
val args = searchSelection.args
val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + PHONE
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
}
fun queryAllContacts(inputQuery: String): Cursor? {
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
val selection =
"""
$BLOCKED = ? AND $HIDDEN = ? AND
(
$SORT_NAME GLOB ? OR
$USERNAME GLOB ? OR
$PHONE GLOB ? OR
$EMAIL GLOB ?
)
""".trimIndent()
val args = SqlUtil.buildArgs(0, 0, query, query, query, query)
return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null)
}
@JvmOverloads
fun queryRecipientsForMentions(inputQuery: String, recipientIds: List<RecipientId>? = null): List<Recipient> {
val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
var ids: String? = null
if (Util.hasItems(recipientIds)) {
ids = TextUtils.join(",", recipientIds?.map { it.serialize() }?.toList() ?: emptyList<String>())
}
val selection = "$BLOCKED = 0 AND ${if (ids != null) "$ID IN ($ids) AND " else ""}$SORT_NAME GLOB ?"
val recipients: MutableList<Recipient> = ArrayList()
RecipientReader(readableDatabase.query(TABLE_NAME, MENTION_SEARCH_PROJECTION, selection, SqlUtil.buildArgs(query), null, null, SORT_NAME)).use { reader ->
var recipient: Recipient? = reader.getNext()
while (recipient != null) {
recipients.add(recipient)
recipient = reader.getNext()
}
}
return recipients
}
fun getRecipientsForMultiDeviceSync(): List<Recipient> {
val subquery = "SELECT ${ThreadDatabase.TABLE_NAME}.${ThreadDatabase.RECIPIENT_ID} FROM ${ThreadDatabase.TABLE_NAME}"
val selection = "$REGISTERED = ? AND $GROUP_ID IS NULL AND $ID != ? AND ($SYSTEM_CONTACT_URI NOT NULL OR $ID IN ($subquery))"
val args = arrayOf(RegisteredState.REGISTERED.id.toString(), Recipient.self().id.serialize())
val recipients: MutableList<Recipient> = ArrayList()
readableDatabase.query(TABLE_NAME, ID_PROJECTION, selection, args, null, null, null).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
recipients.add(Recipient.resolved(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))))
}
}
return recipients
}
/**
* @param lastInteractionThreshold Only include contacts that have been interacted with since this time.
* @param lastProfileFetchThreshold Only include contacts that haven't their profile fetched after this time.
* @param limit Only return at most this many contact.
*/
fun getRecipientsForRoutineProfileFetch(lastInteractionThreshold: Long, lastProfileFetchThreshold: Long, limit: Int): List<RecipientId> {
val threadDatabase = threads
val recipientsWithinInteractionThreshold: MutableSet<RecipientId> = LinkedHashSet()
threadDatabase.readerFor(threadDatabase.getRecentPushConversationList(-1, false)).use { reader ->
var record: ThreadRecord? = reader.next
while (record != null && record.date > lastInteractionThreshold) {
val recipient = Recipient.resolved(record.recipient.id)
if (recipient.isGroup) {
recipientsWithinInteractionThreshold.addAll(recipient.participantIds)
} else {
recipientsWithinInteractionThreshold.add(recipient.id)
}
record = reader.next
}
}
return Recipient.resolvedList(recipientsWithinInteractionThreshold)
.asSequence()
.filterNot { it.isSelf }
.filter { it.lastProfileFetchTime < lastProfileFetchThreshold }
.take(limit)
.map { it.id }
.toMutableList()
}
fun markProfilesFetched(ids: Collection<RecipientId>, time: Long) {
val db = writableDatabase
db.beginTransaction()
try {
val values = ContentValues(1).apply {
put(LAST_PROFILE_FETCH, time)
}
for (id in ids) {
db.update(TABLE_NAME, values, ID_WHERE, arrayOf(id.serialize()))
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
fun applyBlockedUpdate(blocked: List<SignalServiceAddress>, groupIds: List<ByteArray?>) {
val blockedE164 = blocked
.filter { b: SignalServiceAddress -> b.number.isPresent }
.map { b: SignalServiceAddress -> b.number.get() }
.toList()
val blockedUuid = blocked
.map { b: SignalServiceAddress -> b.serviceId.toString().lowercase() }
.toList()
val db = writableDatabase
db.beginTransaction()
try {
val resetBlocked = ContentValues().apply {
put(BLOCKED, 0)
}
db.update(TABLE_NAME, resetBlocked, null, null)
val setBlocked = ContentValues().apply {
put(BLOCKED, 1)
put(PROFILE_SHARING, 0)
}
for (e164 in blockedE164) {
db.update(TABLE_NAME, setBlocked, "$PHONE = ?", arrayOf(e164))
}
for (uuid in blockedUuid) {
db.update(TABLE_NAME, setBlocked, "$SERVICE_ID = ?", arrayOf(uuid))
}
val groupIdStrings: MutableList<V1> = ArrayList(groupIds.size)
for (raw in groupIds) {
try {
groupIdStrings.add(GroupId.v1(raw))
} catch (e: BadGroupIdException) {
Log.w(TAG, "[applyBlockedUpdate] Bad GV1 ID!")
}
}
for (groupId in groupIdStrings) {
db.update(TABLE_NAME, setBlocked, "$GROUP_ID = ?", arrayOf(groupId.toString()))
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
ApplicationDependencies.getRecipientCache().clear()
}
fun updateStorageId(recipientId: RecipientId, id: ByteArray?) {
updateStorageIds(Collections.singletonMap(recipientId, id))
}
private fun updateStorageIds(ids: Map<RecipientId, ByteArray?>) {
val db = writableDatabase
db.beginTransaction()
try {
for ((key, value) in ids) {
val values = ContentValues().apply {
put(STORAGE_SERVICE_ID, Base64.encodeBytes(value!!))
}
db.update(TABLE_NAME, values, ID_WHERE, arrayOf(key.serialize()))
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
for (id in ids.keys) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
fun markPreMessageRequestRecipientsAsProfileSharingEnabled(messageRequestEnableTime: Long) {
val whereArgs = SqlUtil.buildArgs(messageRequestEnableTime, messageRequestEnableTime)
val select =
"""
SELECT r.$ID FROM $TABLE_NAME AS r
INNER JOIN ${ThreadDatabase.TABLE_NAME} AS t ON t.${ThreadDatabase.RECIPIENT_ID} = r.$ID
WHERE
r.$PROFILE_SHARING = 0 AND (
EXISTS(SELECT 1 FROM ${SmsDatabase.TABLE_NAME} WHERE ${SmsDatabase.THREAD_ID} = t.${ThreadDatabase.ID} AND ${SmsDatabase.DATE_RECEIVED} < ?) OR
EXISTS(SELECT 1 FROM ${MmsDatabase.TABLE_NAME} WHERE ${MmsDatabase.THREAD_ID} = t.${ThreadDatabase.ID} AND ${MmsDatabase.DATE_RECEIVED} < ?)
)
""".trimIndent()
val idsToUpdate: MutableList<Long> = ArrayList()
readableDatabase.rawQuery(select, whereArgs).use { cursor ->
while (cursor.moveToNext()) {
idsToUpdate.add(cursor.requireLong(ID))
}
}
if (Util.hasItems(idsToUpdate)) {
val query = SqlUtil.buildSingleCollectionQuery(ID, idsToUpdate)
val values = contentValuesOf(
PROFILE_SHARING to 1,
HIDDEN to 0
)
writableDatabase.update(TABLE_NAME, values, query.where, query.whereArgs)
for (id in idsToUpdate) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(RecipientId.from(id))
}
}
}
/**
* Indicates that the recipient knows our PNI, and therefore needs to be sent PNI signature messages until we know that they have our PNI-ACI association.
*/
fun markNeedsPniSignature(recipientId: RecipientId) {
if (update(recipientId, contentValuesOf(NEEDS_PNI_SIGNATURE to 1))) {
Log.i(TAG, "Marked $recipientId as needing a PNI signature message.")
Recipient.live(recipientId).refresh()
}
}
/**
* Indicates that we successfully told all of this recipient's devices our PNI-ACI association, and therefore no longer needs us to send it to them.
*/
fun clearNeedsPniSignature(recipientId: RecipientId) {
if (update(recipientId, contentValuesOf(NEEDS_PNI_SIGNATURE to 0))) {
Recipient.live(recipientId).refresh()
}
}
fun setHasGroupsInCommon(recipientIds: List<RecipientId?>) {
if (recipientIds.isEmpty()) {
return
}
var query = SqlUtil.buildSingleCollectionQuery(ID, recipientIds)
val db = writableDatabase
db.query(TABLE_NAME, arrayOf(ID), "${query.where} AND $GROUPS_IN_COMMON = 0", query.whereArgs, null, null, null).use { cursor ->
val idsToUpdate: MutableList<Long> = ArrayList(cursor.count)
while (cursor.moveToNext()) {
idsToUpdate.add(cursor.requireLong(ID))
}
if (Util.hasItems(idsToUpdate)) {
query = SqlUtil.buildSingleCollectionQuery(ID, idsToUpdate)
val values = ContentValues().apply {
put(GROUPS_IN_COMMON, 1)
}
val count = db.update(TABLE_NAME, values, query.where, query.whereArgs)
if (count > 0) {
for (id in idsToUpdate) {
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(RecipientId.from(id))
}
}
}
}
}
fun manuallyShowAvatar(recipientId: RecipientId) {
updateExtras(recipientId) { b: RecipientExtras.Builder -> b.setManuallyShownAvatar(true) }
}
fun getCapabilities(id: RecipientId): RecipientRecord.Capabilities? {
readableDatabase
.select(CAPABILITIES)
.from(TABLE_NAME)
.where("$ID = ?", id)
.run()
.use { cursor ->
return if (cursor.moveToFirst()) {
readCapabilities(cursor)
} else {
null
}
}
}
private fun updateExtras(recipientId: RecipientId, updater: java.util.function.Function<RecipientExtras.Builder, RecipientExtras.Builder>) {
val db = writableDatabase
db.beginTransaction()
try {
db.query(TABLE_NAME, arrayOf(ID, EXTRAS), ID_WHERE, SqlUtil.buildArgs(recipientId), null, null, null).use { cursor ->
if (cursor.moveToNext()) {
val state = getRecipientExtras(cursor)
val builder = if (state != null) state.toBuilder() else RecipientExtras.newBuilder()
val updatedState = updater.apply(builder).build().toByteArray()
val values = ContentValues(1).apply {
put(EXTRAS, updatedState)
}
db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(cursor.requireLong(ID)))
}
}
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
}
/**
* Does not trigger any recipient refreshes -- it is assumed the caller handles this.
* Will *not* give storageIds to those that shouldn't get them (e.g. MMS groups, unregistered
* users).
*/
fun rotateStorageId(recipientId: RecipientId) {
val values = ContentValues(1).apply {
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
}
val query = "$ID = ? AND ($GROUP_TYPE IN (?, ?, ?) OR $REGISTERED = ?)"
val args = SqlUtil.buildArgs(recipientId, GroupType.SIGNAL_V1.id, GroupType.SIGNAL_V2.id, GroupType.DISTRIBUTION_LIST.id, RegisteredState.REGISTERED.id)
writableDatabase.update(TABLE_NAME, values, query, args)
}
/**
* Does not trigger any recipient refreshes -- it is assumed the caller handles this.
*/
fun setStorageIdIfNotSet(recipientId: RecipientId) {
val values = ContentValues(1).apply {
put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
}
val query = "$ID = ? AND $STORAGE_SERVICE_ID IS NULL"
val args = SqlUtil.buildArgs(recipientId)
writableDatabase.update(TABLE_NAME, values, query, args)
}
/**
* Updates a group recipient with a new V2 group ID. Should only be done as a part of GV1->GV2
* migration.
*/
fun updateGroupId(v1Id: V1, v2Id: V2) {
val values = ContentValues().apply {
put(GROUP_ID, v2Id.toString())
put(GROUP_TYPE, GroupType.SIGNAL_V2.id)
}
val query = SqlUtil.buildTrueUpdateQuery("$GROUP_ID = ?", SqlUtil.buildArgs(v1Id), values)
if (update(query, values)) {
val id = getByGroupId(v2Id).get()
rotateStorageId(id)
ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
}
}
/**
* Will update the database with the content values you specified. It will make an intelligent
* query such that this will only return true if a row was *actually* updated.
*/
private fun update(id: RecipientId, contentValues: ContentValues): Boolean {
val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(id), contentValues)
return update(updateQuery, contentValues)
}
/**
* Will update the database with the {@param contentValues} you specified.
*
*
* This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}.
*/
private fun update(updateQuery: SqlUtil.Query, contentValues: ContentValues): Boolean {
return writableDatabase.update(TABLE_NAME, contentValues, updateQuery.where, updateQuery.whereArgs) > 0
}
private fun getByColumn(column: String, value: String): Optional<RecipientId> {
val query = "$column = ?"
val args = arrayOf(value)
readableDatabase.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null).use { cursor ->
return if (cursor != null && cursor.moveToFirst()) {
Optional.of(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))))
} else {
Optional.empty()
}
}
}
private fun getOrInsertByColumn(column: String, value: String, contentValues: ContentValues = contentValuesOf(column to value)): GetOrInsertResult {
if (TextUtils.isEmpty(value)) {
throw AssertionError("$column cannot be empty.")
}
var existing = getByColumn(column, value)
if (existing.isPresent) {
return GetOrInsertResult(existing.get(), false)
} else {
val id = writableDatabase.insert(TABLE_NAME, null, contentValues)
if (id < 0) {
existing = getByColumn(column, value)
if (existing.isPresent) {
return GetOrInsertResult(existing.get(), false)
} else {
throw AssertionError("Failed to insert recipient!")
}
} else {
return GetOrInsertResult(RecipientId.from(id), true)
}
}
}
/**
* 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 {
ensureInTransaction()
val db = writableDatabase
val primaryRecord = getRecord(primaryId)
val secondaryRecord = getRecord(secondaryId)
// Clean up any E164-based identities (legacy stuff)
if (secondaryRecord.e164 != null) {
ApplicationDependencies.getProtocolStore().aci().identities().delete(secondaryRecord.e164)
}
// Threads
val threadMerge = threads.merge(primaryId, secondaryId)
threads.setLastScrolled(threadMerge.threadId, 0)
threads.update(threadMerge.threadId, false, false)
// Recipient remaps
for (table in recipientIdDatabaseTables) {
table.remapRecipient(secondaryId, primaryId)
}
// Thread remaps
if (threadMerge.neededMerge) {
for (table in threadIdDatabaseTables) {
table.remapThread(threadMerge.previousThreadId, threadMerge.threadId)
}
// Thread Merge Event
val mergeEvent: ThreadMergeEvent.Builder = ThreadMergeEvent.newBuilder()
if (secondaryRecord.e164 != null) {
mergeEvent.previousE164 = secondaryRecord.e164
}
SignalDatabase.sms.insertThreadMergeEvent(primaryRecord.id, threadMerge.threadId, mergeEvent.build())
}
// Recipient
Log.w(TAG, "Deleting recipient $secondaryId", true)
db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(secondaryId))
RemappedRecords.getInstance().addRecipient(secondaryId, primaryId)
val uuidValues = contentValuesOf(
PHONE to (secondaryRecord.e164 ?: primaryRecord.e164),
SERVICE_ID to (primaryRecord.serviceId ?: secondaryRecord.serviceId)?.toString(),
PNI_COLUMN to (newPni ?: secondaryRecord.pni ?: primaryRecord.pni)?.toString(),
BLOCKED to (secondaryRecord.isBlocked || primaryRecord.isBlocked),
MESSAGE_RINGTONE to Optional.ofNullable(primaryRecord.messageRingtone).or(Optional.ofNullable(secondaryRecord.messageRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null),
MESSAGE_VIBRATE to if (primaryRecord.messageVibrateState != VibrateState.DEFAULT) primaryRecord.messageVibrateState.id else secondaryRecord.messageVibrateState.id,
CALL_RINGTONE to Optional.ofNullable(primaryRecord.callRingtone).or(Optional.ofNullable(secondaryRecord.callRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null),
CALL_VIBRATE to if (primaryRecord.callVibrateState != VibrateState.DEFAULT) primaryRecord.callVibrateState.id else secondaryRecord.callVibrateState.id,
NOTIFICATION_CHANNEL to (primaryRecord.notificationChannel ?: secondaryRecord.notificationChannel),
MUTE_UNTIL to if (primaryRecord.muteUntil > 0) primaryRecord.muteUntil else secondaryRecord.muteUntil,
CHAT_COLORS to Optional.ofNullable(primaryRecord.chatColors).or(Optional.ofNullable(secondaryRecord.chatColors)).map { colors: ChatColors? -> colors!!.serialize().toByteArray() }.orElse(null),
AVATAR_COLOR to primaryRecord.avatarColor.serialize(),
CUSTOM_CHAT_COLORS_ID to Optional.ofNullable(primaryRecord.chatColors).or(Optional.ofNullable(secondaryRecord.chatColors)).map { colors: ChatColors? -> colors!!.id.longValue }.orElse(null),
SEEN_INVITE_REMINDER to secondaryRecord.insightsBannerTier.id,
DEFAULT_SUBSCRIPTION_ID to secondaryRecord.getDefaultSubscriptionId().orElse(-1),
MESSAGE_EXPIRATION_TIME to if (primaryRecord.expireMessages > 0) primaryRecord.expireMessages else secondaryRecord.expireMessages,
REGISTERED to RegisteredState.REGISTERED.id,
SYSTEM_GIVEN_NAME to secondaryRecord.systemProfileName.givenName,
SYSTEM_FAMILY_NAME to secondaryRecord.systemProfileName.familyName,
SYSTEM_JOINED_NAME to secondaryRecord.systemProfileName.toString(),
SYSTEM_PHOTO_URI to secondaryRecord.systemContactPhotoUri,
SYSTEM_PHONE_LABEL to secondaryRecord.systemPhoneLabel,
SYSTEM_CONTACT_URI to secondaryRecord.systemContactUri,
PROFILE_SHARING to (primaryRecord.profileSharing || secondaryRecord.profileSharing),
CAPABILITIES to max(primaryRecord.capabilities.rawBits, secondaryRecord.capabilities.rawBits),
MENTION_SETTING to if (primaryRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) primaryRecord.mentionSetting.id else secondaryRecord.mentionSetting.id
)
if (primaryRecord.profileSharing || secondaryRecord.profileSharing) {
uuidValues.put(HIDDEN, 0)
}
if (primaryRecord.profileKey != null) {
updateProfileValuesForMerge(uuidValues, primaryRecord)
} else if (secondaryRecord.profileKey != null) {
updateProfileValuesForMerge(uuidValues, secondaryRecord)
}
db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(primaryId))
return primaryId
}
private fun ensureInTransaction() {
check(writableDatabase.inTransaction()) { "Must be in a transaction!" }
}
private fun buildContentValuesForNewUser(e164: String?, serviceId: ServiceId?): ContentValues {
val values = ContentValues()
values.put(PHONE, e164)
if (serviceId != null) {
values.put(SERVICE_ID, serviceId.toString().lowercase())
values.put(REGISTERED, RegisteredState.REGISTERED.id)
values.put(UNREGISTERED_TIMESTAMP, 0)
values.put(STORAGE_SERVICE_ID, Base64.encodeBytes(StorageSyncHelper.generateKey()))
values.put(AVATAR_COLOR, AvatarColor.random().serialize())
}
return values
}
private fun buildContentValuesForPnpInsert(e164: String?, pni: PNI?, aci: ACI?): ContentValues {
check(e164 != null || pni != null || aci != null) { "Must provide some sort of identifier!" }
val values = contentValuesOf(
PHONE to e164,
SERVICE_ID to (aci ?: pni)?.toString(),
PNI_COLUMN to pni?.toString(),
STORAGE_SERVICE_ID to Base64.encodeBytes(StorageSyncHelper.generateKey()),
AVATAR_COLOR to AvatarColor.random().serialize()
)
if (pni != null || aci != null) {
values.put(REGISTERED, RegisteredState.REGISTERED.id)
values.put(UNREGISTERED_TIMESTAMP, 0)
}
return values
}
private fun getValuesForStorageContact(contact: SignalContactRecord, isInsert: Boolean): ContentValues {
return ContentValues().apply {
val profileName = ProfileName.fromParts(contact.profileGivenName.orElse(null), contact.profileFamilyName.orElse(null))
val systemName = ProfileName.fromParts(contact.systemGivenName.orElse(null), contact.systemFamilyName.orElse(null))
val username = contact.username.orElse(null)
if (contact.serviceId.isValid) {
put(SERVICE_ID, contact.serviceId.toString())
}
if (FeatureFlags.phoneNumberPrivacy()) {
put(PNI_COLUMN, contact.pni.toString())
}
put(PHONE, contact.number.orElse(null))
put(PROFILE_GIVEN_NAME, profileName.givenName)
put(PROFILE_FAMILY_NAME, profileName.familyName)
put(PROFILE_JOINED_NAME, profileName.toString())
put(SYSTEM_GIVEN_NAME, systemName.givenName)
put(SYSTEM_FAMILY_NAME, systemName.familyName)
put(SYSTEM_JOINED_NAME, systemName.toString())
put(PROFILE_KEY, contact.profileKey.map { source -> Base64.encodeBytes(source) }.orElse(null))
put(USERNAME, if (TextUtils.isEmpty(username)) null else username)
put(PROFILE_SHARING, if (contact.isProfileSharingEnabled) "1" else "0")
put(BLOCKED, if (contact.isBlocked) "1" else "0")
put(MUTE_UNTIL, contact.muteUntil)
put(STORAGE_SERVICE_ID, Base64.encodeBytes(contact.id.raw))
put(HIDDEN, contact.isHidden)
if (contact.hasUnknownFields()) {
put(STORAGE_PROTO, Base64.encodeBytes(Objects.requireNonNull(contact.serializeUnknownFields())))
} else {
putNull(STORAGE_PROTO)
}
put(UNREGISTERED_TIMESTAMP, contact.unregisteredTimestamp)
if (contact.unregisteredTimestamp > 0L) {
put(REGISTERED, RegisteredState.NOT_REGISTERED.id)
} else if (contact.serviceId.isValid) {
put(REGISTERED, RegisteredState.REGISTERED.id)
} else {
Log.w(TAG, "Contact is marked as registered, but has no serviceId! Can't locally mark registered. (Phone: ${contact.number.orElse("null")}, Username: ${username?.isNotEmpty()})")
}
if (isInsert) {
put(AVATAR_COLOR, AvatarColor.random().serialize())
}
}
}
private fun getValuesForStorageGroupV1(groupV1: SignalGroupV1Record, isInsert: Boolean): ContentValues {
return ContentValues().apply {
put(GROUP_ID, GroupId.v1orThrow(groupV1.groupId).toString())
put(GROUP_TYPE, GroupType.SIGNAL_V1.id)
put(PROFILE_SHARING, if (groupV1.isProfileSharingEnabled) "1" else "0")
put(BLOCKED, if (groupV1.isBlocked) "1" else "0")
put(MUTE_UNTIL, groupV1.muteUntil)
put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV1.id.raw))
if (groupV1.hasUnknownFields()) {
put(STORAGE_PROTO, Base64.encodeBytes(groupV1.serializeUnknownFields()))
} else {
putNull(STORAGE_PROTO)
}
if (isInsert) {
put(AVATAR_COLOR, AvatarColor.random().serialize())
}
}
}
private fun getValuesForStorageGroupV2(groupV2: SignalGroupV2Record, isInsert: Boolean): ContentValues {
return ContentValues().apply {
put(GROUP_ID, GroupId.v2(groupV2.masterKeyOrThrow).toString())
put(GROUP_TYPE, GroupType.SIGNAL_V2.id)
put(PROFILE_SHARING, if (groupV2.isProfileSharingEnabled) "1" else "0")
put(BLOCKED, if (groupV2.isBlocked) "1" else "0")
put(MUTE_UNTIL, groupV2.muteUntil)
put(STORAGE_SERVICE_ID, Base64.encodeBytes(groupV2.id.raw))
put(MENTION_SETTING, if (groupV2.notifyForMentionsWhenMuted()) MentionSetting.ALWAYS_NOTIFY.id else MentionSetting.DO_NOT_NOTIFY.id)
if (groupV2.hasUnknownFields()) {
put(STORAGE_PROTO, Base64.encodeBytes(groupV2.serializeUnknownFields()))
} else {
putNull(STORAGE_PROTO)
}
if (isInsert) {
put(AVATAR_COLOR, AvatarColor.random().serialize())
}
}
}
/**
* Should only be used for debugging! A very destructive action that clears all known serviceIds from people with phone numbers (so that we could eventually
* get them back through CDS).
*/
fun debugClearServiceIds(recipientId: RecipientId? = null) {
writableDatabase
.update(TABLE_NAME)
.values(
SERVICE_ID to null,
PNI_COLUMN to null
)
.run {
if (recipientId == null) {
where("$ID != ? AND $PHONE NOT NULL", Recipient.self().id)
} else {
where("$ID = ? AND $PHONE NOT NULL", recipientId)
}
}
.run()
ApplicationDependencies.getRecipientCache().clear()
RecipientId.clearCache()
}
/**
* Should only be used for debugging! A very destructive action that clears all known profile keys and credentials.
*/
fun debugClearProfileData(recipientId: RecipientId? = null) {
writableDatabase
.update(TABLE_NAME)
.values(
PROFILE_KEY to null,
EXPIRING_PROFILE_KEY_CREDENTIAL to null,
PROFILE_GIVEN_NAME to null,
PROFILE_FAMILY_NAME to null,
PROFILE_JOINED_NAME to null,
LAST_PROFILE_FETCH to 0,
SIGNAL_PROFILE_AVATAR to null
)
.run {
if (recipientId == null) {
where("$ID != ?", Recipient.self().id)
} else {
where("$ID = ?", recipientId)
}
}
.run()
ApplicationDependencies.getRecipientCache().clear()
RecipientId.clearCache()
}
/**
* Should only be used for debugging! Clears the E164 and PNI from a recipient.
*/
fun debugClearE164AndPni(recipientId: RecipientId) {
writableDatabase
.update(TABLE_NAME)
.values(
PHONE to null,
PNI_COLUMN to null
)
.where(ID_WHERE, recipientId)
.run()
Recipient.live(recipientId).refresh()
}
fun getRecord(context: Context, cursor: Cursor): RecipientRecord {
return getRecord(context, cursor, ID)
}
fun getRecord(context: Context, cursor: Cursor, idColumnName: String): RecipientRecord {
val profileKeyString = cursor.requireString(PROFILE_KEY)
val expiringProfileKeyCredentialString = cursor.requireString(EXPIRING_PROFILE_KEY_CREDENTIAL)
var profileKey: ByteArray? = null
var expiringProfileKeyCredential: ExpiringProfileKeyCredential? = null
if (profileKeyString != null) {
try {
profileKey = Base64.decode(profileKeyString)
} catch (e: IOException) {
Log.w(TAG, e)
}
if (expiringProfileKeyCredentialString != null) {
try {
val columnDataBytes = Base64.decode(expiringProfileKeyCredentialString)
val columnData = ExpiringProfileKeyCredentialColumnData.parseFrom(columnDataBytes)
if (Arrays.equals(columnData.profileKey.toByteArray(), profileKey)) {
expiringProfileKeyCredential = ExpiringProfileKeyCredential(columnData.expiringProfileKeyCredential.toByteArray())
} else {
Log.i(TAG, "Out of date profile key credential data ignored on read")
}
} catch (e: InvalidInputException) {
Log.w(TAG, "Profile key credential column data could not be read", e)
} catch (e: IOException) {
Log.w(TAG, "Profile key credential column data could not be read", e)
}
}
}
val serializedWallpaper = cursor.requireBlob(WALLPAPER)
val chatWallpaper: ChatWallpaper? = if (serializedWallpaper != null) {
try {
ChatWallpaperFactory.create(Wallpaper.parseFrom(serializedWallpaper))
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, "Failed to parse wallpaper.", e)
null
}
} else {
null
}
val customChatColorsId = cursor.requireLong(CUSTOM_CHAT_COLORS_ID)
val serializedChatColors = cursor.requireBlob(CHAT_COLORS)
val chatColors: ChatColors? = if (serializedChatColors != null) {
try {
forChatColor(forLongValue(customChatColorsId), ChatColor.parseFrom(serializedChatColors))
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, "Failed to parse chat colors.", e)
null
}
} else {
null
}
val recipientId = RecipientId.from(cursor.requireLong(idColumnName))
val distributionListId: DistributionListId? = DistributionListId.fromNullable(cursor.requireLong(DISTRIBUTION_LIST_ID))
val avatarColor: AvatarColor = if (distributionListId != null) AvatarColor.UNKNOWN else AvatarColor.deserialize(cursor.requireString(AVATAR_COLOR))
return RecipientRecord(
id = recipientId,
serviceId = ServiceId.parseOrNull(cursor.requireString(SERVICE_ID)),
pni = PNI.parseOrNull(cursor.requireString(PNI_COLUMN)),
username = cursor.requireString(USERNAME),
e164 = cursor.requireString(PHONE),
email = cursor.requireString(EMAIL),
groupId = GroupId.parseNullableOrThrow(cursor.requireString(GROUP_ID)),
distributionListId = distributionListId,
groupType = GroupType.fromId(cursor.requireInt(GROUP_TYPE)),
isBlocked = cursor.requireBoolean(BLOCKED),
muteUntil = cursor.requireLong(MUTE_UNTIL),
messageVibrateState = VibrateState.fromId(cursor.requireInt(MESSAGE_VIBRATE)),
callVibrateState = VibrateState.fromId(cursor.requireInt(CALL_VIBRATE)),
messageRingtone = Util.uri(cursor.requireString(MESSAGE_RINGTONE)),
callRingtone = Util.uri(cursor.requireString(CALL_RINGTONE)),
defaultSubscriptionId = cursor.requireInt(DEFAULT_SUBSCRIPTION_ID),
expireMessages = cursor.requireInt(MESSAGE_EXPIRATION_TIME),
registered = RegisteredState.fromId(cursor.requireInt(REGISTERED)),
profileKey = profileKey,
expiringProfileKeyCredential = expiringProfileKeyCredential,
systemProfileName = ProfileName.fromParts(cursor.requireString(SYSTEM_GIVEN_NAME), cursor.requireString(SYSTEM_FAMILY_NAME)),
systemDisplayName = cursor.requireString(SYSTEM_JOINED_NAME),
systemContactPhotoUri = cursor.requireString(SYSTEM_PHOTO_URI),
systemPhoneLabel = cursor.requireString(SYSTEM_PHONE_LABEL),
systemContactUri = cursor.requireString(SYSTEM_CONTACT_URI),
signalProfileName = ProfileName.fromParts(cursor.requireString(PROFILE_GIVEN_NAME), cursor.requireString(PROFILE_FAMILY_NAME)),
signalProfileAvatar = cursor.requireString(SIGNAL_PROFILE_AVATAR),
profileAvatarFileDetails = AvatarHelper.getAvatarFileDetails(context, recipientId),
profileSharing = cursor.requireBoolean(PROFILE_SHARING),
lastProfileFetch = cursor.requireLong(LAST_PROFILE_FETCH),
notificationChannel = cursor.requireString(NOTIFICATION_CHANNEL),
unidentifiedAccessMode = UnidentifiedAccessMode.fromMode(cursor.requireInt(UNIDENTIFIED_ACCESS_MODE)),
forceSmsSelection = cursor.requireBoolean(FORCE_SMS_SELECTION),
capabilities = readCapabilities(cursor),
insightsBannerTier = InsightsBannerTier.fromId(cursor.requireInt(SEEN_INVITE_REMINDER)),
storageId = Base64.decodeNullableOrThrow(cursor.requireString(STORAGE_SERVICE_ID)),
mentionSetting = MentionSetting.fromId(cursor.requireInt(MENTION_SETTING)),
wallpaper = chatWallpaper,
chatColors = chatColors,
avatarColor = avatarColor,
about = cursor.requireString(ABOUT),
aboutEmoji = cursor.requireString(ABOUT_EMOJI),
syncExtras = getSyncExtras(cursor),
extras = getExtras(cursor),
hasGroupsInCommon = cursor.requireBoolean(GROUPS_IN_COMMON),
badges = parseBadgeList(cursor.requireBlob(BADGES)),
needsPniSignature = cursor.requireBoolean(NEEDS_PNI_SIGNATURE),
isHidden = cursor.requireBoolean(HIDDEN)
)
}
private fun readCapabilities(cursor: Cursor): RecipientRecord.Capabilities {
val capabilities = cursor.requireLong(CAPABILITIES)
return RecipientRecord.Capabilities(
rawBits = capabilities,
groupsV1MigrationCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GROUPS_V1_MIGRATION, Capabilities.BIT_LENGTH).toInt()),
senderKeyCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.SENDER_KEY, Capabilities.BIT_LENGTH).toInt()),
announcementGroupCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.ANNOUNCEMENT_GROUPS, Capabilities.BIT_LENGTH).toInt()),
changeNumberCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.CHANGE_NUMBER, Capabilities.BIT_LENGTH).toInt()),
storiesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.STORIES, Capabilities.BIT_LENGTH).toInt()),
giftBadgesCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.GIFT_BADGES, Capabilities.BIT_LENGTH).toInt()),
pnpCapability = Recipient.Capability.deserialize(Bitmask.read(capabilities, Capabilities.PNP, Capabilities.BIT_LENGTH).toInt()),
)
}
private fun parseBadgeList(serializedBadgeList: ByteArray?): List<Badge> {
var badgeList: BadgeList? = null
if (serializedBadgeList != null) {
try {
badgeList = BadgeList.parseFrom(serializedBadgeList)
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, e)
}
}
val badges: List<Badge>
if (badgeList != null) {
val protoBadges = badgeList.badgesList
badges = ArrayList(protoBadges.size)
for (protoBadge in protoBadges) {
badges.add(Badges.fromDatabaseBadge(protoBadge))
}
} else {
badges = emptyList()
}
return badges
}
private fun getSyncExtras(cursor: Cursor): RecipientRecord.SyncExtras {
val storageProtoRaw = cursor.optionalString(STORAGE_PROTO).orElse(null)
val storageProto = if (storageProtoRaw != null) Base64.decodeOrThrow(storageProtoRaw) else null
val archived = cursor.optionalBoolean(ThreadDatabase.ARCHIVED).orElse(false)
val forcedUnread = cursor.optionalInt(ThreadDatabase.READ).map { status: Int -> status == ThreadDatabase.ReadStatus.FORCED_UNREAD.serialize() }.orElse(false)
val groupMasterKey = cursor.optionalBlob(GroupDatabase.V2_MASTER_KEY).map { GroupUtil.requireMasterKey(it) }.orElse(null)
val identityKey = cursor.optionalString(IDENTITY_KEY).map { Base64.decodeOrThrow(it) }.orElse(null)
val identityStatus = cursor.optionalInt(IDENTITY_STATUS).map { VerifiedStatus.forState(it) }.orElse(VerifiedStatus.DEFAULT)
val unregisteredTimestamp = cursor.optionalLong(UNREGISTERED_TIMESTAMP).orElse(0)
return RecipientRecord.SyncExtras(
storageProto = storageProto,
groupMasterKey = groupMasterKey,
identityKey = identityKey,
identityStatus = identityStatus,
isArchived = archived,
isForcedUnread = forcedUnread,
unregisteredTimestamp = unregisteredTimestamp
)
}
private fun getExtras(cursor: Cursor): Recipient.Extras? {
return Recipient.Extras.from(getRecipientExtras(cursor))
}
private fun getRecipientExtras(cursor: Cursor): RecipientExtras? {
return cursor.optionalBlob(EXTRAS).map { b: ByteArray? ->
try {
RecipientExtras.parseFrom(b)
} catch (e: InvalidProtocolBufferException) {
Log.w(TAG, e)
throw AssertionError(e)
}
}.orElse(null)
}
private fun updateProfileValuesForMerge(values: ContentValues, record: RecipientRecord) {
values.apply {
put(PROFILE_KEY, if (record.profileKey != null) Base64.encodeBytes(record.profileKey) else null)
putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
put(SIGNAL_PROFILE_AVATAR, record.signalProfileAvatar)
put(PROFILE_GIVEN_NAME, record.signalProfileName.givenName)
put(PROFILE_FAMILY_NAME, record.signalProfileName.familyName)
put(PROFILE_JOINED_NAME, record.signalProfileName.toString())
}
}
/**
* By default, SQLite will prefer numbers over letters when sorting. e.g. (b, a, 1) is sorted as (1, a, b).
* This order by will using a GLOB pattern to instead sort it as (a, b, 1).
*
* @param column The name of the column to sort by
*/
private fun orderByPreferringAlphaOverNumeric(column: String): String {
return "CASE WHEN $column GLOB '[0-9]*' THEN 1 ELSE 0 END, $column"
}
private fun <T> Optional<T>.isAbsent(): Boolean {
return !this.isPresent
}
private fun RecipientRecord.toLogDetails(): RecipientLogDetails {
return RecipientLogDetails(
id = this.id,
serviceId = this.serviceId,
e164 = this.e164
)
}
inner class BulkOperationsHandle internal constructor(private val database: SQLiteDatabase) {
private val pendingRecipients: MutableSet<RecipientId> = mutableSetOf()
fun setSystemContactInfo(
id: RecipientId,
systemProfileName: ProfileName,
systemDisplayName: String?,
photoUri: String?,
systemPhoneLabel: String?,
systemPhoneType: Int,
systemContactUri: String?
) {
val joinedName = Util.firstNonNull(systemDisplayName, systemProfileName.toString())
val refreshQualifyingValues = ContentValues().apply {
put(SYSTEM_GIVEN_NAME, systemProfileName.givenName)
put(SYSTEM_FAMILY_NAME, systemProfileName.familyName)
put(SYSTEM_JOINED_NAME, joinedName)
put(SYSTEM_PHOTO_URI, photoUri)
put(SYSTEM_PHONE_LABEL, systemPhoneLabel)
put(SYSTEM_PHONE_TYPE, systemPhoneType)
put(SYSTEM_CONTACT_URI, systemContactUri)
}
val updatedValues = update(id, refreshQualifyingValues)
if (updatedValues) {
pendingRecipients.add(id)
}
val otherValues = ContentValues().apply {
put(SYSTEM_INFO_PENDING, 0)
}
update(id, otherValues)
}
fun finish() {
markAllRelevantEntriesDirty()
clearSystemDataForPendingInfo()
database.setTransactionSuccessful()
database.endTransaction()
pendingRecipients.forEach { id -> ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) }
}
private fun markAllRelevantEntriesDirty() {
val query = "$SYSTEM_INFO_PENDING = ? AND $STORAGE_SERVICE_ID NOT NULL"
val args = SqlUtil.buildArgs("1")
database.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null).use { cursor ->
while (cursor.moveToNext()) {
val id = RecipientId.from(cursor.requireNonNullString(ID))
rotateStorageId(id)
}
}
}
private fun clearSystemDataForPendingInfo() {
database.update(TABLE_NAME)
.values(
SYSTEM_INFO_PENDING to 0,
SYSTEM_GIVEN_NAME to null,
SYSTEM_FAMILY_NAME to null,
SYSTEM_JOINED_NAME to null,
SYSTEM_PHOTO_URI to null,
SYSTEM_PHONE_LABEL to null,
SYSTEM_CONTACT_URI to null
)
.where("$SYSTEM_INFO_PENDING = ?", 1)
.run()
}
}
interface ColorUpdater {
fun update(name: String, materialColor: MaterialColor?): ChatColors?
}
class RecipientReader internal constructor(private val cursor: Cursor) : Closeable {
fun getCurrent(): Recipient {
val id = RecipientId.from(cursor.requireLong(ID))
return Recipient.resolved(id)
}
fun getNext(): Recipient? {
return if (cursor.moveToNext()) {
getCurrent()
} else {
null
}
}
val count: Int
get() = cursor.count
override fun close() {
cursor.close()
}
}
class MissingRecipientException(id: RecipientId?) : IllegalStateException("Failed to find recipient with ID: $id")
private class GetOrInsertResult(val recipientId: RecipientId, val neededInsert: Boolean)
@VisibleForTesting
internal class ContactSearchSelection private constructor(val where: String, val args: Array<String>) {
@VisibleForTesting
internal class Builder {
private var includeRegistered = false
private var includeNonRegistered = false
private var excludeId: RecipientId? = null
private var excludeGroups = false
private var searchQuery: String? = null
fun withRegistered(includeRegistered: Boolean): Builder {
this.includeRegistered = includeRegistered
return this
}
fun withNonRegistered(includeNonRegistered: Boolean): Builder {
this.includeNonRegistered = includeNonRegistered
return this
}
fun excludeId(recipientId: RecipientId?): Builder {
excludeId = recipientId
return this
}
fun withGroups(includeGroups: Boolean): Builder {
excludeGroups = !includeGroups
return this
}
fun withSearchQuery(searchQuery: String): Builder {
this.searchQuery = searchQuery
return this
}
fun build(): ContactSearchSelection {
check(!(!includeRegistered && !includeNonRegistered)) { "Must include either registered or non-registered recipients in search" }
val stringBuilder = StringBuilder("(")
val args: MutableList<Any?> = LinkedList()
if (includeRegistered) {
stringBuilder.append("(")
args.add(RegisteredState.REGISTERED.id)
args.add(1)
if (Util.isEmpty(searchQuery)) {
stringBuilder.append(SIGNAL_CONTACT)
} else {
stringBuilder.append(QUERY_SIGNAL_CONTACT)
args.add(searchQuery)
args.add(searchQuery)
args.add(searchQuery)
}
stringBuilder.append(")")
}
if (includeRegistered && includeNonRegistered) {
stringBuilder.append(" OR ")
}
if (includeNonRegistered) {
stringBuilder.append("(")
args.add(RegisteredState.REGISTERED.id)
if (Util.isEmpty(searchQuery)) {
stringBuilder.append(NON_SIGNAL_CONTACT)
} else {
stringBuilder.append(QUERY_NON_SIGNAL_CONTACT)
args.add(searchQuery)
args.add(searchQuery)
args.add(searchQuery)
}
stringBuilder.append(")")
}
stringBuilder.append(")")
stringBuilder.append(FILTER_BLOCKED)
args.add(0)
stringBuilder.append(FILTER_HIDDEN)
args.add(0)
if (excludeGroups) {
stringBuilder.append(FILTER_GROUPS)
}
if (excludeId != null) {
stringBuilder.append(FILTER_ID)
args.add(excludeId!!.serialize())
}
return ContactSearchSelection(stringBuilder.toString(), args.map { obj: Any? -> obj.toString() }.toTypedArray())
}
}
companion object {
const val FILTER_GROUPS = " AND $GROUP_ID IS NULL"
const val FILTER_ID = " AND $ID != ?"
const val FILTER_BLOCKED = " AND $BLOCKED = ?"
const val FILTER_HIDDEN = " AND $HIDDEN = ?"
const val NON_SIGNAL_CONTACT = "$REGISTERED != ? AND $SYSTEM_CONTACT_URI NOT NULL AND ($PHONE NOT NULL OR $EMAIL NOT NULL)"
const val QUERY_NON_SIGNAL_CONTACT = "$NON_SIGNAL_CONTACT AND ($PHONE GLOB ? OR $EMAIL GLOB ? OR $SYSTEM_JOINED_NAME GLOB ?)"
const val SIGNAL_CONTACT = "$REGISTERED = ? AND (NULLIF($SYSTEM_JOINED_NAME, '') NOT NULL OR $PROFILE_SHARING = ?) AND ($SORT_NAME NOT NULL OR $USERNAME NOT NULL)"
const val QUERY_SIGNAL_CONTACT = "$SIGNAL_CONTACT AND ($PHONE GLOB ? OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)"
}
}
/**
* Values that represent the index in the capabilities bitmask. Each index can store a 2-bit
* value, which in this case is the value of [Recipient.Capability].
*/
internal object Capabilities {
const val BIT_LENGTH = 2
// const val GROUPS_V2 = 0
const val GROUPS_V1_MIGRATION = 1
const val SENDER_KEY = 2
const val ANNOUNCEMENT_GROUPS = 3
const val CHANGE_NUMBER = 4
const val STORIES = 5
const val GIFT_BADGES = 6
const val PNP = 7
}
enum class VibrateState(val id: Int) {
DEFAULT(0), ENABLED(1), DISABLED(2);
companion object {
fun fromId(id: Int): VibrateState {
return values()[id]
}
fun fromBoolean(enabled: Boolean): VibrateState {
return if (enabled) ENABLED else DISABLED
}
}
}
enum class RegisteredState(val id: Int) {
UNKNOWN(0), REGISTERED(1), NOT_REGISTERED(2);
companion object {
fun fromId(id: Int): RegisteredState {
return values()[id]
}
}
}
enum class UnidentifiedAccessMode(val mode: Int) {
UNKNOWN(0), DISABLED(1), ENABLED(2), UNRESTRICTED(3);
companion object {
fun fromMode(mode: Int): UnidentifiedAccessMode {
return values()[mode]
}
}
}
enum class InsightsBannerTier(val id: Int) {
NO_TIER(0), TIER_ONE(1), TIER_TWO(2);
fun seen(tier: InsightsBannerTier): Boolean {
return tier.id <= id
}
companion object {
fun fromId(id: Int): InsightsBannerTier {
return values()[id]
}
}
}
enum class GroupType(val id: Int) {
NONE(0), MMS(1), SIGNAL_V1(2), SIGNAL_V2(3), DISTRIBUTION_LIST(4);
companion object {
fun fromId(id: Int): GroupType {
return values()[id]
}
}
}
enum class MentionSetting(val id: Int) {
ALWAYS_NOTIFY(0), DO_NOT_NOTIFY(1);
companion object {
fun fromId(id: Int): MentionSetting {
return values()[id]
}
}
}
private sealed class RecipientFetch(val logBundle: LogBundle?) {
/**
* We have a matching recipient, and no writes need to occur.
*/
data class Match(val id: RecipientId, val bundle: LogBundle?) : RecipientFetch(bundle)
/**
* We found a matching recipient and can update them with a new E164.
*/
data class MatchAndUpdateE164(val id: RecipientId, val e164: String, val changedNumber: RecipientId?, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We found a matching recipient and can give them an E164 that used to belong to someone else.
*/
data class MatchAndReassignE164(val id: RecipientId, val e164Id: RecipientId, val e164: String, val changedNumber: RecipientId?, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We found a matching recipient and can update them with a new ACI.
*/
data class MatchAndUpdateAci(val id: RecipientId, val serviceId: ServiceId, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We found a matching recipient and can insert an ACI as a *new user*.
*/
data class MatchAndInsertAci(val id: RecipientId, val serviceId: ServiceId, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* The ACI maps to ACI-only recipient, and the E164 maps to a different E164-only recipient. We need to merge the two together.
*/
data class MatchAndMerge(val sidId: RecipientId, val e164Id: RecipientId, val changedNumber: RecipientId?, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We don't have a matching recipient, so we need to insert one.
*/
data class Insert(val serviceId: ServiceId?, val e164: String?, val bundle: LogBundle) : RecipientFetch(bundle)
/**
* We need to create a new recipient and give it the E164 of an existing recipient.
*/
data class InsertAndReassignE164(val serviceId: ServiceId?, val e164: String?, val e164Id: RecipientId, val bundle: LogBundle) : RecipientFetch(bundle)
}
/**
* Simple class for [fetchRecipient] to pass back info that can be logged.
*/
private data class LogBundle(
val label: String,
val serviceId: ServiceId? = null,
val e164: String? = null,
val bySid: RecipientLogDetails? = null,
val byE164: RecipientLogDetails? = null
) {
fun label(label: String): LogBundle {
return this.copy(label = label)
}
}
/**
* Minimal info about a recipient that we'd want to log. Used in [fetchRecipient].
*/
private data class RecipientLogDetails(
val id: RecipientId,
val serviceId: ServiceId? = null,
val e164: String? = null
)
data class CdsV2Result(
val pni: PNI,
val aci: ACI?
) {
fun bestServiceId(): ServiceId {
return if (aci != null) {
aci
} else {
pni
}
}
}
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>,
)
}