That fuck shit the fascists are using
1package org.tm.archive.database
2
3import android.content.ContentValues
4import android.content.Context
5import android.database.Cursor
6import android.database.sqlite.SQLiteConstraintException
7import android.net.Uri
8import android.text.TextUtils
9import androidx.annotation.VisibleForTesting
10import androidx.core.content.contentValuesOf
11import app.cash.exhaustive.Exhaustive
12import okio.ByteString.Companion.toByteString
13import org.signal.core.util.Base64
14import org.signal.core.util.Bitmask
15import org.signal.core.util.CursorUtil
16import org.signal.core.util.SqlUtil
17import org.signal.core.util.delete
18import org.signal.core.util.exists
19import org.signal.core.util.forEach
20import org.signal.core.util.logging.Log
21import org.signal.core.util.nullIfBlank
22import org.signal.core.util.optionalString
23import org.signal.core.util.or
24import org.signal.core.util.orNull
25import org.signal.core.util.readToList
26import org.signal.core.util.readToSet
27import org.signal.core.util.readToSingleBoolean
28import org.signal.core.util.readToSingleLong
29import org.signal.core.util.readToSingleObject
30import org.signal.core.util.requireBlob
31import org.signal.core.util.requireInt
32import org.signal.core.util.requireLong
33import org.signal.core.util.requireNonNullString
34import org.signal.core.util.requireString
35import org.signal.core.util.select
36import org.signal.core.util.toInt
37import org.signal.core.util.update
38import org.signal.core.util.updateAll
39import org.signal.core.util.withinTransaction
40import org.signal.libsignal.protocol.IdentityKey
41import org.signal.libsignal.protocol.InvalidKeyException
42import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential
43import org.signal.libsignal.zkgroup.profiles.ProfileKey
44import org.signal.storageservice.protos.groups.local.DecryptedGroup
45import org.tm.archive.badges.Badges.toDatabaseBadge
46import org.tm.archive.badges.models.Badge
47import org.tm.archive.color.MaterialColor
48import org.tm.archive.color.MaterialColor.UnknownColorException
49import org.tm.archive.contacts.paged.ContactSearchSortOrder
50import org.tm.archive.conversation.colors.AvatarColor
51import org.tm.archive.conversation.colors.AvatarColorHash
52import org.tm.archive.conversation.colors.ChatColors
53import org.tm.archive.conversation.colors.ChatColors.Companion.forChatColor
54import org.tm.archive.conversation.colors.ChatColors.Id.Companion.forLongValue
55import org.tm.archive.conversation.colors.ChatColorsMapper.getChatColors
56import org.tm.archive.crypto.ProfileKeyUtil
57import org.tm.archive.database.GroupTable.LegacyGroupInsertException
58import org.tm.archive.database.GroupTable.ShowAsStoryState
59import org.tm.archive.database.IdentityTable.VerifiedStatus
60import org.tm.archive.database.RecipientTableCursorUtil.getRecipientExtras
61import org.tm.archive.database.SignalDatabase.Companion.groups
62import org.tm.archive.database.SignalDatabase.Companion.identities
63import org.tm.archive.database.SignalDatabase.Companion.runPostSuccessfulTransaction
64import org.tm.archive.database.SignalDatabase.Companion.sessions
65import org.tm.archive.database.SignalDatabase.Companion.threads
66import org.tm.archive.database.model.DistributionListId
67import org.tm.archive.database.model.RecipientRecord
68import org.tm.archive.database.model.ThreadRecord
69import org.tm.archive.database.model.databaseprotos.BadgeList
70import org.tm.archive.database.model.databaseprotos.ChatColor
71import org.tm.archive.database.model.databaseprotos.DeviceLastResetTime
72import org.tm.archive.database.model.databaseprotos.ExpiringProfileKeyCredentialColumnData
73import org.tm.archive.database.model.databaseprotos.RecipientExtras
74import org.tm.archive.database.model.databaseprotos.SessionSwitchoverEvent
75import org.tm.archive.database.model.databaseprotos.ThreadMergeEvent
76import org.tm.archive.database.model.databaseprotos.Wallpaper
77import org.tm.archive.dependencies.ApplicationDependencies
78import org.tm.archive.groups.BadGroupIdException
79import org.tm.archive.groups.GroupId
80import org.tm.archive.groups.GroupId.V1
81import org.tm.archive.groups.GroupId.V2
82import org.tm.archive.groups.v2.ProfileKeySet
83import org.tm.archive.groups.v2.processing.GroupsV2StateProcessor
84import org.tm.archive.jobs.RequestGroupV2InfoJob
85import org.tm.archive.jobs.RetrieveProfileJob
86import org.tm.archive.keyvalue.SignalStore
87import org.tm.archive.profiles.ProfileName
88import org.tm.archive.recipients.Recipient
89import org.tm.archive.recipients.RecipientId
90import org.tm.archive.service.webrtc.links.CallLinkRoomId
91import org.tm.archive.storage.StorageRecordUpdate
92import org.tm.archive.storage.StorageSyncHelper
93import org.tm.archive.storage.StorageSyncModels
94import org.tm.archive.util.FeatureFlags
95import org.tm.archive.util.IdentityUtil
96import org.tm.archive.util.ProfileUtil
97import org.tm.archive.util.Util
98import org.tm.archive.wallpaper.ChatWallpaper
99import org.tm.archive.wallpaper.WallpaperStorage
100import org.whispersystems.signalservice.api.profiles.SignalServiceProfile
101import org.whispersystems.signalservice.api.push.ServiceId
102import org.whispersystems.signalservice.api.push.ServiceId.ACI
103import org.whispersystems.signalservice.api.push.ServiceId.PNI
104import org.whispersystems.signalservice.api.push.SignalServiceAddress
105import org.whispersystems.signalservice.api.storage.SignalAccountRecord
106import org.whispersystems.signalservice.api.storage.SignalContactRecord
107import org.whispersystems.signalservice.api.storage.SignalGroupV1Record
108import org.whispersystems.signalservice.api.storage.SignalGroupV2Record
109import org.whispersystems.signalservice.api.storage.StorageId
110import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
111import java.io.Closeable
112import java.io.IOException
113import java.util.Collections
114import java.util.LinkedList
115import java.util.Objects
116import java.util.Optional
117import java.util.concurrent.TimeUnit
118import kotlin.jvm.optionals.getOrNull
119import kotlin.math.max
120
121open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTable(context, databaseHelper) {
122
123 val TAG = Log.tag(RecipientTable::class.java)
124
125 companion object {
126 private val UNREGISTERED_LIFESPAN: Long = TimeUnit.DAYS.toMillis(30)
127
128 const val TABLE_NAME = "recipient"
129
130 const val ID = "_id"
131 const val TYPE = "type"
132 const val E164 = "e164"
133 const val ACI_COLUMN = "aci"
134 const val PNI_COLUMN = "pni"
135 const val USERNAME = "username"
136 const val EMAIL = "email"
137 const val GROUP_ID = "group_id"
138 const val DISTRIBUTION_LIST_ID = "distribution_list_id"
139 const val CALL_LINK_ROOM_ID = "call_link_room_id"
140 const val REGISTERED = "registered"
141 const val UNREGISTERED_TIMESTAMP = "unregistered_timestamp"
142 const val BLOCKED = "blocked"
143 const val HIDDEN = "hidden"
144 const val PROFILE_KEY = "profile_key"
145 const val EXPIRING_PROFILE_KEY_CREDENTIAL = "profile_key_credential"
146 const val PROFILE_SHARING = "profile_sharing"
147 const val PROFILE_GIVEN_NAME = "profile_given_name"
148 const val PROFILE_FAMILY_NAME = "profile_family_name"
149 const val PROFILE_JOINED_NAME = "profile_joined_name"
150 const val PROFILE_AVATAR = "profile_avatar"
151 const val LAST_PROFILE_FETCH = "last_profile_fetch"
152 const val SYSTEM_GIVEN_NAME = "system_given_name"
153 const val SYSTEM_FAMILY_NAME = "system_family_name"
154 const val SYSTEM_JOINED_NAME = "system_joined_name"
155 const val SYSTEM_NICKNAME = "system_nickname"
156 const val SYSTEM_PHOTO_URI = "system_photo_uri"
157 const val SYSTEM_PHONE_LABEL = "system_phone_label"
158 const val SYSTEM_PHONE_TYPE = "system_phone_type"
159 const val SYSTEM_CONTACT_URI = "system_contact_uri"
160 const val SYSTEM_INFO_PENDING = "system_info_pending"
161 const val NOTIFICATION_CHANNEL = "notification_channel"
162 const val MESSAGE_RINGTONE = "message_ringtone"
163 const val MESSAGE_VIBRATE = "message_vibrate"
164 const val CALL_RINGTONE = "call_ringtone"
165 const val CALL_VIBRATE = "call_vibrate"
166 const val MUTE_UNTIL = "mute_until"
167 const val MESSAGE_EXPIRATION_TIME = "message_expiration_time"
168 const val SEALED_SENDER_MODE = "sealed_sender_mode"
169 const val STORAGE_SERVICE_ID = "storage_service_id"
170 const val STORAGE_SERVICE_PROTO = "storage_service_proto"
171 const val MENTION_SETTING = "mention_setting"
172 const val CAPABILITIES = "capabilities"
173 const val LAST_SESSION_RESET = "last_session_reset"
174 const val WALLPAPER = "wallpaper"
175 const val WALLPAPER_URI = "wallpaper_uri"
176 const val ABOUT = "about"
177 const val ABOUT_EMOJI = "about_emoji"
178 const val EXTRAS = "extras"
179 const val GROUPS_IN_COMMON = "groups_in_common"
180 const val AVATAR_COLOR = "avatar_color"
181 const val CHAT_COLORS = "chat_colors"
182 const val CUSTOM_CHAT_COLORS_ID = "custom_chat_colors_id"
183 const val BADGES = "badges"
184 const val NEEDS_PNI_SIGNATURE = "needs_pni_signature"
185 const val REPORTING_TOKEN = "reporting_token"
186 const val PHONE_NUMBER_SHARING = "phone_number_sharing"
187 const val PHONE_NUMBER_DISCOVERABLE = "phone_number_discoverable"
188 const val PNI_SIGNATURE_VERIFIED = "pni_signature_verified"
189 const val NICKNAME_GIVEN_NAME = "nickname_given_name"
190 const val NICKNAME_FAMILY_NAME = "nickname_family_name"
191 const val NICKNAME_JOINED_NAME = "nickname_joined_name"
192 const val NOTE = "note"
193
194 const val SEARCH_PROFILE_NAME = "search_signal_profile"
195 const val SORT_NAME = "sort_name"
196 const val IDENTITY_STATUS = "identity_status"
197 const val IDENTITY_KEY = "identity_key"
198
199 @JvmField
200 val CREATE_TABLE =
201 """
202 CREATE TABLE $TABLE_NAME (
203 $ID INTEGER PRIMARY KEY AUTOINCREMENT,
204 $TYPE INTEGER DEFAULT ${RecipientType.INDIVIDUAL.id},
205 $E164 TEXT UNIQUE DEFAULT NULL,
206 $ACI_COLUMN TEXT UNIQUE DEFAULT NULL,
207 $PNI_COLUMN TEXT UNIQUE DEFAULT NULL CHECK (pni LIKE 'PNI:%'),
208 $USERNAME TEXT UNIQUE DEFAULT NULL,
209 $EMAIL TEXT UNIQUE DEFAULT NULL,
210 $GROUP_ID TEXT UNIQUE DEFAULT NULL,
211 $DISTRIBUTION_LIST_ID INTEGER DEFAULT NULL,
212 $CALL_LINK_ROOM_ID TEXT DEFAULT NULL,
213 $REGISTERED INTEGER DEFAULT ${RegisteredState.UNKNOWN.id},
214 $UNREGISTERED_TIMESTAMP INTEGER DEFAULT 0,
215 $BLOCKED INTEGER DEFAULT 0,
216 $HIDDEN INTEGER DEFAULT 0,
217 $PROFILE_KEY TEXT DEFAULT NULL,
218 $EXPIRING_PROFILE_KEY_CREDENTIAL TEXT DEFAULT NULL,
219 $PROFILE_SHARING INTEGER DEFAULT 0,
220 $PROFILE_GIVEN_NAME TEXT DEFAULT NULL,
221 $PROFILE_FAMILY_NAME TEXT DEFAULT NULL,
222 $PROFILE_JOINED_NAME TEXT DEFAULT NULL,
223 $PROFILE_AVATAR TEXT DEFAULT NULL,
224 $LAST_PROFILE_FETCH INTEGER DEFAULT 0,
225 $SYSTEM_GIVEN_NAME TEXT DEFAULT NULL,
226 $SYSTEM_FAMILY_NAME TEXT DEFAULT NULL,
227 $SYSTEM_JOINED_NAME TEXT DEFAULT NULL,
228 $SYSTEM_NICKNAME TEXT DEFAULT NULL,
229 $SYSTEM_PHOTO_URI TEXT DEFAULT NULL,
230 $SYSTEM_PHONE_LABEL TEXT DEFAULT NULL,
231 $SYSTEM_PHONE_TYPE INTEGER DEFAULT -1,
232 $SYSTEM_CONTACT_URI TEXT DEFAULT NULL,
233 $SYSTEM_INFO_PENDING INTEGER DEFAULT 0,
234 $NOTIFICATION_CHANNEL TEXT DEFAULT NULL,
235 $MESSAGE_RINGTONE TEXT DEFAULT NULL,
236 $MESSAGE_VIBRATE INTEGER DEFAULT ${VibrateState.DEFAULT.id},
237 $CALL_RINGTONE TEXT DEFAULT NULL,
238 $CALL_VIBRATE INTEGER DEFAULT ${VibrateState.DEFAULT.id},
239 $MUTE_UNTIL INTEGER DEFAULT 0,
240 $MESSAGE_EXPIRATION_TIME INTEGER DEFAULT 0,
241 $SEALED_SENDER_MODE INTEGER DEFAULT 0,
242 $STORAGE_SERVICE_ID TEXT UNIQUE DEFAULT NULL,
243 $STORAGE_SERVICE_PROTO TEXT DEFAULT NULL,
244 $MENTION_SETTING INTEGER DEFAULT ${MentionSetting.ALWAYS_NOTIFY.id},
245 $CAPABILITIES INTEGER DEFAULT 0,
246 $LAST_SESSION_RESET BLOB DEFAULT NULL,
247 $WALLPAPER BLOB DEFAULT NULL,
248 $WALLPAPER_URI TEXT DEFAULT NULL,
249 $ABOUT TEXT DEFAULT NULL,
250 $ABOUT_EMOJI TEXT DEFAULT NULL,
251 $EXTRAS BLOB DEFAULT NULL,
252 $GROUPS_IN_COMMON INTEGER DEFAULT 0,
253 $AVATAR_COLOR TEXT DEFAULT NULL,
254 $CHAT_COLORS BLOB DEFAULT NULL,
255 $CUSTOM_CHAT_COLORS_ID INTEGER DEFAULT 0,
256 $BADGES BLOB DEFAULT NULL,
257 $NEEDS_PNI_SIGNATURE INTEGER DEFAULT 0,
258 $REPORTING_TOKEN BLOB DEFAULT NULL,
259 $PHONE_NUMBER_SHARING INTEGER DEFAULT ${PhoneNumberSharingState.UNKNOWN.id},
260 $PHONE_NUMBER_DISCOVERABLE INTEGER DEFAULT ${PhoneNumberDiscoverableState.UNKNOWN.id},
261 $PNI_SIGNATURE_VERIFIED INTEGER DEFAULT 0,
262 $NICKNAME_GIVEN_NAME TEXT DEFAULT NULL,
263 $NICKNAME_FAMILY_NAME TEXT DEFAULT NULL,
264 $NICKNAME_JOINED_NAME TEXT DEFAULT NULL,
265 $NOTE TEXT DEFAULT NULL
266 )
267 """
268
269 val CREATE_INDEXS = arrayOf(
270 "CREATE INDEX IF NOT EXISTS recipient_type_index ON $TABLE_NAME ($TYPE);",
271 "CREATE INDEX IF NOT EXISTS recipient_aci_profile_key_index ON $TABLE_NAME ($ACI_COLUMN, $PROFILE_KEY) WHERE $ACI_COLUMN NOT NULL AND $PROFILE_KEY NOT NULL"
272 )
273
274 private val RECIPIENT_PROJECTION: Array<String> = arrayOf(
275 ID,
276 TYPE,
277 E164,
278 ACI_COLUMN,
279 PNI_COLUMN,
280 USERNAME,
281 EMAIL,
282 GROUP_ID,
283 DISTRIBUTION_LIST_ID,
284 CALL_LINK_ROOM_ID,
285 REGISTERED,
286 BLOCKED,
287 HIDDEN,
288 PROFILE_KEY,
289 EXPIRING_PROFILE_KEY_CREDENTIAL,
290 PROFILE_SHARING,
291 PROFILE_GIVEN_NAME,
292 PROFILE_FAMILY_NAME,
293 PROFILE_AVATAR,
294 LAST_PROFILE_FETCH,
295 SYSTEM_GIVEN_NAME,
296 SYSTEM_FAMILY_NAME,
297 SYSTEM_JOINED_NAME,
298 SYSTEM_PHOTO_URI,
299 SYSTEM_PHONE_LABEL,
300 SYSTEM_PHONE_TYPE,
301 SYSTEM_CONTACT_URI,
302 NOTIFICATION_CHANNEL,
303 MESSAGE_RINGTONE,
304 MESSAGE_VIBRATE,
305 CALL_RINGTONE,
306 CALL_VIBRATE,
307 MUTE_UNTIL,
308 MESSAGE_EXPIRATION_TIME,
309 SEALED_SENDER_MODE,
310 STORAGE_SERVICE_ID,
311 MENTION_SETTING,
312 CAPABILITIES,
313 WALLPAPER,
314 WALLPAPER_URI,
315 ABOUT,
316 ABOUT_EMOJI,
317 EXTRAS,
318 GROUPS_IN_COMMON,
319 AVATAR_COLOR,
320 CHAT_COLORS,
321 CUSTOM_CHAT_COLORS_ID,
322 BADGES,
323 NEEDS_PNI_SIGNATURE,
324 REPORTING_TOKEN,
325 PHONE_NUMBER_SHARING,
326 NICKNAME_GIVEN_NAME,
327 NICKNAME_FAMILY_NAME,
328 NOTE
329 )
330
331 private val ID_PROJECTION = arrayOf(ID)
332
333 private val SEARCH_PROJECTION = arrayOf(
334 ID,
335 SYSTEM_JOINED_NAME,
336 E164,
337 EMAIL,
338 SYSTEM_PHONE_LABEL,
339 SYSTEM_PHONE_TYPE,
340 REGISTERED,
341 ABOUT,
342 ABOUT_EMOJI,
343 EXTRAS,
344 GROUPS_IN_COMMON,
345 "COALESCE(NULLIF($PROFILE_JOINED_NAME, ''), NULLIF($PROFILE_GIVEN_NAME, '')) AS $SEARCH_PROFILE_NAME",
346 """
347 LOWER(
348 COALESCE(
349 NULLIF($NICKNAME_JOINED_NAME, ''),
350 NULLIF($NICKNAME_GIVEN_NAME, ''),
351 NULLIF($SYSTEM_JOINED_NAME, ''),
352 NULLIF($SYSTEM_GIVEN_NAME, ''),
353 NULLIF($PROFILE_JOINED_NAME, ''),
354 NULLIF($PROFILE_GIVEN_NAME, ''),
355 NULLIF($USERNAME, '')
356 )
357 ) AS $SORT_NAME
358 """
359 )
360
361 @JvmField
362 val SEARCH_PROJECTION_NAMES = arrayOf(
363 ID,
364 SYSTEM_JOINED_NAME,
365 E164,
366 EMAIL,
367 SYSTEM_PHONE_LABEL,
368 SYSTEM_PHONE_TYPE,
369 REGISTERED,
370 ABOUT,
371 ABOUT_EMOJI,
372 EXTRAS,
373 GROUPS_IN_COMMON,
374 SEARCH_PROFILE_NAME,
375 SORT_NAME
376 )
377
378 private val TYPED_RECIPIENT_PROJECTION: Array<String> = RECIPIENT_PROJECTION
379 .map { columnName -> "$TABLE_NAME.$columnName" }
380 .toTypedArray()
381
382 @JvmField
383 val TYPED_RECIPIENT_PROJECTION_NO_ID: Array<String> = TYPED_RECIPIENT_PROJECTION.copyOfRange(1, TYPED_RECIPIENT_PROJECTION.size)
384
385 private val MENTION_SEARCH_PROJECTION = arrayOf(
386 ID,
387 """
388 REPLACE(
389 COALESCE(
390 NULLIF($NICKNAME_JOINED_NAME, ''),
391 NULLIF($NICKNAME_GIVEN_NAME, ''),
392 NULLIF($SYSTEM_JOINED_NAME, ''),
393 NULLIF($SYSTEM_GIVEN_NAME, ''),
394 NULLIF($PROFILE_JOINED_NAME, ''),
395 NULLIF($PROFILE_GIVEN_NAME, ''),
396 NULLIF($USERNAME, ''),
397 NULLIF($E164, '')
398 ),
399 ' ',
400 ''
401 ) AS $SORT_NAME
402 """
403 )
404
405 /** Used as a placeholder recipient for self during migrations when self isn't yet available. */
406 private val PLACEHOLDER_SELF_ID = -2L
407
408 @JvmStatic
409 fun maskCapabilitiesToLong(capabilities: SignalServiceProfile.Capabilities): Long {
410 var value: Long = 0
411 value = Bitmask.update(value, Capabilities.PNP, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPnp).serialize().toLong())
412 value = Bitmask.update(value, Capabilities.PAYMENT_ACTIVATION, Capabilities.BIT_LENGTH, Recipient.Capability.fromBoolean(capabilities.isPaymentActivation).serialize().toLong())
413 return value
414 }
415 }
416
417 fun getByE164(e164: String): Optional<RecipientId> {
418 return getByColumn(E164, e164)
419 }
420
421 fun getByGroupId(groupId: GroupId): Optional<RecipientId> {
422 return getByColumn(GROUP_ID, groupId.toString())
423 }
424
425 fun getByServiceId(serviceId: ServiceId): Optional<RecipientId> {
426 return when (serviceId) {
427 is ACI -> getByAci(serviceId)
428 is PNI -> getByPni(serviceId)
429 }
430 }
431
432 fun getByAci(aci: ACI): Optional<RecipientId> {
433 return getByColumn(ACI_COLUMN, aci.toString())
434 }
435
436 fun getByPni(pni: PNI): Optional<RecipientId> {
437 return getByColumn(PNI_COLUMN, pni.toString())
438 }
439
440 fun getByUsername(username: String): Optional<RecipientId> {
441 return getByColumn(USERNAME, username)
442 }
443
444 fun getByCallLinkRoomId(callLinkRoomId: CallLinkRoomId): Optional<RecipientId> {
445 return getByColumn(CALL_LINK_ROOM_ID, callLinkRoomId.serialize())
446 }
447
448 fun isAssociated(serviceId: ServiceId, pni: PNI): Boolean {
449 return readableDatabase.exists(TABLE_NAME).where("$ACI_COLUMN = ? AND $PNI_COLUMN = ?", serviceId.toString(), pni.toString()).run()
450 }
451
452 fun getByE164IfRegisteredAndDiscoverable(e164: String): RecipientId? {
453 return readableDatabase
454 .select(ID)
455 .from(TABLE_NAME)
456 .where("$E164 = ? AND $REGISTERED = ${RegisteredState.REGISTERED.id} AND $PHONE_NUMBER_DISCOVERABLE = ${PhoneNumberDiscoverableState.DISCOVERABLE.id} AND ($PNI_COLUMN NOT NULL OR $ACI_COLUMN NOT NULL)", e164)
457 .run()
458 .readToSingleObject { RecipientId.from(it.requireLong(ID)) }
459 }
460
461 @JvmOverloads
462 fun getAndPossiblyMerge(serviceId: ServiceId?, e164: String?, changeSelf: Boolean = false): RecipientId {
463 require(serviceId != null || e164 != null) { "Must provide an ACI or E164!" }
464 return when (serviceId) {
465 is ACI -> getAndPossiblyMerge(aci = serviceId, pni = null, e164 = e164, pniVerified = false, changeSelf = changeSelf)
466 is PNI -> getAndPossiblyMerge(aci = null, pni = serviceId, e164 = e164, pniVerified = false, changeSelf = changeSelf)
467 else -> getAndPossiblyMerge(aci = null, pni = null, e164 = e164, pniVerified = false, changeSelf = changeSelf)
468 }
469 }
470
471 /**
472 * Gets and merges a (serviceId, pni, e164) tuple, doing merges/updates as needed, and giving you back the final RecipientId.
473 * It is assumed that the tuple is verified. Do not give this method an untrusted association.
474 */
475 fun getAndPossiblyMergePnpVerified(aci: ACI?, pni: PNI?, e164: String?): RecipientId {
476 return getAndPossiblyMerge(aci = aci, pni = pni, e164 = e164, pniVerified = true, changeSelf = false)
477 }
478
479 @VisibleForTesting
480 fun getAndPossiblyMerge(aci: ACI?, pni: PNI?, e164: String?, pniVerified: Boolean = false, changeSelf: Boolean = false): RecipientId {
481 require(aci != null || pni != null || e164 != null) { "Must provide an ACI, PNI, or E164!" }
482
483 // To avoid opening a transaction and doing extra reads, we start with a single read that checks if all of the fields already match a single recipient
484 val singleMatch: RecipientId? = getRecipientIdIfAllFieldsMatch(aci, pni, e164)
485 if (singleMatch != null) {
486 return singleMatch
487 }
488
489 Log.d(TAG, "[getAndPossiblyMerge] Requires a transaction.")
490
491 val db = writableDatabase
492 lateinit var result: ProcessPnpTupleResult
493
494 db.withinTransaction {
495 result = processPnpTuple(e164 = e164, pni = pni, aci = aci, pniVerified = pniVerified, changeSelf = changeSelf)
496
497 if (result.operations.isNotEmpty() || result.requiredInsert) {
498 Log.i(TAG, "[getAndPossiblyMerge] ($aci, $pni, $e164) BreadCrumbs: ${result.breadCrumbs}, Operations: ${result.operations}, RequiredInsert: ${result.requiredInsert}, FinalId: ${result.finalId}")
499 }
500
501 db.runPostSuccessfulTransaction {
502 if (result.affectedIds.isNotEmpty()) {
503 result.affectedIds.forEach { ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(it) }
504 RetrieveProfileJob.enqueue(result.affectedIds)
505 }
506
507 if (result.oldIds.isNotEmpty()) {
508 result.oldIds.forEach { oldId ->
509 Recipient.live(oldId).refresh(result.finalId)
510 ApplicationDependencies.getRecipientCache().remap(oldId, result.finalId)
511 }
512 }
513
514 if (result.affectedIds.isNotEmpty() || result.oldIds.isNotEmpty()) {
515 StorageSyncHelper.scheduleSyncForDataChange()
516 RecipientId.clearCache()
517 }
518 }
519 }
520
521 return result.finalId
522 }
523
524 fun getAllServiceIdProfileKeyPairs(): Map<ServiceId, ProfileKey> {
525 val serviceIdToProfileKey: MutableMap<ServiceId, ProfileKey> = mutableMapOf()
526
527 readableDatabase
528 .select(ACI_COLUMN, PROFILE_KEY)
529 .from(TABLE_NAME)
530 .where("$ACI_COLUMN NOT NULL AND $PROFILE_KEY NOT NULL")
531 .run()
532 .use { cursor ->
533 while (cursor.moveToNext()) {
534 val aci: ACI? = ACI.parseOrNull(cursor.requireString(ACI_COLUMN))
535 val profileKey: ProfileKey? = ProfileKeyUtil.profileKeyOrNull(cursor.requireString(PROFILE_KEY))
536
537 if (aci != null && profileKey != null) {
538 serviceIdToProfileKey[aci] = profileKey
539 }
540 }
541 }
542
543 return serviceIdToProfileKey
544 }
545
546 fun getOrInsertFromServiceId(serviceId: ServiceId): RecipientId {
547 return getAndPossiblyMerge(serviceId = serviceId, e164 = null)
548 }
549
550 fun getOrInsertFromE164(e164: String): RecipientId {
551 return getAndPossiblyMerge(serviceId = null, e164 = e164)
552 }
553
554 fun getOrInsertFromEmail(email: String): RecipientId {
555 return getOrInsertByColumn(EMAIL, email).recipientId
556 }
557
558 @JvmOverloads
559 fun getOrInsertFromDistributionListId(distributionListId: DistributionListId, storageId: ByteArray? = null): RecipientId {
560 return getOrInsertByColumn(
561 DISTRIBUTION_LIST_ID,
562 distributionListId.serialize(),
563 ContentValues().apply {
564 put(TYPE, RecipientType.DISTRIBUTION_LIST.id)
565 put(DISTRIBUTION_LIST_ID, distributionListId.serialize())
566 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(storageId ?: StorageSyncHelper.generateKey()))
567 put(PROFILE_SHARING, 1)
568 }
569 ).recipientId
570 }
571
572 fun getOrInsertFromCallLinkRoomId(callLinkRoomId: CallLinkRoomId): RecipientId {
573 return getOrInsertByColumn(
574 CALL_LINK_ROOM_ID,
575 callLinkRoomId.serialize(),
576 contentValuesOf(
577 TYPE to RecipientType.CALL_LINK.id,
578 CALL_LINK_ROOM_ID to callLinkRoomId.serialize(),
579 PROFILE_SHARING to 1
580 )
581 ).recipientId
582 }
583
584 fun getDistributionListRecipientIds(): List<RecipientId> {
585 val recipientIds = mutableListOf<RecipientId>()
586 readableDatabase.query(TABLE_NAME, arrayOf(ID), "$DISTRIBUTION_LIST_ID is not NULL", null, null, null, null).use { cursor ->
587 while (cursor != null && cursor.moveToNext()) {
588 recipientIds.add(RecipientId.from(CursorUtil.requireLong(cursor, ID)))
589 }
590 }
591
592 return recipientIds
593 }
594
595 fun getOrInsertFromGroupId(groupId: GroupId): RecipientId {
596 var existing = getByGroupId(groupId)
597
598 if (existing.isPresent) {
599 return existing.get()
600 } else if (groupId.isV1 && groups.groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
601 throw LegacyGroupInsertException(groupId)
602 } else {
603 val values = ContentValues().apply {
604 put(GROUP_ID, groupId.toString())
605 put(AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize())
606 }
607
608 val id = writableDatabase.insert(TABLE_NAME, null, values)
609 if (id < 0) {
610 existing = getByColumn(GROUP_ID, groupId.toString())
611 if (existing.isPresent) {
612 return existing.get()
613 } else if (groupId.isV1 && groups.groupExists(groupId.requireV1().deriveV2MigrationGroupId())) {
614 throw LegacyGroupInsertException(groupId)
615 } else {
616 throw AssertionError("Failed to insert recipient!")
617 }
618 } else {
619 val groupUpdates = ContentValues().apply {
620 if (groupId.isMms) {
621 put(TYPE, RecipientType.MMS.id)
622 } else {
623 if (groupId.isV2) {
624 put(TYPE, RecipientType.GV2.id)
625 } else {
626 put(TYPE, RecipientType.GV1.id)
627 }
628 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
629 }
630 }
631
632 val recipientId = RecipientId.from(id)
633 val updateSuccess = update(recipientId, groupUpdates)
634
635 if (!updateSuccess) {
636 Log.w(TAG, "Failed to update newly-created record for $recipientId")
637 }
638
639 Log.i(TAG, "Group $groupId was newly-inserted as $recipientId")
640
641 return recipientId
642 }
643 }
644 }
645
646 /**
647 * See [Recipient.externalPossiblyMigratedGroup].
648 */
649 fun getOrInsertFromPossiblyMigratedGroupId(groupId: GroupId): RecipientId {
650 val db = writableDatabase
651 db.beginTransaction()
652
653 try {
654 val existing = getByColumn(GROUP_ID, groupId.toString())
655 if (existing.isPresent) {
656 db.setTransactionSuccessful()
657 return existing.get()
658 }
659
660 if (groupId.isV1) {
661 val v2 = getByGroupId(groupId.requireV1().deriveV2MigrationGroupId())
662 if (v2.isPresent) {
663 db.setTransactionSuccessful()
664 return v2.get()
665 }
666 }
667
668 val id = getOrInsertFromGroupId(groupId)
669 db.setTransactionSuccessful()
670 return id
671 } finally {
672 db.endTransaction()
673 }
674 }
675
676 fun getAll(): RecipientIterator {
677 val cursor = readableDatabase
678 .select()
679 .from(TABLE_NAME)
680 .run()
681
682 return RecipientIterator(context, cursor)
683 }
684
685 /**
686 * Only call once to create initial release channel recipient.
687 */
688 fun insertReleaseChannelRecipient(): RecipientId {
689 val values = ContentValues().apply {
690 put(AVATAR_COLOR, AvatarColor.random().serialize())
691 }
692
693 val id = writableDatabase.insert(TABLE_NAME, null, values)
694 if (id < 0) {
695 throw AssertionError("Failed to insert recipient!")
696 } else {
697 return GetOrInsertResult(RecipientId.from(id), true).recipientId
698 }
699 }
700
701 fun getBlocked(): Cursor {
702 return readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$BLOCKED = 1", null, null, null, null)
703 }
704
705 fun readerForBlocked(cursor: Cursor): RecipientReader {
706 return RecipientReader(cursor)
707 }
708
709 fun getRecipientsWithNotificationChannels(): RecipientReader {
710 val cursor = readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$NOTIFICATION_CHANNEL NOT NULL", null, null, null, null)
711 return RecipientReader(cursor)
712 }
713
714 fun getRecords(ids: Collection<RecipientId>): Map<RecipientId, RecipientRecord> {
715 val queries = SqlUtil.buildCollectionQuery(
716 column = ID,
717 values = ids.map { it.serialize() }
718 )
719
720 val foundRecords = queries.flatMap { query ->
721 readableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, null).readToList { cursor ->
722 RecipientTableCursorUtil.getRecord(context, cursor)
723 }
724 }
725
726 val foundIds = foundRecords.map { record -> record.id }
727 val remappedRecords = ids.filterNot { it in foundIds }.map(::findRemappedIdRecord)
728
729 return (foundRecords + remappedRecords).associateBy { it.id }
730 }
731
732 fun getRecord(id: RecipientId): RecipientRecord {
733 val query = "$ID = ?"
734 val args = arrayOf(id.serialize())
735
736 readableDatabase.query(TABLE_NAME, RECIPIENT_PROJECTION, query, args, null, null, null).use { cursor ->
737 return if (cursor != null && cursor.moveToNext()) {
738 RecipientTableCursorUtil.getRecord(context, cursor)
739 } else {
740 findRemappedIdRecord(id)
741 }
742 }
743 }
744
745 private fun findRemappedIdRecord(id: RecipientId): RecipientRecord {
746 val remapped = RemappedRecords.getInstance().getRecipient(id)
747
748 return if (remapped.isPresent) {
749 Log.w(TAG, "Missing recipient for $id, but found it in the remapped records as ${remapped.get()}")
750 getRecord(remapped.get())
751 } else {
752 throw MissingRecipientException(id)
753 }
754 }
755
756 fun getRecordForSync(id: RecipientId): RecipientRecord? {
757 val query = "$TABLE_NAME.$ID = ?"
758 val args = arrayOf(id.serialize())
759 val recordForSync = getRecordForSync(query, args)
760
761 if (recordForSync.isEmpty()) {
762 return null
763 }
764
765 if (recordForSync.size > 1) {
766 throw AssertionError()
767 }
768
769 return recordForSync[0]
770 }
771
772 fun getByStorageId(storageId: ByteArray): RecipientRecord? {
773 val result = getRecordForSync("$TABLE_NAME.$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeWithPadding(storageId)))
774
775 return if (result.isNotEmpty()) {
776 result[0]
777 } else {
778 null
779 }
780 }
781
782 fun markNeedsSyncWithoutRefresh(recipientIds: Collection<RecipientId>) {
783 val db = writableDatabase
784 db.beginTransaction()
785 try {
786 for (recipientId in recipientIds) {
787 rotateStorageId(recipientId)
788 }
789 db.setTransactionSuccessful()
790 } finally {
791 db.endTransaction()
792 }
793 }
794
795 fun markNeedsSync(recipientIds: Collection<RecipientId>) {
796 writableDatabase
797 .withinTransaction {
798 for (recipientId in recipientIds) {
799 markNeedsSync(recipientId)
800 }
801 }
802 }
803
804 fun markNeedsSync(recipientId: RecipientId) {
805 rotateStorageId(recipientId)
806 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
807 }
808
809 fun markAllSystemContactsNeedsSync() {
810 writableDatabase.withinTransaction { db ->
811 db
812 .select(ID)
813 .from(TABLE_NAME)
814 .where("$SYSTEM_CONTACT_URI NOT NULL")
815 .run()
816 .use { cursor ->
817 while (cursor.moveToNext()) {
818 rotateStorageId(RecipientId.from(cursor.requireLong(ID)))
819 }
820 }
821 }
822 }
823
824 fun applyStorageIdUpdates(storageIds: Map<RecipientId, StorageId>) {
825 val db = writableDatabase
826 db.beginTransaction()
827 try {
828 val query = "$ID = ?"
829 for ((key, value) in storageIds) {
830 val values = ContentValues().apply {
831 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(value.raw))
832 }
833 db.update(TABLE_NAME, values, query, arrayOf(key.serialize()))
834 }
835 db.setTransactionSuccessful()
836 } finally {
837 db.endTransaction()
838 }
839
840 for (id in storageIds.keys) {
841 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
842 }
843 }
844
845 fun applyStorageSyncContactInsert(insert: SignalContactRecord) {
846 val db = writableDatabase
847 val threadDatabase = threads
848 val values = getValuesForStorageContact(insert, true)
849 val id = db.insertWithOnConflict(TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE)
850
851 val recipientId: RecipientId
852 if (id < 0) {
853 Log.w(TAG, "[applyStorageSyncContactInsert] Failed to insert. Possibly merging.")
854 recipientId = getAndPossiblyMerge(aci = insert.aci.orNull(), pni = insert.pni.orNull(), e164 = insert.number.orNull(), pniVerified = insert.isPniSignatureVerified)
855 db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId))
856 } else {
857 recipientId = RecipientId.from(id)
858 }
859
860 if (insert.identityKey.isPresent && (insert.aci.isPresent || insert.pni.isPresent)) {
861 try {
862 val serviceId: ServiceId = insert.aci.orNull() ?: insert.pni.get()
863 val identityKey = IdentityKey(insert.identityKey.get(), 0)
864 identities.updateIdentityAfterSync(serviceId.toString(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(insert.identityState))
865 } catch (e: InvalidKeyException) {
866 Log.w(TAG, "Failed to process identity key during insert! Skipping.", e)
867 }
868 }
869
870 updateExtras(recipientId) {
871 it.hideStory(insert.shouldHideStory())
872 }
873
874 threadDatabase.applyStorageSyncUpdate(recipientId, insert)
875 }
876
877 fun applyStorageSyncContactUpdate(update: StorageRecordUpdate<SignalContactRecord>) {
878 val db = writableDatabase
879 val identityStore = ApplicationDependencies.getProtocolStore().aci().identities()
880 val values = getValuesForStorageContact(update.new, false)
881
882 try {
883 val updateCount = db.update(TABLE_NAME, values, "$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeWithPadding(update.old.id.raw)))
884 if (updateCount < 1) {
885 throw AssertionError("Had an update, but it didn't match any rows!")
886 }
887 } catch (e: SQLiteConstraintException) {
888 Log.w(TAG, "[applyStorageSyncContactUpdate] Failed to update a user by storageId.")
889 var recipientId = getByColumn(STORAGE_SERVICE_ID, Base64.encodeWithPadding(update.old.id.raw)).get()
890
891 Log.w(TAG, "[applyStorageSyncContactUpdate] Found user $recipientId. Possibly merging.")
892 recipientId = getAndPossiblyMerge(aci = update.new.aci.orElse(null), pni = update.new.pni.orElse(null), e164 = update.new.number.orElse(null), pniVerified = update.new.isPniSignatureVerified)
893
894 Log.w(TAG, "[applyStorageSyncContactUpdate] Merged into $recipientId")
895 db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(recipientId))
896 }
897
898 val recipientId = getByStorageKeyOrThrow(update.new.id.raw)
899 if (StorageSyncHelper.profileKeyChanged(update)) {
900 val clearValues = ContentValues(1).apply {
901 putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
902 }
903 db.update(TABLE_NAME, clearValues, ID_WHERE, SqlUtil.buildArgs(recipientId))
904 }
905
906 try {
907 val oldIdentityRecord = identityStore.getIdentityRecord(recipientId)
908 if (update.new.identityKey.isPresent && update.new.aci.isPresent) {
909 val identityKey = IdentityKey(update.new.identityKey.get(), 0)
910 identities.updateIdentityAfterSync(update.new.aci.get().toString(), recipientId, identityKey, StorageSyncModels.remoteToLocalIdentityStatus(update.new.identityState))
911 }
912
913 val newIdentityRecord = identityStore.getIdentityRecord(recipientId)
914 if (newIdentityRecord.isPresent && newIdentityRecord.get().verifiedStatus == VerifiedStatus.VERIFIED && (!oldIdentityRecord.isPresent || oldIdentityRecord.get().verifiedStatus != VerifiedStatus.VERIFIED)) {
915 IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), true, true)
916 } else if (newIdentityRecord.isPresent && newIdentityRecord.get().verifiedStatus != VerifiedStatus.VERIFIED && oldIdentityRecord.isPresent && oldIdentityRecord.get().verifiedStatus == VerifiedStatus.VERIFIED) {
917 IdentityUtil.markIdentityVerified(context, Recipient.resolved(recipientId), false, true)
918 }
919 } catch (e: InvalidKeyException) {
920 Log.w(TAG, "Failed to process identity key during update! Skipping.", e)
921 }
922
923 updateExtras(recipientId) {
924 it.hideStory(update.new.shouldHideStory())
925 }
926
927 threads.applyStorageSyncUpdate(recipientId, update.new)
928 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
929 }
930
931 fun applyStorageSyncGroupV1Insert(insert: SignalGroupV1Record) {
932 val id = writableDatabase.insertOrThrow(TABLE_NAME, null, getValuesForStorageGroupV1(insert, true))
933
934 val recipientId = RecipientId.from(id)
935 threads.applyStorageSyncUpdate(recipientId, insert)
936 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
937 }
938
939 fun applyStorageSyncGroupV1Update(update: StorageRecordUpdate<SignalGroupV1Record>) {
940 val values = getValuesForStorageGroupV1(update.new, false)
941
942 val updateCount = writableDatabase.update(TABLE_NAME, values, STORAGE_SERVICE_ID + " = ?", arrayOf(Base64.encodeWithPadding(update.old.id.raw)))
943 if (updateCount < 1) {
944 throw AssertionError("Had an update, but it didn't match any rows!")
945 }
946
947 val recipient = Recipient.externalGroupExact(GroupId.v1orThrow(update.old.groupId))
948 threads.applyStorageSyncUpdate(recipient.id, update.new)
949 recipient.live().refresh()
950 }
951
952 fun applyStorageSyncGroupV2Insert(insert: SignalGroupV2Record) {
953 val masterKey = insert.masterKeyOrThrow
954 val groupId = GroupId.v2(masterKey)
955 val values = getValuesForStorageGroupV2(insert, true)
956
957 writableDatabase.insertOrThrow(TABLE_NAME, null, values)
958 val recipient = Recipient.externalGroupExact(groupId)
959
960 Log.i(TAG, "Creating restore placeholder for $groupId")
961 val createdId = groups.create(
962 masterKey,
963 DecryptedGroup.Builder()
964 .revision(GroupsV2StateProcessor.RESTORE_PLACEHOLDER_REVISION)
965 .build()
966 )
967
968 if (createdId == null) {
969 Log.w(TAG, "Unable to create restore placeholder for $groupId, group already exists")
970 }
971
972 groups.setShowAsStoryState(groupId, insert.storySendMode.toShowAsStoryState())
973 updateExtras(recipient.id) {
974 it.hideStory(insert.shouldHideStory())
975 }
976
977 Log.i(TAG, "Scheduling request for latest group info for $groupId")
978 ApplicationDependencies.getJobManager().add(RequestGroupV2InfoJob(groupId))
979 threads.applyStorageSyncUpdate(recipient.id, insert)
980 recipient.live().refresh()
981 }
982
983 fun applyStorageSyncGroupV2Update(update: StorageRecordUpdate<SignalGroupV2Record>) {
984 val values = getValuesForStorageGroupV2(update.new, false)
985
986 val updateCount = writableDatabase.update(TABLE_NAME, values, "$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeWithPadding(update.old.id.raw)))
987 if (updateCount < 1) {
988 throw AssertionError("Had an update, but it didn't match any rows!")
989 }
990
991 val masterKey = update.old.masterKeyOrThrow
992 val groupId = GroupId.v2(masterKey)
993 val recipient = Recipient.externalGroupExact(groupId)
994
995 updateExtras(recipient.id) {
996 it.hideStory(update.new.shouldHideStory())
997 }
998
999 groups.setShowAsStoryState(groupId, update.new.storySendMode.toShowAsStoryState())
1000 threads.applyStorageSyncUpdate(recipient.id, update.new)
1001 recipient.live().refresh()
1002 }
1003
1004 fun applyStorageSyncAccountUpdate(update: StorageRecordUpdate<SignalAccountRecord>) {
1005 val profileName = ProfileName.fromParts(update.new.givenName.orElse(null), update.new.familyName.orElse(null))
1006 val localKey = ProfileKeyUtil.profileKeyOptional(update.old.profileKey.orElse(null))
1007 val remoteKey = ProfileKeyUtil.profileKeyOptional(update.new.profileKey.orElse(null))
1008 val profileKey: String? = remoteKey.or(localKey).map { obj: ProfileKey -> obj.serialize() }.map { source: ByteArray? -> Base64.encodeWithPadding(source!!) }.orElse(null)
1009 if (!remoteKey.isPresent) {
1010 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"}.")
1011 }
1012
1013 val values = ContentValues().apply {
1014 put(PROFILE_GIVEN_NAME, profileName.givenName)
1015 put(PROFILE_FAMILY_NAME, profileName.familyName)
1016 put(PROFILE_JOINED_NAME, profileName.toString())
1017
1018 if (profileKey != null) {
1019 put(PROFILE_KEY, profileKey)
1020 } else {
1021 Log.w(TAG, "Avoided attempt to apply null profile key in account record update!")
1022 }
1023
1024 put(USERNAME, update.new.username)
1025 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(update.new.id.raw))
1026
1027 if (update.new.hasUnknownFields()) {
1028 put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(Objects.requireNonNull(update.new.serializeUnknownFields())))
1029 } else {
1030 putNull(STORAGE_SERVICE_PROTO)
1031 }
1032 }
1033
1034 if (update.new.username != null) {
1035 writableDatabase
1036 .update(TABLE_NAME)
1037 .values(USERNAME to null)
1038 .where("$USERNAME = ?", update.new.username!!)
1039 .run()
1040 }
1041
1042 val updateCount = writableDatabase.update(TABLE_NAME, values, "$STORAGE_SERVICE_ID = ?", arrayOf(Base64.encodeWithPadding(update.old.id.raw)))
1043 if (updateCount < 1) {
1044 throw AssertionError("Account update didn't match any rows!")
1045 }
1046
1047 if (remoteKey != localKey) {
1048 Log.i(TAG, "Our own profile key was changed during a storage sync.", Throwable())
1049 runPostSuccessfulTransaction { ProfileUtil.handleSelfProfileKeyChange() }
1050 }
1051
1052 threads.applyStorageSyncUpdate(Recipient.self().id, update.new)
1053 Recipient.self().live().refresh()
1054 }
1055
1056 /**
1057 * Removes storageIds from unregistered recipients who were unregistered more than [UNREGISTERED_LIFESPAN] ago.
1058 * @return The number of rows affected.
1059 */
1060 fun removeStorageIdsFromOldUnregisteredRecipients(now: Long): Int {
1061 return writableDatabase
1062 .update(TABLE_NAME)
1063 .values(STORAGE_SERVICE_ID to null)
1064 .where("$STORAGE_SERVICE_ID NOT NULL AND $UNREGISTERED_TIMESTAMP > 0 AND $UNREGISTERED_TIMESTAMP < ?", now - UNREGISTERED_LIFESPAN)
1065 .run()
1066 }
1067
1068 /**
1069 * Removes storageIds from unregistered contacts that have storageIds in the provided collection.
1070 * @return The number of updated rows.
1071 */
1072 fun removeStorageIdsFromLocalOnlyUnregisteredRecipients(storageIds: Collection<StorageId>): Int {
1073 val values = contentValuesOf(STORAGE_SERVICE_ID to null)
1074 var updated = 0
1075
1076 SqlUtil.buildCollectionQuery(STORAGE_SERVICE_ID, storageIds.map { Base64.encodeWithPadding(it.raw) }, "$UNREGISTERED_TIMESTAMP > 0 AND")
1077 .forEach {
1078 updated += writableDatabase.update(TABLE_NAME, values, it.where, it.whereArgs)
1079 }
1080
1081 return updated
1082 }
1083
1084 /**
1085 * Takes a mapping of old->new phone numbers and updates the table to match.
1086 * Intended to be used to handle changing number formats.
1087 */
1088 fun rewritePhoneNumbers(mapping: Map<String, String>) {
1089 if (mapping.isEmpty()) return
1090
1091 Log.i(TAG, "Rewriting ${mapping.size} phone numbers.")
1092
1093 writableDatabase.withinTransaction {
1094 for ((originalE164, updatedE164) in mapping) {
1095 writableDatabase.update(TABLE_NAME)
1096 .values(E164 to updatedE164)
1097 .where("$E164 = ?", originalE164)
1098 .run(SQLiteDatabase.CONFLICT_IGNORE)
1099 }
1100 }
1101 }
1102
1103 private fun getByStorageKeyOrThrow(storageKey: ByteArray): RecipientId {
1104 val query = "$STORAGE_SERVICE_ID = ?"
1105 val args = arrayOf(Base64.encodeWithPadding(storageKey))
1106
1107 readableDatabase.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null).use { cursor ->
1108 return if (cursor != null && cursor.moveToFirst()) {
1109 val id = cursor.getLong(cursor.getColumnIndexOrThrow(ID))
1110 RecipientId.from(id)
1111 } else {
1112 throw AssertionError("No recipient with that storage key!")
1113 }
1114 }
1115 }
1116
1117 private fun GroupV2Record.StorySendMode.toShowAsStoryState(): ShowAsStoryState {
1118 return when (this) {
1119 GroupV2Record.StorySendMode.DEFAULT -> ShowAsStoryState.IF_ACTIVE
1120 GroupV2Record.StorySendMode.DISABLED -> ShowAsStoryState.NEVER
1121 GroupV2Record.StorySendMode.ENABLED -> ShowAsStoryState.ALWAYS
1122 else -> ShowAsStoryState.IF_ACTIVE
1123 }
1124 }
1125
1126 private fun getRecordForSync(query: String?, args: Array<String>?): List<RecipientRecord> {
1127 val table =
1128 """
1129 $TABLE_NAME LEFT OUTER JOIN ${IdentityTable.TABLE_NAME} ON ($TABLE_NAME.$ACI_COLUMN = ${IdentityTable.TABLE_NAME}.${IdentityTable.ADDRESS} OR ($TABLE_NAME.$ACI_COLUMN IS NULL AND $TABLE_NAME.$PNI_COLUMN = ${IdentityTable.TABLE_NAME}.${IdentityTable.ADDRESS}))
1130 LEFT OUTER JOIN ${GroupTable.TABLE_NAME} ON $TABLE_NAME.$GROUP_ID = ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID}
1131 LEFT OUTER JOIN ${ThreadTable.TABLE_NAME} ON $TABLE_NAME.$ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID}
1132 """
1133 val out: MutableList<RecipientRecord> = ArrayList()
1134 val columns: Array<String> = TYPED_RECIPIENT_PROJECTION + arrayOf(
1135 SYSTEM_NICKNAME,
1136 "$TABLE_NAME.$STORAGE_SERVICE_PROTO",
1137 "$TABLE_NAME.$UNREGISTERED_TIMESTAMP",
1138 "$TABLE_NAME.$PNI_SIGNATURE_VERIFIED",
1139 "${GroupTable.TABLE_NAME}.${GroupTable.V2_MASTER_KEY}",
1140 "${ThreadTable.TABLE_NAME}.${ThreadTable.ARCHIVED}",
1141 "${ThreadTable.TABLE_NAME}.${ThreadTable.READ}",
1142 "${IdentityTable.TABLE_NAME}.${IdentityTable.VERIFIED} AS $IDENTITY_STATUS",
1143 "${IdentityTable.TABLE_NAME}.${IdentityTable.IDENTITY_KEY} AS $IDENTITY_KEY"
1144 )
1145
1146 readableDatabase.query(table, columns, query, args, "$TABLE_NAME.$ID", null, null).use { cursor ->
1147 while (cursor != null && cursor.moveToNext()) {
1148 out.add(RecipientTableCursorUtil.getRecord(context, cursor))
1149 }
1150 }
1151
1152 return out
1153 }
1154
1155 /**
1156 * @return All storage ids for ContactRecords, excluding the ones that need to be deleted.
1157 */
1158 fun getContactStorageSyncIds(): List<StorageId> {
1159 return ArrayList(getContactStorageSyncIdsMap().values)
1160 }
1161
1162 /**
1163 * @return All storage IDs for synced records, excluding the ones that need to be deleted.
1164 */
1165 fun getContactStorageSyncIdsMap(): Map<RecipientId, StorageId> {
1166 val out: MutableMap<RecipientId, StorageId> = HashMap()
1167
1168 readableDatabase
1169 .select(ID, STORAGE_SERVICE_ID, TYPE)
1170 .from(TABLE_NAME)
1171 .where(
1172 """
1173 $STORAGE_SERVICE_ID NOT NULL AND (
1174 ($TYPE = ? AND ($ACI_COLUMN NOT NULL OR $PNI_COLUMN NOT NULL) AND $ID != ?)
1175 OR
1176 $TYPE = ?
1177 OR
1178 $DISTRIBUTION_LIST_ID NOT NULL AND $DISTRIBUTION_LIST_ID IN (
1179 SELECT ${DistributionListTables.ListTable.ID}
1180 FROM ${DistributionListTables.ListTable.TABLE_NAME}
1181 )
1182 )
1183 """,
1184 RecipientType.INDIVIDUAL.id,
1185 Recipient.self().id,
1186 RecipientType.GV1.id
1187 )
1188 .run()
1189 .use { cursor ->
1190 while (cursor.moveToNext()) {
1191 val id = RecipientId.from(cursor.requireLong(ID))
1192 val encodedKey = cursor.requireNonNullString(STORAGE_SERVICE_ID)
1193 val recipientType = RecipientType.fromId(cursor.requireInt(TYPE))
1194 val key = Base64.decodeOrThrow(encodedKey)
1195
1196 when (recipientType) {
1197 RecipientType.INDIVIDUAL -> out[id] = StorageId.forContact(key)
1198 RecipientType.GV1 -> out[id] = StorageId.forGroupV1(key)
1199 RecipientType.DISTRIBUTION_LIST -> out[id] = StorageId.forStoryDistributionList(key)
1200 else -> throw AssertionError()
1201 }
1202 }
1203 }
1204
1205 for (id in groups.getAllGroupV2Ids()) {
1206 val recipient = Recipient.externalGroupExact(id)
1207 val recipientId = recipient.id
1208 val existing: RecipientRecord = getRecordForSync(recipientId) ?: throw AssertionError()
1209 val key = existing.storageId ?: throw AssertionError()
1210 out[recipientId] = StorageId.forGroupV2(key)
1211 }
1212
1213 return out
1214 }
1215
1216 /**
1217 * Given a collection of [RecipientId]s, this will do an efficient bulk query to find all matching E164s.
1218 * If one cannot be found, no error thrown, it will just be omitted.
1219 */
1220 fun getE164sForIds(ids: Collection<RecipientId>): Set<String> {
1221 val queries: List<SqlUtil.Query> = SqlUtil.buildCustomCollectionQuery(
1222 "$ID = ?",
1223 ids.map { arrayOf(it.serialize()) }.toList()
1224 )
1225
1226 val out: MutableSet<String> = mutableSetOf()
1227
1228 for (query in queries) {
1229 readableDatabase.query(TABLE_NAME, arrayOf(E164), query.where, query.whereArgs, null, null, null).use { cursor ->
1230 while (cursor.moveToNext()) {
1231 val e164: String? = cursor.requireString(E164)
1232 if (e164 != null) {
1233 out.add(e164)
1234 }
1235 }
1236 }
1237 }
1238
1239 return out
1240 }
1241
1242 /**
1243 * @param clearInfoForMissingContacts If true, this will clear any saved contact details for any recipient that hasn't been updated
1244 * by the time finish() is called. Basically this should be true for full syncs and false for
1245 * partial syncs.
1246 */
1247 fun beginBulkSystemContactUpdate(clearInfoForMissingContacts: Boolean): BulkOperationsHandle {
1248 writableDatabase.beginTransaction()
1249
1250 if (clearInfoForMissingContacts) {
1251 writableDatabase
1252 .update(TABLE_NAME)
1253 .values(SYSTEM_INFO_PENDING to 1)
1254 .where("$SYSTEM_CONTACT_URI NOT NULL")
1255 .run()
1256 }
1257
1258 return BulkOperationsHandle(writableDatabase)
1259 }
1260
1261 fun onUpdatedChatColors(chatColors: ChatColors) {
1262 val where = "$CUSTOM_CHAT_COLORS_ID = ?"
1263 val args = SqlUtil.buildArgs(chatColors.id.longValue)
1264 val updated: MutableList<RecipientId> = LinkedList()
1265
1266 readableDatabase.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null).use { cursor ->
1267 while (cursor != null && cursor.moveToNext()) {
1268 updated.add(RecipientId.from(cursor.requireLong(ID)))
1269 }
1270 }
1271
1272 if (updated.isEmpty()) {
1273 Log.d(TAG, "No recipients utilizing updated chat color.")
1274 } else {
1275 val values = ContentValues(2).apply {
1276 put(CHAT_COLORS, chatColors.serialize().encode())
1277 put(CUSTOM_CHAT_COLORS_ID, chatColors.id.longValue)
1278 }
1279
1280 writableDatabase.update(TABLE_NAME, values, where, args)
1281
1282 for (recipientId in updated) {
1283 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
1284 }
1285 }
1286 }
1287
1288 fun onDeletedChatColors(chatColors: ChatColors) {
1289 val where = "$CUSTOM_CHAT_COLORS_ID = ?"
1290 val args = SqlUtil.buildArgs(chatColors.id.longValue)
1291 val updated: MutableList<RecipientId> = LinkedList()
1292
1293 readableDatabase.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null).use { cursor ->
1294 while (cursor != null && cursor.moveToNext()) {
1295 updated.add(RecipientId.from(cursor.requireLong(ID)))
1296 }
1297 }
1298
1299 if (updated.isEmpty()) {
1300 Log.d(TAG, "No recipients utilizing deleted chat color.")
1301 } else {
1302 val values = ContentValues(2).apply {
1303 put(CHAT_COLORS, null as ByteArray?)
1304 put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue)
1305 }
1306
1307 writableDatabase.update(TABLE_NAME, values, where, args)
1308
1309 for (recipientId in updated) {
1310 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
1311 }
1312 }
1313 }
1314
1315 fun getColorUsageCount(chatColorsId: ChatColors.Id): Int {
1316 val where = "$CUSTOM_CHAT_COLORS_ID = ?"
1317 val args = SqlUtil.buildArgs(chatColorsId.longValue)
1318
1319 readableDatabase.query(TABLE_NAME, arrayOf("COUNT(*)"), where, args, null, null, null).use { cursor ->
1320 return if (cursor.moveToFirst()) {
1321 cursor.getInt(0)
1322 } else {
1323 0
1324 }
1325 }
1326 }
1327
1328 fun clearAllColors() {
1329 val database = writableDatabase
1330 val where = "$CUSTOM_CHAT_COLORS_ID != ?"
1331 val args = SqlUtil.buildArgs(ChatColors.Id.NotSet.longValue)
1332 val toUpdate: MutableList<RecipientId> = LinkedList()
1333
1334 database.query(TABLE_NAME, SqlUtil.buildArgs(ID), where, args, null, null, null).use { cursor ->
1335 while (cursor != null && cursor.moveToNext()) {
1336 toUpdate.add(RecipientId.from(cursor.requireLong(ID)))
1337 }
1338 }
1339
1340 if (toUpdate.isEmpty()) {
1341 return
1342 }
1343
1344 val values = ContentValues().apply {
1345 put(CHAT_COLORS, null as ByteArray?)
1346 put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue)
1347 }
1348 database.update(TABLE_NAME, values, where, args)
1349
1350 for (id in toUpdate) {
1351 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1352 }
1353 }
1354
1355 fun clearColor(id: RecipientId) {
1356 val values = ContentValues().apply {
1357 put(CHAT_COLORS, null as ByteArray?)
1358 put(CUSTOM_CHAT_COLORS_ID, ChatColors.Id.NotSet.longValue)
1359 }
1360 if (update(id, values)) {
1361 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1362 }
1363 }
1364
1365 fun setColor(id: RecipientId, color: ChatColors) {
1366 val values = ContentValues().apply {
1367 put(CHAT_COLORS, color.serialize().encode())
1368 put(CUSTOM_CHAT_COLORS_ID, color.id.longValue)
1369 }
1370 if (update(id, values)) {
1371 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1372 }
1373 }
1374
1375 fun setBlocked(id: RecipientId, blocked: Boolean) {
1376 val values = ContentValues().apply {
1377 put(BLOCKED, if (blocked) 1 else 0)
1378 }
1379 if (update(id, values)) {
1380 rotateStorageId(id)
1381 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1382 }
1383 }
1384
1385 fun setMessageRingtone(id: RecipientId, notification: Uri?) {
1386 val values = ContentValues().apply {
1387 put(MESSAGE_RINGTONE, notification?.toString())
1388 }
1389 if (update(id, values)) {
1390 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1391 }
1392 }
1393
1394 fun setCallRingtone(id: RecipientId, ringtone: Uri?) {
1395 val values = ContentValues().apply {
1396 put(CALL_RINGTONE, ringtone?.toString())
1397 }
1398 if (update(id, values)) {
1399 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1400 }
1401 }
1402
1403 fun setMessageVibrate(id: RecipientId, enabled: VibrateState) {
1404 val values = ContentValues().apply {
1405 put(MESSAGE_VIBRATE, enabled.id)
1406 }
1407 if (update(id, values)) {
1408 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1409 }
1410 }
1411
1412 fun setCallVibrate(id: RecipientId, enabled: VibrateState) {
1413 val values = ContentValues().apply {
1414 put(CALL_VIBRATE, enabled.id)
1415 }
1416 if (update(id, values)) {
1417 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1418 }
1419 }
1420
1421 fun setMuted(id: RecipientId, until: Long) {
1422 val values = ContentValues().apply {
1423 put(MUTE_UNTIL, until)
1424 }
1425
1426 if (update(id, values)) {
1427 rotateStorageId(id)
1428 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1429 }
1430
1431 StorageSyncHelper.scheduleSyncForDataChange()
1432 }
1433
1434 fun setMuted(ids: Collection<RecipientId>, until: Long) {
1435 val db = writableDatabase
1436
1437 db.beginTransaction()
1438 try {
1439 val query = SqlUtil.buildSingleCollectionQuery(ID, ids)
1440 val values = ContentValues().apply {
1441 put(MUTE_UNTIL, until)
1442 }
1443
1444 db.update(TABLE_NAME, values, query.where, query.whereArgs)
1445 for (id in ids) {
1446 rotateStorageId(id)
1447 }
1448
1449 db.setTransactionSuccessful()
1450 } finally {
1451 db.endTransaction()
1452 }
1453
1454 for (id in ids) {
1455 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1456 }
1457
1458 StorageSyncHelper.scheduleSyncForDataChange()
1459 }
1460
1461 fun setExpireMessages(id: RecipientId, expiration: Int) {
1462 val values = ContentValues(1).apply {
1463 put(MESSAGE_EXPIRATION_TIME, expiration)
1464 }
1465 if (update(id, values)) {
1466 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1467 }
1468 }
1469
1470 fun setUnidentifiedAccessMode(id: RecipientId, unidentifiedAccessMode: UnidentifiedAccessMode) {
1471 val values = ContentValues(1).apply {
1472 put(SEALED_SENDER_MODE, unidentifiedAccessMode.mode)
1473 }
1474 if (update(id, values)) {
1475 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1476 }
1477 }
1478
1479 fun setLastSessionResetTime(id: RecipientId, lastResetTime: DeviceLastResetTime) {
1480 val values = ContentValues(1).apply {
1481 put(LAST_SESSION_RESET, lastResetTime.encode())
1482 }
1483 update(id, values)
1484 }
1485
1486 fun getLastSessionResetTimes(id: RecipientId): DeviceLastResetTime {
1487 readableDatabase.query(TABLE_NAME, arrayOf(LAST_SESSION_RESET), ID_WHERE, SqlUtil.buildArgs(id), null, null, null).use { cursor ->
1488 if (cursor.moveToFirst()) {
1489 return try {
1490 val serialized = cursor.requireBlob(LAST_SESSION_RESET)
1491 if (serialized != null) {
1492 DeviceLastResetTime.ADAPTER.decode(serialized)
1493 } else {
1494 DeviceLastResetTime()
1495 }
1496 } catch (e: IOException) {
1497 Log.w(TAG, e)
1498 DeviceLastResetTime()
1499 }
1500 }
1501 }
1502
1503 return DeviceLastResetTime()
1504 }
1505
1506 fun setBadges(id: RecipientId, badges: List<Badge>) {
1507 val badgeList = BadgeList(badges = badges.map { toDatabaseBadge(it) })
1508
1509 val values = ContentValues(1).apply {
1510 put(BADGES, badgeList.encode())
1511 }
1512
1513 if (update(id, values)) {
1514 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1515 }
1516 }
1517
1518 fun setCapabilities(id: RecipientId, capabilities: SignalServiceProfile.Capabilities) {
1519 val values = ContentValues(1).apply {
1520 put(CAPABILITIES, maskCapabilitiesToLong(capabilities))
1521 }
1522
1523 if (update(id, values)) {
1524 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1525 }
1526 }
1527
1528 fun setMentionSetting(id: RecipientId, mentionSetting: MentionSetting) {
1529 val values = ContentValues().apply {
1530 put(MENTION_SETTING, mentionSetting.id)
1531 }
1532 if (update(id, values)) {
1533 rotateStorageId(id)
1534 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1535 StorageSyncHelper.scheduleSyncForDataChange()
1536 }
1537 }
1538
1539 /**
1540 * Updates the profile key.
1541 *
1542 * If it changes, it clears out the profile key credential and resets the unidentified access mode.
1543 * @return true iff changed.
1544 */
1545 fun setProfileKey(id: RecipientId, profileKey: ProfileKey): Boolean {
1546 val selection = "$ID = ?"
1547 val args = arrayOf(id.serialize())
1548 val encodedProfileKey = Base64.encodeWithPadding(profileKey.serialize())
1549 val valuesToCompare = ContentValues(1).apply {
1550 put(PROFILE_KEY, encodedProfileKey)
1551 }
1552 val valuesToSet = ContentValues(3).apply {
1553 put(PROFILE_KEY, encodedProfileKey)
1554 putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
1555 put(SEALED_SENDER_MODE, UnidentifiedAccessMode.UNKNOWN.mode)
1556 }
1557
1558 val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, valuesToCompare)
1559
1560 if (update(updateQuery, valuesToSet)) {
1561 rotateStorageId(id)
1562 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1563 StorageSyncHelper.scheduleSyncForDataChange()
1564
1565 if (id == Recipient.self().id) {
1566 Log.i(TAG, "Our own profile key was changed.", Throwable())
1567 runPostSuccessfulTransaction { ProfileUtil.handleSelfProfileKeyChange() }
1568 }
1569
1570 return true
1571 }
1572 return false
1573 }
1574
1575 /**
1576 * Sets the profile key iff currently null.
1577 *
1578 * If it sets it, it also clears out the profile key credential and resets the unidentified access mode.
1579 * @return true iff changed.
1580 */
1581 fun setProfileKeyIfAbsent(id: RecipientId, profileKey: ProfileKey): Boolean {
1582 val selection = "$ID = ? AND $PROFILE_KEY is NULL"
1583 val args = arrayOf(id.serialize())
1584 val valuesToSet = ContentValues(3).apply {
1585 put(PROFILE_KEY, Base64.encodeWithPadding(profileKey.serialize()))
1586 putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
1587 put(SEALED_SENDER_MODE, UnidentifiedAccessMode.UNKNOWN.mode)
1588 }
1589
1590 if (writableDatabase.update(TABLE_NAME, valuesToSet, selection, args) > 0) {
1591 rotateStorageId(id)
1592 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1593 return true
1594 } else {
1595 return false
1596 }
1597 }
1598
1599 /**
1600 * Updates the profile key credential as long as the profile key matches.
1601 */
1602 fun setProfileKeyCredential(
1603 id: RecipientId,
1604 profileKey: ProfileKey,
1605 expiringProfileKeyCredential: ExpiringProfileKeyCredential
1606 ): Boolean {
1607 val selection = "$ID = ? AND $PROFILE_KEY = ?"
1608 val args = arrayOf(id.serialize(), Base64.encodeWithPadding(profileKey.serialize()))
1609 val columnData = ExpiringProfileKeyCredentialColumnData.Builder()
1610 .profileKey(profileKey.serialize().toByteString())
1611 .expiringProfileKeyCredential(expiringProfileKeyCredential.serialize().toByteString())
1612 .build()
1613 val values = ContentValues(1).apply {
1614 put(EXPIRING_PROFILE_KEY_CREDENTIAL, Base64.encodeWithPadding(columnData.encode()))
1615 }
1616 val updateQuery = SqlUtil.buildTrueUpdateQuery(selection, args, values)
1617
1618 val updated = update(updateQuery, values)
1619 if (updated) {
1620 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1621 }
1622
1623 return updated
1624 }
1625
1626 fun clearProfileKeyCredential(id: RecipientId) {
1627 val values = ContentValues(1)
1628 values.putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
1629 if (update(id, values)) {
1630 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1631 }
1632 }
1633
1634 /**
1635 * Fills in gaps (nulls) in profile key knowledge from new profile keys.
1636 *
1637 *
1638 * If from authoritative source, this will overwrite local, otherwise it will only write to the
1639 * database if missing.
1640 */
1641 fun persistProfileKeySet(profileKeySet: ProfileKeySet): Set<RecipientId> {
1642 val profileKeys = profileKeySet.profileKeys
1643 val authoritativeProfileKeys = profileKeySet.authoritativeProfileKeys
1644 val totalKeys = profileKeys.size + authoritativeProfileKeys.size
1645
1646 if (totalKeys == 0) {
1647 return emptySet()
1648 }
1649
1650 Log.i(TAG, "Persisting $totalKeys Profile keys, ${authoritativeProfileKeys.size} of which are authoritative")
1651
1652 val updated = HashSet<RecipientId>(totalKeys)
1653 val selfId = Recipient.self().id
1654
1655 for ((key, value) in profileKeys) {
1656 val recipientId = getOrInsertFromServiceId(key)
1657 if (setProfileKeyIfAbsent(recipientId, value)) {
1658 Log.i(TAG, "Learned new profile key")
1659 updated.add(recipientId)
1660 }
1661 }
1662
1663 for ((key, value) in authoritativeProfileKeys) {
1664 val recipientId = getOrInsertFromServiceId(key)
1665
1666 if (selfId == recipientId) {
1667 Log.i(TAG, "Seen authoritative update for self")
1668 if (value != ProfileKeyUtil.getSelfProfileKey()) {
1669 Log.w(TAG, "Seen authoritative update for self that didn't match local, scheduling storage sync")
1670 StorageSyncHelper.scheduleSyncForDataChange()
1671 }
1672 } else {
1673 Log.i(TAG, "Profile key from owner $recipientId")
1674 if (setProfileKey(recipientId, value)) {
1675 Log.i(TAG, "Learned new profile key from owner")
1676 updated.add(recipientId)
1677 }
1678 }
1679 }
1680
1681 return updated
1682 }
1683
1684 fun containsId(id: RecipientId): Boolean {
1685 return readableDatabase
1686 .exists(TABLE_NAME)
1687 .where("$ID = ?", id.serialize())
1688 .run()
1689 }
1690
1691 fun setReportingToken(id: RecipientId, reportingToken: ByteArray) {
1692 val values = ContentValues(1).apply {
1693 put(REPORTING_TOKEN, reportingToken)
1694 }
1695
1696 if (update(id, values)) {
1697 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1698 }
1699 }
1700
1701 fun getReportingToken(id: RecipientId): ByteArray? {
1702 readableDatabase
1703 .select(REPORTING_TOKEN)
1704 .from(TABLE_NAME)
1705 .where(ID_WHERE, id)
1706 .run()
1707 .use { cursor ->
1708 if (cursor.moveToFirst()) {
1709 return cursor.requireBlob(REPORTING_TOKEN)
1710 } else {
1711 return null
1712 }
1713 }
1714 }
1715
1716 fun getSimilarRecipientIds(recipient: Recipient): List<RecipientId> {
1717 val projection = SqlUtil.buildArgs(ID, "COALESCE(NULLIF($SYSTEM_JOINED_NAME, ''), NULLIF($PROFILE_JOINED_NAME, '')) AS checked_name")
1718 val where = "checked_name = ? AND $HIDDEN = ?"
1719 val arguments = SqlUtil.buildArgs(recipient.profileName.toString(), 0)
1720
1721 readableDatabase.query(TABLE_NAME, projection, where, arguments, null, null, null).use { cursor ->
1722 if (cursor == null || cursor.count == 0) {
1723 return emptyList()
1724 }
1725 val results: MutableList<RecipientId> = ArrayList(cursor.count)
1726 while (cursor.moveToNext()) {
1727 results.add(RecipientId.from(cursor.requireLong(ID)))
1728 }
1729 return results
1730 }
1731 }
1732
1733 fun setSystemContactName(id: RecipientId, systemContactName: String) {
1734 val values = ContentValues().apply {
1735 put(SYSTEM_JOINED_NAME, systemContactName)
1736 }
1737 if (update(id, values)) {
1738 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1739 }
1740 }
1741
1742 fun setNicknameAndNote(id: RecipientId, nickname: ProfileName, note: String) {
1743 val contentValues = contentValuesOf(
1744 NICKNAME_GIVEN_NAME to nickname.givenName.nullIfBlank(),
1745 NICKNAME_FAMILY_NAME to nickname.familyName.nullIfBlank(),
1746 NICKNAME_JOINED_NAME to nickname.toString().nullIfBlank(),
1747 NOTE to note.nullIfBlank()
1748 )
1749 if (update(id, contentValues)) {
1750 rotateStorageId(id)
1751 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1752 StorageSyncHelper.scheduleSyncForDataChange()
1753 }
1754 }
1755
1756 fun setProfileName(id: RecipientId, profileName: ProfileName) {
1757 val contentValues = ContentValues(1).apply {
1758 put(PROFILE_GIVEN_NAME, profileName.givenName.nullIfBlank())
1759 put(PROFILE_FAMILY_NAME, profileName.familyName.nullIfBlank())
1760 put(PROFILE_JOINED_NAME, profileName.toString().nullIfBlank())
1761 }
1762 if (update(id, contentValues)) {
1763 rotateStorageId(id)
1764 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1765 StorageSyncHelper.scheduleSyncForDataChange()
1766 }
1767 }
1768
1769 fun setProfileAvatar(id: RecipientId, profileAvatar: String?) {
1770 val contentValues = ContentValues(1).apply {
1771 put(PROFILE_AVATAR, profileAvatar)
1772 }
1773 if (update(id, contentValues)) {
1774 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1775 if (id == Recipient.self().id) {
1776 rotateStorageId(id)
1777 StorageSyncHelper.scheduleSyncForDataChange()
1778 }
1779 }
1780 }
1781
1782 fun setAbout(id: RecipientId, about: String?, emoji: String?) {
1783 val contentValues = ContentValues().apply {
1784 put(ABOUT, about)
1785 put(ABOUT_EMOJI, emoji)
1786 }
1787
1788 if (update(id, contentValues)) {
1789 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1790 }
1791 }
1792
1793 fun markHidden(id: RecipientId, clearProfileKey: Boolean = false, showMessageRequest: Boolean = false) {
1794 val contentValues = if (clearProfileKey) {
1795 contentValuesOf(
1796 HIDDEN to if (showMessageRequest) Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST.serialize() else Recipient.HiddenState.HIDDEN.serialize(),
1797 PROFILE_SHARING to 0,
1798 PROFILE_KEY to null
1799 )
1800 } else {
1801 contentValuesOf(
1802 HIDDEN to if (showMessageRequest) Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST.serialize() else Recipient.HiddenState.HIDDEN.serialize(),
1803 PROFILE_SHARING to 0
1804 )
1805 }
1806
1807 val updated = writableDatabase.update(TABLE_NAME, contentValues, "$ID_WHERE AND $TYPE = ?", SqlUtil.buildArgs(id, RecipientType.INDIVIDUAL.id)) > 0
1808 if (updated) {
1809 SignalDatabase.distributionLists.removeMemberFromAllLists(id)
1810 SignalDatabase.messages.deleteStoriesForRecipient(id)
1811 rotateStorageId(id)
1812 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1813 StorageSyncHelper.scheduleSyncForDataChange()
1814 } else {
1815 Log.w(TAG, "Failed to hide recipient $id")
1816 }
1817 }
1818
1819 fun setProfileSharing(id: RecipientId, enabled: Boolean) {
1820 val contentValues = ContentValues(1).apply {
1821 put(PROFILE_SHARING, if (enabled) 1 else 0)
1822 }
1823
1824 if (enabled) {
1825 contentValues.put(HIDDEN, 0)
1826 }
1827
1828 val profiledUpdated = update(id, contentValues)
1829
1830 if (profiledUpdated && enabled) {
1831 val group = groups.getGroup(id)
1832 if (group.isPresent) {
1833 setHasGroupsInCommon(group.get().members)
1834 }
1835 }
1836
1837 if (profiledUpdated) {
1838 rotateStorageId(id)
1839 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1840 StorageSyncHelper.scheduleSyncForDataChange()
1841 }
1842 }
1843
1844 fun setNotificationChannel(id: RecipientId, notificationChannel: String?) {
1845 val contentValues = ContentValues(1).apply {
1846 put(NOTIFICATION_CHANNEL, notificationChannel)
1847 }
1848 if (update(id, contentValues)) {
1849 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1850 }
1851 }
1852
1853 fun setPhoneNumberSharing(id: RecipientId, phoneNumberSharing: PhoneNumberSharingState) {
1854 val contentValues = contentValuesOf(
1855 PHONE_NUMBER_SHARING to phoneNumberSharing.id
1856 )
1857 if (update(id, contentValues)) {
1858 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1859 }
1860 }
1861
1862 fun resetAllWallpaper() {
1863 val database = writableDatabase
1864 val selection = SqlUtil.buildArgs(ID, WALLPAPER_URI)
1865 val where = "$WALLPAPER IS NOT NULL"
1866 val idWithWallpaper: MutableList<Pair<RecipientId, String?>> = LinkedList()
1867
1868 database.beginTransaction()
1869 try {
1870 database.query(TABLE_NAME, selection, where, null, null, null, null).use { cursor ->
1871 while (cursor != null && cursor.moveToNext()) {
1872 idWithWallpaper.add(
1873 Pair(
1874 RecipientId.from(cursor.requireInt(ID).toLong()),
1875 cursor.optionalString(WALLPAPER_URI).orElse(null)
1876 )
1877 )
1878 }
1879 }
1880
1881 if (idWithWallpaper.isEmpty()) {
1882 return
1883 }
1884
1885 val values = ContentValues(2).apply {
1886 putNull(WALLPAPER_URI)
1887 putNull(WALLPAPER)
1888 }
1889
1890 val rowsUpdated = database.update(TABLE_NAME, values, where, null)
1891 if (rowsUpdated == idWithWallpaper.size) {
1892 for (pair in idWithWallpaper) {
1893 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(pair.first)
1894 if (pair.second != null) {
1895 WallpaperStorage.onWallpaperDeselected(context, Uri.parse(pair.second))
1896 }
1897 }
1898 } else {
1899 throw AssertionError("expected " + idWithWallpaper.size + " but got " + rowsUpdated)
1900 }
1901 } finally {
1902 database.setTransactionSuccessful()
1903 database.endTransaction()
1904 }
1905 }
1906
1907 fun setWallpaper(id: RecipientId, chatWallpaper: ChatWallpaper?) {
1908 setWallpaper(id, chatWallpaper?.serialize())
1909 }
1910
1911 private fun setWallpaper(id: RecipientId, wallpaper: Wallpaper?) {
1912 val existingWallpaperUri = getWallpaperUri(id)
1913 val values = ContentValues().apply {
1914 put(WALLPAPER, wallpaper?.encode())
1915 if (wallpaper?.file_ != null) {
1916 put(WALLPAPER_URI, wallpaper.file_.uri)
1917 } else {
1918 putNull(WALLPAPER_URI)
1919 }
1920 }
1921
1922 if (update(id, values)) {
1923 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
1924 }
1925
1926 if (existingWallpaperUri != null) {
1927 WallpaperStorage.onWallpaperDeselected(context, existingWallpaperUri)
1928 }
1929 }
1930
1931 fun setDimWallpaperInDarkTheme(id: RecipientId, enabled: Boolean) {
1932 val wallpaper = getWallpaper(id) ?: throw IllegalStateException("No wallpaper set for $id")
1933 val updated = wallpaper.newBuilder()
1934 .dimLevelInDarkTheme(if (enabled) ChatWallpaper.FIXED_DIM_LEVEL_FOR_DARK_THEME else 0f)
1935 .build()
1936
1937 setWallpaper(id, updated)
1938 }
1939
1940 private fun getWallpaper(id: RecipientId): Wallpaper? {
1941 readableDatabase.query(TABLE_NAME, arrayOf(WALLPAPER), ID_WHERE, SqlUtil.buildArgs(id), null, null, null).use { cursor ->
1942 if (cursor.moveToFirst()) {
1943 val raw = cursor.requireBlob(WALLPAPER)
1944 return if (raw != null) {
1945 try {
1946 Wallpaper.ADAPTER.decode(raw)
1947 } catch (e: IOException) {
1948 null
1949 }
1950 } else {
1951 null
1952 }
1953 }
1954 }
1955
1956 return null
1957 }
1958
1959 private fun getWallpaperUri(id: RecipientId): Uri? {
1960 val wallpaper = getWallpaper(id)
1961
1962 return if (wallpaper != null && wallpaper.file_ != null) {
1963 Uri.parse(wallpaper.file_.uri)
1964 } else {
1965 null
1966 }
1967 }
1968
1969 fun getWallpaperUriUsageCount(uri: Uri): Int {
1970 val query = "$WALLPAPER_URI = ?"
1971 val args = SqlUtil.buildArgs(uri)
1972
1973 readableDatabase.query(TABLE_NAME, arrayOf("COUNT(*)"), query, args, null, null, null).use { cursor ->
1974 if (cursor.moveToFirst()) {
1975 return cursor.getInt(0)
1976 }
1977 }
1978
1979 return 0
1980 }
1981
1982 fun getPhoneNumberDiscoverability(id: RecipientId): PhoneNumberDiscoverableState? {
1983 return readableDatabase
1984 .select(PHONE_NUMBER_DISCOVERABLE)
1985 .from(TABLE_NAME)
1986 .where("$ID = ?", id)
1987 .run()
1988 .readToSingleObject { PhoneNumberDiscoverableState.fromId(it.requireInt(PHONE_NUMBER_DISCOVERABLE)) }
1989 }
1990
1991 /**
1992 * @return True if setting the phone number resulted in changed recipientId, otherwise false.
1993 */
1994 fun setPhoneNumber(id: RecipientId, e164: String): Boolean {
1995 val db = writableDatabase
1996
1997 db.beginTransaction()
1998 return try {
1999 setPhoneNumberOrThrow(id, e164)
2000 db.setTransactionSuccessful()
2001 false
2002 } catch (e: SQLiteConstraintException) {
2003 Log.w(TAG, "[setPhoneNumber] Hit a conflict when trying to update $id. Possibly merging.")
2004
2005 val existing: RecipientRecord = getRecord(id)
2006 val newId = getAndPossiblyMerge(existing.aci, e164)
2007 Log.w(TAG, "[setPhoneNumber] Resulting id: $newId")
2008
2009 db.setTransactionSuccessful()
2010 newId != existing.id
2011 } finally {
2012 db.endTransaction()
2013 }
2014 }
2015
2016 private fun removePhoneNumber(recipientId: RecipientId) {
2017 val values = ContentValues().apply {
2018 putNull(E164)
2019 putNull(PNI_COLUMN)
2020 }
2021
2022 if (update(recipientId, values)) {
2023 rotateStorageId(recipientId)
2024 }
2025 }
2026
2027 /**
2028 * Should only use if you are confident that this will not result in any contact merging.
2029 */
2030 @Throws(SQLiteConstraintException::class)
2031 fun setPhoneNumberOrThrow(id: RecipientId, e164: String) {
2032 val contentValues = ContentValues(1).apply {
2033 put(E164, e164)
2034 }
2035 if (update(id, contentValues)) {
2036 rotateStorageId(id)
2037 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
2038 StorageSyncHelper.scheduleSyncForDataChange()
2039 }
2040 }
2041
2042 @Throws(SQLiteConstraintException::class)
2043 fun setPhoneNumberOrThrowSilent(id: RecipientId, e164: String) {
2044 val contentValues = ContentValues(1).apply {
2045 put(E164, e164)
2046 }
2047 if (update(id, contentValues)) {
2048 rotateStorageId(id)
2049 }
2050 }
2051
2052 /**
2053 * Associates the provided IDs together. The assumption here is that all of the IDs correspond to the local user and have been verified.
2054 */
2055 fun linkIdsForSelf(aci: ACI, pni: PNI, e164: String) {
2056 val id: RecipientId = getAndPossiblyMerge(aci = aci, pni = pni, e164 = e164, changeSelf = true, pniVerified = true)
2057 updatePendingSelfData(id)
2058 }
2059
2060 /**
2061 * Does *not* handle clearing the recipient cache. It is assumed the caller handles this.
2062 */
2063 fun updateSelfE164(e164: String, pni: PNI) {
2064 val db = writableDatabase
2065
2066 db.beginTransaction()
2067 try {
2068 val id = Recipient.self().id
2069 val newId = getAndPossiblyMerge(aci = SignalStore.account().requireAci(), pni = pni, e164 = e164, pniVerified = true, changeSelf = true)
2070
2071 if (id == newId) {
2072 Log.i(TAG, "[updateSelfPhone] Phone updated for self")
2073 } else {
2074 throw AssertionError("[updateSelfPhone] Self recipient id changed when updating e164. old: $id new: $newId")
2075 }
2076
2077 db.updateAll(TABLE_NAME)
2078 .values(NEEDS_PNI_SIGNATURE to 0)
2079 .run()
2080
2081 SignalDatabase.pendingPniSignatureMessages.deleteAll()
2082
2083 db.setTransactionSuccessful()
2084 } finally {
2085 db.endTransaction()
2086 }
2087 }
2088
2089 fun getUsername(id: RecipientId): String? {
2090 return writableDatabase.query(TABLE_NAME, arrayOf(USERNAME), "$ID = ?", SqlUtil.buildArgs(id), null, null, null).use {
2091 if (it.moveToFirst()) {
2092 it.requireString(USERNAME)
2093 } else {
2094 null
2095 }
2096 }
2097 }
2098
2099 fun setUsername(id: RecipientId, username: String?) {
2100 writableDatabase.withinTransaction {
2101 if (username != null) {
2102 val existingUsername = getByUsername(username)
2103 if (existingUsername.isPresent && id != existingUsername.get()) {
2104 Log.i(TAG, "Username was previously thought to be owned by " + existingUsername.get() + ". Clearing their username.")
2105 setUsername(existingUsername.get(), null)
2106 }
2107 }
2108
2109 if (update(id, contentValuesOf(USERNAME to username))) {
2110 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
2111 rotateStorageId(id)
2112 StorageSyncHelper.scheduleSyncForDataChange()
2113 }
2114 }
2115 }
2116
2117 fun setHideStory(id: RecipientId, hideStory: Boolean) {
2118 updateExtras(id) { it.hideStory(hideStory) }
2119 rotateStorageId(id)
2120 StorageSyncHelper.scheduleSyncForDataChange()
2121 }
2122
2123 fun updateLastStoryViewTimestamp(id: RecipientId) {
2124 updateExtras(id) { it.lastStoryView(System.currentTimeMillis()) }
2125 }
2126
2127 fun clearUsernameIfExists(username: String) {
2128 val existingUsername = getByUsername(username)
2129 if (existingUsername.isPresent) {
2130 setUsername(existingUsername.get(), null)
2131 }
2132 }
2133
2134 fun getAllE164s(): Set<String> {
2135 val results: MutableSet<String> = HashSet()
2136 readableDatabase.query(TABLE_NAME, arrayOf(E164), null, null, null, null, null).use { cursor ->
2137 while (cursor != null && cursor.moveToNext()) {
2138 val number = cursor.getString(cursor.getColumnIndexOrThrow(E164))
2139 if (!TextUtils.isEmpty(number)) {
2140 results.add(number)
2141 }
2142 }
2143 }
2144 return results
2145 }
2146
2147 /** A function that's just to help with some temporary bug investigation. */
2148 private fun getAllPnis(): Set<PNI> {
2149 return readableDatabase
2150 .select(PNI_COLUMN)
2151 .from(TABLE_NAME)
2152 .where("$PNI_COLUMN NOT NULL")
2153 .run()
2154 .readToSet { PNI.parseOrThrow(it.requireString(PNI_COLUMN)) }
2155 }
2156
2157 /**
2158 * Gives you all of the recipientIds of possibly-registered users (i.e. REGISTERED or UNKNOWN) that can be found by the set of
2159 * provided E164s.
2160 */
2161 fun getAllPossiblyRegisteredByE164(e164s: Set<String>): Set<RecipientId> {
2162 val results: MutableSet<RecipientId> = mutableSetOf()
2163 val queries: List<SqlUtil.Query> = SqlUtil.buildCollectionQuery(E164, e164s)
2164
2165 for (query in queries) {
2166 readableDatabase.query(TABLE_NAME, arrayOf(ID, REGISTERED), query.where, query.whereArgs, null, null, null).use { cursor ->
2167 while (cursor.moveToNext()) {
2168 if (RegisteredState.fromId(cursor.requireInt(REGISTERED)) != RegisteredState.NOT_REGISTERED) {
2169 results += RecipientId.from(cursor.requireLong(ID))
2170 }
2171 }
2172 }
2173 }
2174
2175 return results
2176 }
2177
2178 fun setPni(id: RecipientId, pni: PNI) {
2179 writableDatabase
2180 .update(TABLE_NAME)
2181 .values(ACI_COLUMN to pni.toString())
2182 .where("$ID = ? AND ($ACI_COLUMN IS NULL OR $ACI_COLUMN = $PNI_COLUMN)", id)
2183 .run()
2184
2185 writableDatabase
2186 .update(TABLE_NAME)
2187 .values(PNI_COLUMN to pni.toString())
2188 .where("$ID = ?", id)
2189 .run()
2190 }
2191
2192 /**
2193 * @return True if setting the UUID resulted in changed recipientId, otherwise false.
2194 */
2195 fun markRegistered(id: RecipientId, serviceId: ServiceId): Boolean {
2196 val db = writableDatabase
2197
2198 db.beginTransaction()
2199 try {
2200 markRegisteredOrThrow(id, serviceId)
2201 db.setTransactionSuccessful()
2202 return false
2203 } catch (e: SQLiteConstraintException) {
2204 Log.w(TAG, "[markRegistered] Hit a conflict when trying to update $id. Possibly merging.")
2205
2206 val existing = getRecord(id)
2207 val newId = getAndPossiblyMerge(serviceId, existing.e164)
2208 Log.w(TAG, "[markRegistered] Merged into $newId")
2209
2210 db.setTransactionSuccessful()
2211 return newId != existing.id
2212 } finally {
2213 db.endTransaction()
2214 }
2215 }
2216
2217 /**
2218 * Should only use if you are confident that this shouldn't result in any contact merging.
2219 */
2220 fun markRegisteredOrThrow(id: RecipientId, serviceId: ServiceId) {
2221 val contentValues = contentValuesOf(
2222 REGISTERED to RegisteredState.REGISTERED.id,
2223 ACI_COLUMN to serviceId.toString().lowercase(),
2224 UNREGISTERED_TIMESTAMP to 0
2225 )
2226 if (update(id, contentValues)) {
2227 Log.i(TAG, "Newly marked $id as registered.")
2228 setStorageIdIfNotSet(id)
2229 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
2230 }
2231 }
2232
2233 fun markUnregistered(id: RecipientId) {
2234 val record = getRecord(id)
2235
2236 if (record.aci != null && record.pni != null) {
2237 markUnregisteredAndSplit(id, record)
2238 } else {
2239 markUnregisteredWithoutSplit(id)
2240 }
2241 }
2242
2243 /**
2244 * Marks the user unregistered and also splits it into an ACI-only and PNI-only contact.
2245 * This is to allow a new user to register the number with a new ACI.
2246 */
2247 private fun markUnregisteredAndSplit(id: RecipientId, record: RecipientRecord) {
2248 check(record.aci != null && record.pni != null)
2249
2250 val contentValues = contentValuesOf(
2251 REGISTERED to RegisteredState.NOT_REGISTERED.id,
2252 UNREGISTERED_TIMESTAMP to System.currentTimeMillis(),
2253 E164 to null,
2254 PNI_COLUMN to null
2255 )
2256
2257 if (update(id, contentValues)) {
2258 Log.i(TAG, "[WithSplit] Newly marked $id as unregistered.")
2259 markNeedsSync(id)
2260 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
2261 }
2262
2263 val splitId = getAndPossiblyMerge(null, record.pni, record.e164)
2264 Log.i(TAG, "Split off new recipient as $splitId (ACI-only recipient is $id)")
2265 }
2266
2267 /**
2268 * Marks the user unregistered without splitting the contact into an ACI-only and PNI-only contact.
2269 */
2270 private fun markUnregisteredWithoutSplit(id: RecipientId) {
2271 val contentValues = contentValuesOf(
2272 REGISTERED to RegisteredState.NOT_REGISTERED.id,
2273 UNREGISTERED_TIMESTAMP to System.currentTimeMillis()
2274 )
2275
2276 if (update(id, contentValues)) {
2277 Log.i(TAG, "[WithoutSplit] Newly marked $id as unregistered.")
2278 markNeedsSync(id)
2279 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
2280 }
2281 }
2282
2283 /**
2284 * Removes the target recipient's E164+PNI, then creates a new recipient with that E164+PNI.
2285 * Done so we can match a split contact during storage sync.
2286 */
2287 fun splitForStorageSyncIfNecessary(aci: ACI) {
2288 val recipientId = getByAci(aci).getOrNull() ?: return
2289 val record = getRecord(recipientId)
2290
2291 if (record.pni == null && record.e164 == null) {
2292 return
2293 }
2294
2295 Log.i(TAG, "Splitting $recipientId for storage sync", true)
2296
2297 writableDatabase
2298 .update(TABLE_NAME)
2299 .values(
2300 PNI_COLUMN to null,
2301 E164 to null
2302 )
2303 .where("$ID = ?", record.id)
2304 .run()
2305
2306 getAndPossiblyMerge(null, record.pni, record.e164)
2307 }
2308
2309 fun processIndividualCdsLookup(aci: ACI?, pni: PNI, e164: String): RecipientId {
2310 return getAndPossiblyMerge(aci = aci, pni = pni, e164 = e164)
2311 }
2312
2313 /**
2314 * Processes CDSv2 results, merging recipients as necessary. Does not mark users as
2315 * registered.
2316 *
2317 * @return A set of [RecipientId]s that were updated/inserted.
2318 */
2319 fun bulkProcessCdsResult(mapping: Map<String, CdsV2Result>): Set<RecipientId> {
2320 val ids: MutableSet<RecipientId> = mutableSetOf()
2321 val db = writableDatabase
2322
2323 db.beginTransaction()
2324 try {
2325 for ((e164, result) in mapping) {
2326 ids += getAndPossiblyMerge(aci = result.aci, pni = result.pni, e164 = e164, pniVerified = false, changeSelf = false)
2327 }
2328
2329 db.setTransactionSuccessful()
2330 } finally {
2331 db.endTransaction()
2332 }
2333
2334 return ids
2335 }
2336
2337 fun bulkUpdatedRegisteredStatus(registered: Set<RecipientId>, unregistered: Collection<RecipientId>) {
2338 writableDatabase.withinTransaction {
2339 val existingRegistered: Set<RecipientId> = getRegistered()
2340 val needsMarkRegistered: Set<RecipientId> = registered - existingRegistered
2341
2342 val registeredValues = contentValuesOf(
2343 REGISTERED to RegisteredState.REGISTERED.id,
2344 UNREGISTERED_TIMESTAMP to 0
2345 )
2346
2347 val newlyRegistered: MutableSet<RecipientId> = mutableSetOf()
2348
2349 for (id in needsMarkRegistered) {
2350 if (update(id, registeredValues)) {
2351 newlyRegistered += id
2352 setStorageIdIfNotSet(id)
2353 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
2354 }
2355 }
2356
2357 if (newlyRegistered.isNotEmpty()) {
2358 Log.i(TAG, "Newly marked the following as registered: $newlyRegistered")
2359 }
2360
2361 val newlyUnregistered: MutableSet<RecipientId> = mutableSetOf()
2362
2363 val unregisteredValues = contentValuesOf(
2364 REGISTERED to RegisteredState.NOT_REGISTERED.id,
2365 UNREGISTERED_TIMESTAMP to System.currentTimeMillis()
2366 )
2367
2368 for (id in unregistered) {
2369 if (update(id, unregisteredValues)) {
2370 newlyUnregistered += id
2371 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
2372 }
2373 }
2374
2375 if (newlyUnregistered.isNotEmpty()) {
2376 Log.i(TAG, "Newly marked the following as unregistered: $newlyUnregistered")
2377 }
2378 }
2379 }
2380
2381 /**
2382 * Takes a tuple of (e164, pni, aci) and incorporates it into our database.
2383 * It is assumed that we are in a transaction.
2384 *
2385 * @return The [RecipientId] of the resulting recipient.
2386 */
2387 @VisibleForTesting
2388 fun processPnpTuple(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false): ProcessPnpTupleResult {
2389 val changeSet: PnpChangeSet = processPnpTupleToChangeSet(e164, pni, aci, pniVerified, changeSelf)
2390
2391 val affectedIds: MutableSet<RecipientId> = mutableSetOf()
2392 val oldIds: MutableSet<RecipientId> = mutableSetOf()
2393 var changedNumberId: RecipientId? = null
2394
2395 for (operation in changeSet.operations) {
2396 @Exhaustive
2397 when (operation) {
2398 is PnpOperation.RemoveE164,
2399 is PnpOperation.RemovePni,
2400 is PnpOperation.SetAci,
2401 is PnpOperation.SetE164,
2402 is PnpOperation.SetPni -> {
2403 affectedIds.add(operation.recipientId)
2404 }
2405
2406 is PnpOperation.Merge -> {
2407 oldIds.add(operation.secondaryId)
2408 affectedIds.add(operation.primaryId)
2409 }
2410
2411 is PnpOperation.SessionSwitchoverInsert -> {}
2412 is PnpOperation.ChangeNumberInsert -> changedNumberId = operation.recipientId
2413 }
2414 }
2415
2416 val finalId: RecipientId = writePnpChangeSetToDisk(changeSet, pni, pniVerified)
2417
2418 return ProcessPnpTupleResult(
2419 finalId = finalId,
2420 requiredInsert = changeSet.id is PnpIdResolver.PnpInsert,
2421 affectedIds = affectedIds,
2422 oldIds = oldIds,
2423 changedNumberId = changedNumberId,
2424 operations = changeSet.operations.toList(),
2425 breadCrumbs = changeSet.breadCrumbs
2426 )
2427 }
2428
2429 @VisibleForTesting
2430 fun writePnpChangeSetToDisk(changeSet: PnpChangeSet, inputPni: PNI?, pniVerified: Boolean): RecipientId {
2431 var hadThreadMerge = false
2432 for (operation in changeSet.operations) {
2433 @Exhaustive
2434 when (operation) {
2435 is PnpOperation.RemoveE164 -> {
2436 writableDatabase
2437 .update(TABLE_NAME)
2438 .values(E164 to null)
2439 .where("$ID = ?", operation.recipientId)
2440 .run()
2441 }
2442
2443 is PnpOperation.RemovePni -> {
2444 writableDatabase
2445 .update(TABLE_NAME)
2446 .values(
2447 PNI_COLUMN to null,
2448 PNI_SIGNATURE_VERIFIED to 0
2449 )
2450 .where("$ID = ?", operation.recipientId)
2451 .run()
2452 }
2453
2454 is PnpOperation.SetAci -> {
2455 writableDatabase
2456 .update(TABLE_NAME)
2457 .values(
2458 ACI_COLUMN to operation.aci.toString(),
2459 REGISTERED to RegisteredState.REGISTERED.id,
2460 UNREGISTERED_TIMESTAMP to 0,
2461 PNI_SIGNATURE_VERIFIED to pniVerified.toInt()
2462 )
2463 .where("$ID = ?", operation.recipientId)
2464 .run()
2465 }
2466
2467 is PnpOperation.SetE164 -> {
2468 writableDatabase
2469 .update(TABLE_NAME)
2470 .values(E164 to operation.e164)
2471 .where("$ID = ?", operation.recipientId)
2472 .run()
2473 }
2474
2475 is PnpOperation.SetPni -> {
2476 writableDatabase
2477 .update(TABLE_NAME)
2478 .values(
2479 PNI_COLUMN to operation.pni.toString(),
2480 REGISTERED to RegisteredState.REGISTERED.id,
2481 UNREGISTERED_TIMESTAMP to 0,
2482 PNI_SIGNATURE_VERIFIED to 0
2483 )
2484 .where("$ID = ?", operation.recipientId)
2485 .run()
2486 }
2487
2488 is PnpOperation.Merge -> {
2489 val mergeResult: MergeResult = merge(operation.primaryId, operation.secondaryId, inputPni, pniVerified)
2490 hadThreadMerge = hadThreadMerge || mergeResult.neededThreadMerge
2491 }
2492
2493 is PnpOperation.SessionSwitchoverInsert -> {
2494 if (hadThreadMerge) {
2495 Log.d(TAG, "Skipping SSE insert because we already had a thread merge event.")
2496 } else {
2497 val threadId: Long? = threads.getThreadIdFor(operation.recipientId)
2498 if (threadId != null) {
2499 val event = SessionSwitchoverEvent(e164 = operation.e164 ?: "")
2500 try {
2501 SignalDatabase.messages.insertSessionSwitchoverEvent(operation.recipientId, threadId, event)
2502 } catch (e: Exception) {
2503 Log.e(TAG, "About to crash! Breadcrumbs: ${changeSet.breadCrumbs}, Operations: ${changeSet.operations}, ID: ${changeSet.id}", true)
2504
2505 val allPnis: Set<PNI> = getAllPnis()
2506 val pnisWithSessions: Set<PNI> = sessions.findAllThatHaveAnySession(allPnis)
2507 Log.e(TAG, "We know of ${allPnis.size} PNIs, and there are sessions with ${pnisWithSessions.size} of them.", true)
2508
2509 val record = getRecord(operation.recipientId)
2510 Log.e(TAG, "ID: ${record.id}, E164: ${record.e164}, ACI: ${record.aci}, PNI: ${record.pni}, Registered: ${record.registered}", true)
2511
2512 if (record.aci != null && record.aci == SignalStore.account().aci) {
2513 if (pnisWithSessions.contains(SignalStore.account().pni!!)) {
2514 throw SseWithSelfAci(e)
2515 } else {
2516 throw SseWithSelfAciNoSession(e)
2517 }
2518 }
2519
2520 if (record.pni != null && record.pni == SignalStore.account().pni) {
2521 if (pnisWithSessions.contains(SignalStore.account().pni!!)) {
2522 throw SseWithSelfPni(e)
2523 } else {
2524 throw SseWithSelfPniNoSession(e)
2525 }
2526 }
2527
2528 if (record.e164 != null && record.e164 == SignalStore.account().e164) {
2529 if (pnisWithSessions.contains(SignalStore.account().pni!!)) {
2530 throw SseWithSelfE164(e)
2531 } else {
2532 throw SseWithSelfE164NoSession(e)
2533 }
2534 }
2535
2536 if (pnisWithSessions.isEmpty()) {
2537 throw SseWithNoPniSessionsException(e)
2538 } else if (pnisWithSessions.size == 1) {
2539 if (pnisWithSessions.first() == SignalStore.account().pni) {
2540 throw SseWithASinglePniSessionForSelfException(e)
2541 } else {
2542 throw SseWithASinglePniSessionException(e)
2543 }
2544 } else {
2545 throw SseWithMultiplePniSessionsException(e)
2546 }
2547 }
2548 }
2549 }
2550 }
2551
2552 is PnpOperation.ChangeNumberInsert -> {
2553 if (changeSet.id is PnpIdResolver.PnpNoopId) {
2554 SignalDatabase.messages.insertNumberChangeMessages(changeSet.id.recipientId)
2555 } else {
2556 throw IllegalStateException("There's a change number event on a newly-inserted recipient?")
2557 }
2558 }
2559 }
2560 }
2561
2562 return when (changeSet.id) {
2563 is PnpIdResolver.PnpNoopId -> {
2564 changeSet.id.recipientId
2565 }
2566
2567 is PnpIdResolver.PnpInsert -> {
2568 val id: Long = writableDatabase.insert(TABLE_NAME, null, buildContentValuesForNewUser(changeSet.id.e164, changeSet.id.pni, changeSet.id.aci, pniVerified))
2569 RecipientId.from(id)
2570 }
2571 }
2572 }
2573
2574 /**
2575 * Takes a tuple of (e164, pni, aci) and converts that into a list of changes that would need to be made to
2576 * merge that data into our database.
2577 *
2578 * The database will be read, but not written to, during this function.
2579 * It is assumed that we are in a transaction.
2580 */
2581 @VisibleForTesting
2582 fun processPnpTupleToChangeSet(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean = false): PnpChangeSet {
2583 check(e164 != null || pni != null || aci != null) { "Must provide at least one field!" }
2584
2585 val breadCrumbs: MutableList<String> = mutableListOf()
2586
2587 val partialData = PnpDataSet(
2588 e164 = e164,
2589 pni = pni,
2590 aci = aci,
2591 byE164 = e164?.let { getByE164(it).orElse(null) },
2592 byPni = pni?.let { getByPni(it).orElse(null) },
2593 byAci = aci?.let { getByAci(it).orElse(null) }
2594 )
2595
2596 val allRequiredDbFields: MutableList<RecipientId?> = mutableListOf()
2597 if (e164 != null) {
2598 allRequiredDbFields += partialData.byE164
2599 }
2600 if (aci != null) {
2601 allRequiredDbFields += partialData.byAci
2602 }
2603 if (pni != null) {
2604 allRequiredDbFields += partialData.byPni
2605 }
2606
2607 val allRequiredDbFieldPopulated: Boolean = allRequiredDbFields.all { it != null }
2608
2609 // All IDs agree and the database is up-to-date
2610 if (partialData.commonId != null && allRequiredDbFieldPopulated) {
2611 breadCrumbs.add("CommonIdAndUpToDate")
2612 return PnpChangeSet(id = PnpIdResolver.PnpNoopId(partialData.commonId), breadCrumbs = breadCrumbs)
2613 }
2614
2615 // All ID's agree, but we need to update the database
2616 if (partialData.commonId != null && !allRequiredDbFieldPopulated) {
2617 breadCrumbs.add("CommonIdButNeedsUpdate")
2618 return processNonMergePnpUpdate(e164, pni, aci, commonId = partialData.commonId, pniVerified = pniVerified, changeSelf = changeSelf, breadCrumbs = breadCrumbs)
2619 }
2620
2621 // Nothing matches
2622 if (partialData.byE164 == null && partialData.byPni == null && partialData.byAci == null) {
2623 breadCrumbs += "NothingMatches"
2624 return PnpChangeSet(
2625 id = PnpIdResolver.PnpInsert(
2626 e164 = e164,
2627 pni = pni,
2628 aci = aci
2629 ),
2630 breadCrumbs = breadCrumbs
2631 )
2632 }
2633
2634 // At this point, we know that records have been found for at least two of the fields,
2635 // and that there are at least two unique IDs among the records.
2636 //
2637 // In other words, *some* sort of merging of data must now occur.
2638 // It may be that some data just gets shuffled around, or it may be that
2639 // two or more records get merged into one record, with the others being deleted.
2640
2641 breadCrumbs += "NeedsMerge"
2642
2643 val preMergeData = partialData.copy(
2644 e164Record = partialData.byE164?.let { getRecord(it) },
2645 pniRecord = partialData.byPni?.let { getRecord(it) },
2646 aciRecord = partialData.byAci?.let { getRecord(it) }
2647 )
2648
2649 check(preMergeData.commonId == null)
2650 check(listOfNotNull(preMergeData.byE164, preMergeData.byPni, preMergeData.byAci).size >= 2)
2651
2652 val operations: LinkedHashSet<PnpOperation> = linkedSetOf()
2653
2654 operations += processPossibleE164PniMerge(preMergeData, pniVerified, changeSelf, breadCrumbs)
2655 operations += processPossiblePniAciMerge(preMergeData.perform(operations), pniVerified, changeSelf, breadCrumbs)
2656 operations += processPossibleE164AciMerge(preMergeData.perform(operations), pniVerified, changeSelf, breadCrumbs)
2657
2658 val postMergeData: PnpDataSet = preMergeData.perform(operations)
2659 val primaryId: RecipientId = listOfNotNull(postMergeData.byAci, postMergeData.byE164, postMergeData.byPni).first()
2660
2661 if (postMergeData.byAci == null && aci != null) {
2662 breadCrumbs += "FinalUpdateAci"
2663 operations += PnpOperation.SetAci(
2664 recipientId = primaryId,
2665 aci = aci
2666 )
2667
2668 if (needsSessionSwitchoverEvent(pniVerified, postMergeData.pni, aci)) {
2669 breadCrumbs += "FinalUpdateAciSSE"
2670 operations += PnpOperation.SessionSwitchoverInsert(
2671 recipientId = primaryId,
2672 e164 = postMergeData.e164
2673 )
2674 }
2675 }
2676
2677 if (postMergeData.byE164 == null && e164 != null && (changeSelf || notSelf(e164, pni, aci))) {
2678 breadCrumbs += "FinalUpdateE164"
2679 operations += PnpOperation.SetE164(
2680 recipientId = primaryId,
2681 e164 = e164
2682 )
2683 }
2684
2685 if (postMergeData.byPni == null && pni != null) {
2686 breadCrumbs += "FinalUpdatePni"
2687 operations += PnpOperation.SetPni(
2688 recipientId = primaryId,
2689 pni = pni
2690 )
2691 }
2692
2693 sessionSwitchoverEventIfNeeded(pniVerified, preMergeData.pniRecord, postMergeData.pniRecord)?.let {
2694 breadCrumbs += "FinalUpdateSSEPniRecord"
2695 operations += it
2696 }
2697
2698 sessionSwitchoverEventIfNeeded(pniVerified, preMergeData.aciRecord, postMergeData.aciRecord)?.let {
2699 breadCrumbs += "FinalUpdateSSEPniAciRecord"
2700 operations += it
2701 }
2702
2703 return PnpChangeSet(
2704 id = PnpIdResolver.PnpNoopId(primaryId),
2705 operations = operations,
2706 breadCrumbs = breadCrumbs
2707 )
2708 }
2709
2710 /**
2711 * If all of the non-null fields match a single recipient, return it. Otherwise null.
2712 */
2713 private fun getRecipientIdIfAllFieldsMatch(aci: ACI?, pni: PNI?, e164: String?): RecipientId? {
2714 if (aci == null && pni == null && e164 == null) {
2715 return null
2716 }
2717
2718 val columns = listOf(
2719 ACI_COLUMN to aci?.toString(),
2720 PNI_COLUMN to pni?.toString(),
2721 E164 to e164
2722 ).filter { it.second != null }
2723
2724 val query = columns
2725 .map { "${it.first} = ?" }
2726 .joinToString(separator = " AND ")
2727
2728 val args: Array<String> = columns.map { it.second!! }.toTypedArray()
2729
2730 val ids: List<Long> = readableDatabase
2731 .select(ID)
2732 .from(TABLE_NAME)
2733 .where(query, args)
2734 .run()
2735 .readToList { it.requireLong(ID) }
2736
2737 return if (ids.size == 1) {
2738 RecipientId.from(ids[0])
2739 } else {
2740 null
2741 }
2742 }
2743
2744 /**
2745 * A session switchover event indicates a situation where we start communicating with a different session that we were before.
2746 * If a switchover is "verified" (i.e. proven safe cryptographically by the sender), then this doesn't require a user-visible event.
2747 * But if it's not verified and we're switching from one established session to another, the user needs to be aware.
2748 */
2749 private fun needsSessionSwitchoverEvent(pniVerified: Boolean, oldServiceId: ServiceId?, newServiceId: ServiceId?): Boolean {
2750 return !pniVerified &&
2751 oldServiceId != null &&
2752 newServiceId != null &&
2753 oldServiceId != newServiceId &&
2754 sessions.hasAnySessionFor(oldServiceId.toString()) &&
2755 identities.getIdentityStoreRecord(oldServiceId)?.identityKey != identities.getIdentityStoreRecord(newServiceId)?.identityKey
2756 }
2757
2758 /**
2759 * For details on SSE's, see [needsSessionSwitchoverEvent]. This method is just a helper around comparing service ID's from two
2760 * records and turning it into a possible event.
2761 */
2762 private fun sessionSwitchoverEventIfNeeded(pniVerified: Boolean, oldRecord: RecipientRecord?, newRecord: RecipientRecord?): PnpOperation? {
2763 return if (oldRecord != null && newRecord != null && oldRecord.serviceId == oldRecord.pni && newRecord.serviceId == newRecord.aci && needsSessionSwitchoverEvent(pniVerified, oldRecord.serviceId, newRecord.serviceId)) {
2764 PnpOperation.SessionSwitchoverInsert(
2765 recipientId = newRecord.id,
2766 e164 = newRecord.e164
2767 )
2768 } else {
2769 null
2770 }
2771 }
2772
2773 private fun notSelf(data: PnpDataSet): Boolean {
2774 return notSelf(data.e164, data.pni, data.aci)
2775 }
2776
2777 private fun notSelf(e164: String?, pni: PNI?, aci: ACI?): Boolean {
2778 return (e164 == null || e164 != SignalStore.account().e164) &&
2779 (pni == null || pni != SignalStore.account().pni) &&
2780 (aci == null || aci != SignalStore.account().aci)
2781 }
2782
2783 private fun isSelf(data: PnpDataSet): Boolean {
2784 return isSelf(data.e164, data.pni, data.aci)
2785 }
2786
2787 private fun isSelf(e164: String?, pni: PNI?, aci: ACI?): Boolean {
2788 return (e164 != null && e164 == SignalStore.account().e164) ||
2789 (pni != null && pni == SignalStore.account().pni) ||
2790 (aci != null && aci == SignalStore.account().aci)
2791 }
2792
2793 private fun processNonMergePnpUpdate(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean, changeSelf: Boolean, commonId: RecipientId, breadCrumbs: MutableList<String>): PnpChangeSet {
2794 val record: RecipientRecord = getRecord(commonId)
2795
2796 val operations: LinkedHashSet<PnpOperation> = linkedSetOf()
2797
2798 // 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.
2799 if (aci != null && aci != record.aci && record.aci != null) {
2800 breadCrumbs += "AciDoesNotMatchCommonRecord"
2801
2802 if (record.e164 == e164 && (changeSelf || notSelf(e164, pni, aci))) {
2803 breadCrumbs += "StealingE164"
2804 operations += PnpOperation.RemoveE164(record.id)
2805 operations += PnpOperation.RemovePni(record.id)
2806 } else if (record.pni == pni) {
2807 breadCrumbs += "StealingPni"
2808 operations += PnpOperation.RemovePni(record.id)
2809 }
2810
2811 val insertE164: String? = if (changeSelf || notSelf(e164, pni, aci)) e164 else null
2812 val insertPni: PNI? = if (changeSelf || notSelf(e164, pni, aci)) pni else null
2813
2814 return PnpChangeSet(
2815 id = PnpIdResolver.PnpInsert(insertE164, insertPni, aci),
2816 operations = operations,
2817 breadCrumbs = breadCrumbs
2818 )
2819 }
2820
2821 var updatedNumber = false
2822 if (e164 != null && record.e164 != e164 && (changeSelf || notSelf(e164, pni, aci))) {
2823 operations += PnpOperation.SetE164(
2824 recipientId = commonId,
2825 e164 = e164
2826 )
2827 updatedNumber = true
2828 }
2829
2830 if (pni != null && record.pni != pni) {
2831 operations += PnpOperation.SetPni(
2832 recipientId = commonId,
2833 pni = pni
2834 )
2835 }
2836
2837 if (aci != null && record.aci != aci) {
2838 operations += PnpOperation.SetAci(
2839 recipientId = commonId,
2840 aci = aci
2841 )
2842 }
2843
2844 if (record.e164 != null && updatedNumber && notSelf(e164, pni, aci) && !record.isBlocked) {
2845 breadCrumbs += "NonMergeChangeNumber"
2846 operations += PnpOperation.ChangeNumberInsert(
2847 recipientId = commonId,
2848 oldE164 = record.e164,
2849 newE164 = e164!!
2850 )
2851 }
2852
2853 val oldServiceId: ServiceId? = record.aci ?: record.pni
2854 val newServiceId: ServiceId? = aci ?: pni ?: oldServiceId
2855
2856 if (needsSessionSwitchoverEvent(pniVerified, oldServiceId, newServiceId)) {
2857 breadCrumbs += "NonMergeSSE"
2858 operations += PnpOperation.SessionSwitchoverInsert(recipientId = commonId, e164 = record.e164 ?: e164)
2859 }
2860
2861 return PnpChangeSet(
2862 id = PnpIdResolver.PnpNoopId(commonId),
2863 operations = operations,
2864 breadCrumbs = breadCrumbs
2865 )
2866 }
2867
2868 /**
2869 * Resolves any possible E164-PNI conflicts/merges. In these situations, the E164-based row is more dominant
2870 * and can "steal" data from PNI-based rows, or merge PNI-based rows into itself.
2871 *
2872 * We do have to be careful when merging/stealing data to leave possible ACI's that could be on the PNI
2873 * row alone: remember, ACI's are forever-bound to a given RecipientId.
2874 */
2875 private fun processPossibleE164PniMerge(data: PnpDataSet, pniVerified: Boolean, changeSelf: Boolean, breadCrumbs: MutableList<String>): LinkedHashSet<PnpOperation> {
2876 // Filter to ensure that we're only looking at situations where a PNI and E164 record both exist but do not match
2877 if (data.pni == null || data.byPni == null || data.pniRecord == null || data.e164 == null || data.byE164 == null || data.e164Record == null || data.e164Record.id == data.pniRecord.id) {
2878 return linkedSetOf()
2879 }
2880
2881 // We have found records for both the E164 and PNI, and they're different
2882 breadCrumbs += "E164PniMerge"
2883
2884 if (!changeSelf && isSelf(data)) {
2885 breadCrumbs += "ChangeSelfPreventsE164PniMerge"
2886 return linkedSetOf()
2887 }
2888
2889 val operations: LinkedHashSet<PnpOperation> = linkedSetOf()
2890
2891 if (data.pniRecord.pniOnly()) {
2892 // The PNI record only has a single identifier. We know we must merge.
2893 breadCrumbs += "PniOnly"
2894
2895 if (data.e164Record.pni != null) {
2896 // The e164 record we're merging into has a PNI already. This means that we've entered an 'unstable PNI mapping' scenario.
2897 // This isn't expected, but we need to handle it gracefully and merge the two rows together.
2898 operations += PnpOperation.RemovePni(data.byE164)
2899
2900 if (needsSessionSwitchoverEvent(pniVerified, data.e164Record.pni, data.pni)) {
2901 breadCrumbs += "E164IdentityMismatchesPniIdentity"
2902 operations += PnpOperation.SessionSwitchoverInsert(data.byE164, data.e164)
2903 }
2904 }
2905
2906 operations += PnpOperation.Merge(
2907 primaryId = data.byE164,
2908 secondaryId = data.byPni
2909 )
2910 } else {
2911 // The record we're taking data from also has either an ACI or e164, so we need to leave that data behind
2912
2913 breadCrumbs += if (data.pniRecord.aci != null && data.pniRecord.e164 != null) {
2914 "PniRecordHasE164AndAci"
2915 } else if (data.pniRecord.aci != null) {
2916 "PniRecordHasAci"
2917 } else {
2918 "PniRecordHasE164"
2919 }
2920
2921 // Move the PNI from the PNI record to the e164 record
2922 operations += PnpOperation.RemovePni(data.byPni)
2923 operations += PnpOperation.SetPni(
2924 recipientId = data.byE164,
2925 pni = data.pni
2926 )
2927
2928 // By migrating the PNI to the e164 record, we may cause an SSE
2929 if (needsSessionSwitchoverEvent(pniVerified, data.e164Record.serviceId, data.e164Record.aci ?: data.pni)) {
2930 breadCrumbs += "PniE164SSE"
2931 operations += PnpOperation.SessionSwitchoverInsert(recipientId = data.byE164, e164 = data.e164Record.e164)
2932 }
2933
2934 // This is a defensive move where we put an SSE in the session we stole the PNI from and where we're moving it to in order
2935 // to avoid a multi-step PNI swap. You could imagine that we might remove the PNI in this function call, but then add one back
2936 // in the next function call, and each step on it's own would think that no SSE is necessary. Given that this scenario only
2937 // happens with an unstable PNI-E164 mapping, we get out ahead of it by putting an SSE in both preemptively.
2938 if (!pniVerified && data.pniRecord.aci == null && sessions.hasAnySessionFor(data.pni.toString())) {
2939 breadCrumbs += "DefensiveSSEByPni"
2940 operations += PnpOperation.SessionSwitchoverInsert(recipientId = data.byPni, e164 = data.pniRecord.e164)
2941
2942 if (data.e164Record.aci == null) {
2943 breadCrumbs += "DefensiveSSEByE164"
2944 operations += PnpOperation.SessionSwitchoverInsert(recipientId = data.byE164, e164 = data.e164Record.e164)
2945 }
2946 }
2947 }
2948
2949 return operations
2950 }
2951
2952 /**
2953 * Resolves any possible PNI-ACI conflicts/merges. In these situations, the ACI-based row is more dominant
2954 * and can "steal" data from PNI-based rows, or merge PNI-based rows into itself.
2955 */
2956 private fun processPossiblePniAciMerge(data: PnpDataSet, pniVerified: Boolean, changeSelf: Boolean, breadCrumbs: MutableList<String>): LinkedHashSet<PnpOperation> {
2957 // Filter to ensure that we're only looking at situations where a PNI and ACI record both exist but do not match
2958 if (data.pni == null || data.byPni == null || data.pniRecord == null || data.aci == null || data.byAci == null || data.aciRecord == null || data.pniRecord.id == data.aciRecord.id) {
2959 return linkedSetOf()
2960 }
2961
2962 // We have found records for both the PNI and ACI, and they're different
2963 breadCrumbs += "PniAciMerge"
2964
2965 if (!changeSelf && isSelf(data)) {
2966 breadCrumbs += "ChangeSelfPreventsPniAciMerge"
2967 return linkedSetOf()
2968 }
2969
2970 val operations: LinkedHashSet<PnpOperation> = linkedSetOf()
2971
2972 // The PNI record only has a single identifier. We know we must merge.
2973 if (data.pniRecord.pniOnly()) {
2974 breadCrumbs += "PniOnly"
2975
2976 if (data.aciRecord.pni != null) {
2977 operations += PnpOperation.RemovePni(data.byAci)
2978 }
2979
2980 operations += PnpOperation.Merge(
2981 primaryId = data.byAci,
2982 secondaryId = data.byPni
2983 )
2984 } else if (data.pniRecord.aci == null && (data.e164 == null || data.pniRecord.e164 == data.e164)) {
2985 // The PNI has no ACI and possibly some e164. We're going to be stealing all of it's fields,
2986 // so this is basically a merge with a little bit of extra prep.
2987 breadCrumbs += "PniRecordHasNoAci"
2988
2989 if (data.aciRecord.pni != null) {
2990 operations += PnpOperation.RemovePni(data.byAci)
2991 }
2992
2993 val newE164 = data.pniRecord.e164 ?: data.e164
2994
2995 if (data.aciRecord.e164 != null && data.aciRecord.e164 != newE164 && newE164 != null) {
2996 operations += PnpOperation.RemoveE164(data.byAci)
2997
2998 // This also becomes a change number event
2999 if (notSelf(data) && !data.aciRecord.isBlocked) {
3000 breadCrumbs += "PniMatchingE164NoAciChangeNumber"
3001 operations += PnpOperation.ChangeNumberInsert(
3002 recipientId = data.byAci,
3003 oldE164 = data.aciRecord.e164,
3004 newE164 = newE164
3005 )
3006 }
3007 }
3008
3009 operations += PnpOperation.Merge(
3010 primaryId = data.byAci,
3011 secondaryId = data.byPni
3012 )
3013 } else {
3014 // The PNI record has a different ACI, meaning we need to steal what we need and leave the rest behind
3015
3016 breadCrumbs += if (data.pniRecord.aci != null && data.pniRecord.e164 != data.e164) {
3017 "PniRecordHasAci"
3018 } else if (data.pniRecord.aci != null) {
3019 "PniRecordHasAci"
3020 } else {
3021 "PniRecordHasNonMatchingE164"
3022 }
3023
3024 operations += PnpOperation.RemovePni(data.byPni)
3025
3026 operations += PnpOperation.SetPni(
3027 recipientId = data.byAci,
3028 pni = data.pni
3029 )
3030
3031 if (data.e164 != null && data.aciRecord.e164 != data.e164) {
3032 if (data.pniRecord.e164 == data.e164) {
3033 operations += PnpOperation.RemoveE164(
3034 recipientId = data.byPni
3035 )
3036 }
3037
3038 operations += PnpOperation.SetE164(
3039 recipientId = data.byAci,
3040 e164 = data.e164
3041 )
3042
3043 if (data.aciRecord.e164 != null && notSelf(data) && !data.aciRecord.isBlocked) {
3044 breadCrumbs += "PniHasExtraFieldChangeNumber"
3045 operations += PnpOperation.ChangeNumberInsert(
3046 recipientId = data.byAci,
3047 oldE164 = data.aciRecord.e164,
3048 newE164 = data.e164
3049 )
3050 }
3051 }
3052 }
3053
3054 return operations
3055 }
3056
3057 /**
3058 * Resolves any possible E164-ACI conflicts/merges. In these situations, the ACI-based row is more dominant
3059 * and can "steal" data from E164-based rows, or merge E164-based rows into itself.
3060 */
3061 private fun processPossibleE164AciMerge(data: PnpDataSet, pniVerified: Boolean, changeSelf: Boolean, breadCrumbs: MutableList<String>): List<PnpOperation> {
3062 // Filter to ensure that we're only looking at situations where a E164 and ACI record both exist but do not match
3063 if (data.e164 == null || data.byE164 == null || data.e164Record == null || data.aci == null || data.byAci == null || data.aciRecord == null || data.e164Record.id == data.aciRecord.id) {
3064 return emptyList()
3065 }
3066
3067 // We have found records for both the E164 and ACI, and they're different
3068 breadCrumbs += "E164AciMerge"
3069
3070 if (!changeSelf && isSelf(data)) {
3071 breadCrumbs += "ChangeSelfPreventsE164AciMerge"
3072 return emptyList()
3073 }
3074
3075 val operations: MutableList<PnpOperation> = mutableListOf()
3076
3077 // The E164 record only has a single identifier. We know we must merge.
3078 if (data.e164Record.e164Only()) {
3079 breadCrumbs += "E164Only"
3080
3081 if (data.aciRecord.e164 != null && data.aciRecord.e164 != data.e164) {
3082 operations += PnpOperation.RemoveE164(data.byAci)
3083 }
3084
3085 operations += PnpOperation.Merge(
3086 primaryId = data.byAci,
3087 secondaryId = data.byE164
3088 )
3089
3090 if (data.aciRecord.e164 != null && data.aciRecord.e164 != data.e164 && notSelf(data) && !data.aciRecord.isBlocked) {
3091 breadCrumbs += "E164OnlyChangeNumber"
3092 operations += PnpOperation.ChangeNumberInsert(
3093 recipientId = data.byAci,
3094 oldE164 = data.aciRecord.e164,
3095 newE164 = data.e164
3096 )
3097 }
3098 } else if (data.e164Record.pni != null && data.e164Record.pni == data.pni) {
3099 // The E164 record also has the PNI on it. We're going to be stealing both fields,
3100 // so this is basically a merge with a little bit of extra prep.
3101 breadCrumbs += "E164RecordHasMatchingPni"
3102
3103 if (data.aciRecord.pni != null) {
3104 operations += PnpOperation.RemovePni(data.byAci)
3105 }
3106
3107 if (data.aciRecord.e164 != null && data.aciRecord.e164 != data.e164) {
3108 operations += PnpOperation.RemoveE164(data.byAci)
3109 }
3110
3111 operations += PnpOperation.Merge(
3112 primaryId = data.byAci,
3113 secondaryId = data.byE164
3114 )
3115
3116 if (data.aciRecord.e164 != null && data.aciRecord.e164 != data.e164 && notSelf(data) && !data.aciRecord.isBlocked) {
3117 breadCrumbs += "E164MatchingPniChangeNumber"
3118 operations += PnpOperation.ChangeNumberInsert(
3119 recipientId = data.byAci,
3120 oldE164 = data.aciRecord.e164,
3121 newE164 = data.e164
3122 )
3123 }
3124 } else {
3125 check(data.e164Record.pni == null || data.e164Record.pni != data.pni)
3126 breadCrumbs += "E164RecordHasNonMatchingPni"
3127
3128 operations += PnpOperation.RemoveE164(data.byE164)
3129
3130 operations += PnpOperation.SetE164(
3131 recipientId = data.byAci,
3132 e164 = data.e164
3133 )
3134
3135 if (data.aciRecord.e164 != null && data.aciRecord.e164 != data.e164 && notSelf(data) && !data.aciRecord.isBlocked) {
3136 breadCrumbs += "E164NonMatchingPniChangeNumber"
3137 operations += PnpOperation.ChangeNumberInsert(
3138 recipientId = data.byAci,
3139 oldE164 = data.aciRecord.e164,
3140 newE164 = data.e164
3141 )
3142 }
3143 }
3144
3145 return operations
3146 }
3147
3148 fun getRegistered(): Set<RecipientId> {
3149 val results: MutableSet<RecipientId> = mutableSetOf()
3150
3151 readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$REGISTERED = ? and $HIDDEN = ?", arrayOf("1", "${Recipient.HiddenState.NOT_HIDDEN.serialize()}"), null, null, null).use { cursor ->
3152 while (cursor != null && cursor.moveToNext()) {
3153 results += RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))
3154 }
3155 }
3156
3157 return results
3158 }
3159
3160 fun getSystemContacts(): List<RecipientId> {
3161 val results: MutableList<RecipientId> = LinkedList()
3162
3163 readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$SYSTEM_JOINED_NAME IS NOT NULL AND $SYSTEM_JOINED_NAME != \"\"", null, null, null, null).use { cursor ->
3164 while (cursor != null && cursor.moveToNext()) {
3165 results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))))
3166 }
3167 }
3168
3169 return results
3170 }
3171
3172 /** True if the recipient exists and is muted, otherwise false. */
3173 fun isMuted(id: RecipientId): Boolean {
3174 return readableDatabase
3175 .select(MUTE_UNTIL)
3176 .from(TABLE_NAME)
3177 .where("$ID = ?", id)
3178 .run()
3179 .readToSingleBoolean()
3180 }
3181
3182 /** All e164's that are eligible for having a signal link added to their system contact entry. */
3183 fun getE164sForSystemContactLinks(): Set<String> {
3184 return readableDatabase
3185 .select(E164)
3186 .from(TABLE_NAME)
3187 .where("$REGISTERED = ? and $HIDDEN = ? AND $E164 NOT NULL AND $PHONE_NUMBER_DISCOVERABLE != ?", RegisteredState.REGISTERED.id, Recipient.HiddenState.NOT_HIDDEN.serialize(), PhoneNumberDiscoverableState.NOT_DISCOVERABLE)
3188 .run()
3189 .readToSet { cursor ->
3190 cursor.requireNonNullString(E164)
3191 }
3192 }
3193
3194 /**
3195 * We no longer automatically generate a chat color. This method is used only
3196 * in the case of a legacy migration and otherwise should not be called.
3197 */
3198 @Deprecated("")
3199 fun updateSystemContactColors() {
3200 val db = readableDatabase
3201 val updates: MutableMap<RecipientId, ChatColors> = HashMap()
3202
3203 db.beginTransaction()
3204 try {
3205 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 ->
3206 while (cursor != null && cursor.moveToNext()) {
3207 val id = cursor.requireLong(ID)
3208 val serializedColor = cursor.requireString("color")
3209 val customChatColorsId = cursor.requireLong(CUSTOM_CHAT_COLORS_ID)
3210 val serializedChatColors = cursor.requireBlob(CHAT_COLORS)
3211 var chatColors: ChatColors? = if (serializedChatColors != null) {
3212 try {
3213 forChatColor(forLongValue(customChatColorsId), ChatColor.ADAPTER.decode(serializedChatColors))
3214 } catch (e: IOException) {
3215 null
3216 }
3217 } else {
3218 null
3219 }
3220
3221 if (chatColors != null) {
3222 return
3223 }
3224
3225 chatColors = if (serializedColor != null) {
3226 try {
3227 getChatColors(MaterialColor.fromSerialized(serializedColor))
3228 } catch (e: UnknownColorException) {
3229 return
3230 }
3231 } else {
3232 return
3233 }
3234
3235 val contentValues = ContentValues().apply {
3236 put(CHAT_COLORS, chatColors.serialize().encode())
3237 put(CUSTOM_CHAT_COLORS_ID, chatColors.id.longValue)
3238 }
3239 db.update(TABLE_NAME, contentValues, "$ID = ?", arrayOf(id.toString()))
3240 updates[RecipientId.from(id)] = chatColors
3241 }
3242 }
3243 } finally {
3244 db.setTransactionSuccessful()
3245 db.endTransaction()
3246 updates.entries.forEach { ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(it.key) }
3247 }
3248 }
3249
3250 fun queryByInternalFields(query: String): List<RecipientRecord> {
3251 if (query.isBlank()) {
3252 return emptyList()
3253 }
3254
3255 return readableDatabase
3256 .select()
3257 .from(TABLE_NAME)
3258 .where("$ID LIKE ? OR $ACI_COLUMN LIKE ? OR $PNI_COLUMN LIKE ?", "%$query%", "%$query%", "%$query%")
3259 .run()
3260 .readToList { cursor ->
3261 RecipientTableCursorUtil.getRecord(context, cursor)
3262 }
3263 }
3264
3265 fun getSignalContacts(includeSelf: Boolean): Cursor? {
3266 return getSignalContacts(includeSelf, "$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $USERNAME, $E164")
3267 }
3268
3269 fun getSignalContactsCount(includeSelf: Boolean): Int {
3270 return getSignalContacts(includeSelf)?.count ?: 0
3271 }
3272
3273 private fun getSignalContacts(includeSelf: Boolean, orderBy: String? = null): Cursor? {
3274 val searchSelection = ContactSearchSelection.Builder()
3275 .withRegistered(true)
3276 .withGroups(false)
3277 .excludeId(if (includeSelf) null else Recipient.self().id)
3278 .build()
3279 val selection = searchSelection.where
3280 val args = searchSelection.args
3281 return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
3282 }
3283
3284 fun querySignalContacts(contactSearchQuery: ContactSearchQuery): Cursor? {
3285 val query = SqlUtil.buildCaseInsensitiveGlobPattern(contactSearchQuery.query)
3286
3287 val searchSelection = ContactSearchSelection.Builder()
3288 .withRegistered(true)
3289 .withGroups(false)
3290 .excludeId(if (contactSearchQuery.includeSelf) null else Recipient.self().id)
3291 .withSearchQuery(query)
3292 .build()
3293 val selection = searchSelection.where
3294 val args = searchSelection.args
3295 val orderBy = "${if (contactSearchQuery.contactSearchSortOrder == ContactSearchSortOrder.RECENCY) "${ThreadTable.TABLE_NAME}.${ThreadTable.DATE} DESC, " else ""}$SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $E164"
3296
3297 //language=roomsql
3298 val join = if (contactSearchQuery.contactSearchSortOrder == ContactSearchSortOrder.RECENCY) {
3299 "LEFT OUTER JOIN ${ThreadTable.TABLE_NAME} ON ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$ID"
3300 } else {
3301 ""
3302 }
3303
3304 return if (contactSearchQuery.contactSearchSortOrder == ContactSearchSortOrder.RECENCY) {
3305 val ambiguous = listOf(ID)
3306 val projection = SEARCH_PROJECTION.map {
3307 if (it in ambiguous) "$TABLE_NAME.$it" else it
3308 } + "${ThreadTable.TABLE_NAME}.${ThreadTable.DATE}"
3309
3310 //language=roomsql
3311 readableDatabase.query(
3312 """
3313 SELECT ${projection.joinToString(",")}
3314 FROM $TABLE_NAME
3315 $join
3316 WHERE $selection
3317 ORDER BY $orderBy
3318 """.trimIndent(),
3319 args
3320 )
3321 } else {
3322 readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
3323 }
3324 }
3325
3326 fun querySignalContactLetterHeaders(inputQuery: String, includeSelf: Boolean, includePush: Boolean, includeSms: Boolean): Map<RecipientId, String> {
3327 val searchSelection = ContactSearchSelection.Builder()
3328 .withRegistered(includePush)
3329 .withNonRegistered(includeSms)
3330 .withGroups(false)
3331 .excludeId(if (includeSelf) null else Recipient.self().id)
3332 .withSearchQuery(inputQuery)
3333 .build()
3334
3335 return readableDatabase.query(
3336 """
3337 SELECT
3338 _id,
3339 UPPER(SUBSTR($SORT_NAME, 0, 2)) AS letter_header
3340 FROM (
3341 SELECT ${SEARCH_PROJECTION.joinToString(", ")}
3342 FROM recipient
3343 WHERE ${searchSelection.where}
3344 ORDER BY $SORT_NAME, $SYSTEM_JOINED_NAME, $SEARCH_PROFILE_NAME, $E164
3345 )
3346 GROUP BY letter_header
3347 """,
3348 searchSelection.args
3349 ).use { cursor ->
3350 if (cursor.count == 0) {
3351 emptyMap()
3352 } else {
3353 val resultsMap = mutableMapOf<RecipientId, String>()
3354 while (cursor.moveToNext()) {
3355 cursor.requireString("letter_header")?.let {
3356 resultsMap[RecipientId.from(cursor.requireLong(ID))] = it
3357 }
3358 }
3359
3360 resultsMap
3361 }
3362 }
3363 }
3364
3365 fun getNonSignalContacts(): Cursor? {
3366 val searchSelection = ContactSearchSelection.Builder().withNonRegistered(true)
3367 .withGroups(false)
3368 .build()
3369 val selection = searchSelection.where
3370 val args = searchSelection.args
3371 val orderBy = "$SYSTEM_JOINED_NAME, $E164"
3372 return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
3373 }
3374
3375 fun queryNonSignalContacts(inputQuery: String): Cursor? {
3376 val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
3377 val searchSelection = ContactSearchSelection.Builder()
3378 .withNonRegistered(true)
3379 .withGroups(false)
3380 .withSearchQuery(query)
3381 .build()
3382 val selection = searchSelection.where
3383 val args = searchSelection.args
3384 val orderBy = "$SYSTEM_JOINED_NAME, $E164"
3385 return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
3386 }
3387
3388 fun getNonGroupContacts(includeSelf: Boolean): Cursor? {
3389 val searchSelection = ContactSearchSelection.Builder()
3390 .withRegistered(true)
3391 .withNonRegistered(true)
3392 .withGroups(false)
3393 .excludeId(if (includeSelf) null else Recipient.self().id)
3394 .build()
3395 val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + E164
3396 return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, searchSelection.where, searchSelection.args, null, null, orderBy)
3397 }
3398
3399 fun queryNonGroupContacts(inputQuery: String, includeSelf: Boolean): Cursor? {
3400 val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
3401
3402 val searchSelection = ContactSearchSelection.Builder()
3403 .withRegistered(true)
3404 .withNonRegistered(true)
3405 .withGroups(false)
3406 .excludeId(if (includeSelf) null else Recipient.self().id)
3407 .withSearchQuery(query)
3408 .build()
3409 val selection = searchSelection.where
3410 val args = searchSelection.args
3411 val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + E164
3412
3413 return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
3414 }
3415
3416 fun getGroupMemberContacts(): Cursor? {
3417 val searchSelection = ContactSearchSelection.Builder()
3418 .withGroupMembers(true)
3419 .excludeId(Recipient.self().id)
3420 .build()
3421
3422 val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + E164
3423 return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, searchSelection.where, searchSelection.args, null, null, orderBy)
3424 }
3425
3426 fun queryGroupMemberContacts(inputQuery: String): Cursor? {
3427 val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
3428 val searchSelection = ContactSearchSelection.Builder()
3429 .withGroupMembers(true)
3430 .excludeId(Recipient.self().id)
3431 .withSearchQuery(query)
3432 .build()
3433
3434 val selection = searchSelection.where
3435 val args = searchSelection.args
3436 val orderBy = orderByPreferringAlphaOverNumeric(SORT_NAME) + ", " + E164
3437
3438 return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy)
3439 }
3440
3441 fun queryAllContacts(inputQuery: String): Cursor? {
3442 val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
3443 val selection =
3444 """
3445 $BLOCKED = ? AND
3446 (
3447 $SORT_NAME GLOB ? OR
3448 $USERNAME GLOB ? OR
3449 ${ContactSearchSelection.E164_SEARCH} OR
3450 $EMAIL GLOB ?
3451 )
3452 """
3453 val args = SqlUtil.buildArgs(0, query, query, query, query)
3454 return readableDatabase.query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null)
3455 }
3456
3457 /**
3458 * Gets the query used for performing the all contacts search so that it can be injected as a subquery.
3459 */
3460 fun getAllContactsSubquery(inputQuery: String): SqlUtil.Query {
3461 val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
3462
3463 //language=sql
3464 val subquery = """SELECT $ID FROM (
3465 SELECT ${SEARCH_PROJECTION.joinToString(",")} FROM $TABLE_NAME
3466 WHERE $BLOCKED = ? AND $HIDDEN = ? AND
3467 (
3468 $SORT_NAME GLOB ? OR
3469 $USERNAME GLOB ? OR
3470 ${ContactSearchSelection.E164_SEARCH} OR
3471 $EMAIL GLOB ?
3472 ))
3473 """
3474
3475 return SqlUtil.Query(subquery, SqlUtil.buildArgs(0, 0, query, query, query, query))
3476 }
3477
3478 /**
3479 * Queries all contacts without an active thread.
3480 */
3481 fun getAllContactsWithoutThreads(inputQuery: String): Cursor {
3482 val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
3483
3484 //language=sql
3485 val subquery = """
3486 SELECT ${SEARCH_PROJECTION.joinToString(", ")} FROM $TABLE_NAME
3487 WHERE $BLOCKED = ? AND $HIDDEN = ? AND NOT EXISTS (SELECT 1 FROM ${ThreadTable.TABLE_NAME} WHERE ${ThreadTable.TABLE_NAME}.${ThreadTable.ACTIVE} = 1 AND ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} = $TABLE_NAME.$ID LIMIT 1)
3488 AND (
3489 $SORT_NAME GLOB ? OR
3490 $USERNAME GLOB ? OR
3491 ${ContactSearchSelection.E164_SEARCH} OR
3492 $EMAIL GLOB ?
3493 )
3494 """
3495
3496 return readableDatabase.query(subquery, SqlUtil.buildArgs(0, 0, query, query, query, query))
3497 }
3498
3499 @JvmOverloads
3500 fun queryRecipientsForMentions(inputQuery: String, recipientIds: List<RecipientId>? = null): List<Recipient> {
3501 val query = SqlUtil.buildCaseInsensitiveGlobPattern(inputQuery)
3502 var ids: String? = null
3503
3504 if (Util.hasItems(recipientIds)) {
3505 ids = TextUtils.join(",", recipientIds?.map { it.serialize() }?.toList() ?: emptyList<String>())
3506 }
3507
3508 val selection = "$BLOCKED = 0 AND ${if (ids != null) "$ID IN ($ids) AND " else ""}$SORT_NAME GLOB ?"
3509 val recipients: MutableList<Recipient> = ArrayList()
3510
3511 RecipientReader(readableDatabase.query(TABLE_NAME, MENTION_SEARCH_PROJECTION, selection, SqlUtil.buildArgs(query), null, null, SORT_NAME)).use { reader ->
3512 var recipient: Recipient? = reader.getNext()
3513 while (recipient != null) {
3514 if (!recipient.isSelf) {
3515 recipients.add(recipient)
3516 }
3517 recipient = reader.getNext()
3518 }
3519 }
3520
3521 return recipients
3522 }
3523
3524 fun getRecipientsForMultiDeviceSync(): List<Recipient> {
3525 val subquery = "SELECT ${ThreadTable.TABLE_NAME}.${ThreadTable.RECIPIENT_ID} FROM ${ThreadTable.TABLE_NAME}"
3526 val selection = "$REGISTERED = ? AND $GROUP_ID IS NULL AND $ID != ? AND ($ACI_COLUMN NOT NULL OR $E164 NOT NULL) AND ($SYSTEM_CONTACT_URI NOT NULL OR $ID IN ($subquery))"
3527 val args = arrayOf(RegisteredState.REGISTERED.id.toString(), Recipient.self().id.serialize())
3528 val recipients: MutableList<Recipient> = ArrayList()
3529
3530 readableDatabase.query(TABLE_NAME, ID_PROJECTION, selection, args, null, null, null).use { cursor ->
3531 while (cursor != null && cursor.moveToNext()) {
3532 recipients.add(Recipient.resolved(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))))
3533 }
3534 }
3535 return recipients
3536 }
3537
3538 /**
3539 * @param lastInteractionThreshold Only include contacts that have been interacted with since this time.
3540 * @param lastProfileFetchThreshold Only include contacts that haven't their profile fetched after this time.
3541 * @param limit Only return at most this many contact.
3542 */
3543 fun getRecipientsForRoutineProfileFetch(lastInteractionThreshold: Long, lastProfileFetchThreshold: Long, limit: Int): List<RecipientId> {
3544 val threadDatabase = threads
3545 val recipientsWithinInteractionThreshold: MutableSet<RecipientId> = LinkedHashSet()
3546
3547 threadDatabase.readerFor(threadDatabase.getRecentPushConversationList(-1, false)).use { reader ->
3548 var record: ThreadRecord? = reader.getNext()
3549
3550 while (record != null && record.date > lastInteractionThreshold) {
3551 val recipient = Recipient.resolved(record.recipient.id)
3552 if (recipient.isGroup) {
3553 recipientsWithinInteractionThreshold.addAll(recipient.participantIds)
3554 } else {
3555 recipientsWithinInteractionThreshold.add(recipient.id)
3556 }
3557 record = reader.getNext()
3558 }
3559 }
3560
3561 return Recipient.resolvedList(recipientsWithinInteractionThreshold)
3562 .asSequence()
3563 .filterNot { it.isSelf }
3564 .filter { it.lastProfileFetchTime < lastProfileFetchThreshold }
3565 .take(limit)
3566 .map { it.id }
3567 .toMutableList()
3568 }
3569
3570 fun markProfilesFetched(ids: Collection<RecipientId>, time: Long) {
3571 writableDatabase.withinTransaction { db ->
3572 val values = contentValuesOf(LAST_PROFILE_FETCH to time)
3573
3574 SqlUtil.buildCollectionQuery(ID, ids).forEach { query ->
3575 db.update(TABLE_NAME, values, query.where, query.whereArgs)
3576 }
3577 }
3578 }
3579
3580 fun applyBlockedUpdate(blocked: List<SignalServiceAddress>, groupIds: List<ByteArray?>) {
3581 val blockedE164 = blocked
3582 .filter { b: SignalServiceAddress -> b.number.isPresent }
3583 .map { b: SignalServiceAddress -> b.number.get() }
3584 .toList()
3585
3586 val blockedUuid = blocked
3587 .map { b: SignalServiceAddress -> b.serviceId.toString().lowercase() }
3588 .toList()
3589
3590 val db = writableDatabase
3591 db.beginTransaction()
3592 try {
3593 val resetBlocked = ContentValues().apply {
3594 put(BLOCKED, 0)
3595 }
3596 db.update(TABLE_NAME, resetBlocked, null, null)
3597
3598 val setBlocked = ContentValues().apply {
3599 put(BLOCKED, 1)
3600 put(PROFILE_SHARING, 0)
3601 }
3602
3603 for (e164 in blockedE164) {
3604 db.update(TABLE_NAME, setBlocked, "$E164 = ?", arrayOf(e164))
3605 }
3606
3607 for (uuid in blockedUuid) {
3608 db.update(TABLE_NAME, setBlocked, "$ACI_COLUMN = ?", arrayOf(uuid))
3609 }
3610
3611 val groupIdStrings: MutableList<V1> = ArrayList(groupIds.size)
3612 for (raw in groupIds) {
3613 try {
3614 groupIdStrings.add(GroupId.v1(raw))
3615 } catch (e: BadGroupIdException) {
3616 Log.w(TAG, "[applyBlockedUpdate] Bad GV1 ID!")
3617 }
3618 }
3619
3620 for (groupId in groupIdStrings) {
3621 db.update(TABLE_NAME, setBlocked, "$GROUP_ID = ?", arrayOf(groupId.toString()))
3622 }
3623
3624 db.setTransactionSuccessful()
3625 } finally {
3626 db.endTransaction()
3627 }
3628
3629 ApplicationDependencies.getRecipientCache().clear()
3630 }
3631
3632 fun updateStorageId(recipientId: RecipientId, id: ByteArray?) {
3633 updateStorageIds(Collections.singletonMap(recipientId, id))
3634 }
3635
3636 private fun updateStorageIds(ids: Map<RecipientId, ByteArray?>) {
3637 val db = writableDatabase
3638 db.beginTransaction()
3639 try {
3640 for ((key, value) in ids) {
3641 val values = ContentValues().apply {
3642 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(value!!))
3643 }
3644 db.update(TABLE_NAME, values, ID_WHERE, arrayOf(key.serialize()))
3645 }
3646 db.setTransactionSuccessful()
3647 } finally {
3648 db.endTransaction()
3649 }
3650
3651 for (id in ids.keys) {
3652 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
3653 }
3654 }
3655
3656 fun markPreMessageRequestRecipientsAsProfileSharingEnabled(messageRequestEnableTime: Long) {
3657 val whereArgs = SqlUtil.buildArgs(messageRequestEnableTime)
3658 val select =
3659 """
3660 SELECT r.$ID FROM $TABLE_NAME AS r
3661 INNER JOIN ${ThreadTable.TABLE_NAME} AS t ON t.${ThreadTable.RECIPIENT_ID} = r.$ID
3662 WHERE
3663 r.$PROFILE_SHARING = 0 AND (
3664 EXISTS(SELECT 1 FROM ${MessageTable.TABLE_NAME} WHERE ${MessageTable.THREAD_ID} = t.${ThreadTable.ID} AND ${MessageTable.DATE_RECEIVED} < ?)
3665 )
3666 """
3667
3668 val idsToUpdate: MutableList<Long> = ArrayList()
3669 readableDatabase.rawQuery(select, whereArgs).use { cursor ->
3670 while (cursor.moveToNext()) {
3671 idsToUpdate.add(cursor.requireLong(ID))
3672 }
3673 }
3674
3675 if (Util.hasItems(idsToUpdate)) {
3676 val query = SqlUtil.buildSingleCollectionQuery(ID, idsToUpdate)
3677
3678 val values = contentValuesOf(
3679 PROFILE_SHARING to 1,
3680 HIDDEN to 0
3681 )
3682
3683 writableDatabase.update(TABLE_NAME, values, query.where, query.whereArgs)
3684
3685 for (id in idsToUpdate) {
3686 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(RecipientId.from(id))
3687 }
3688 }
3689 }
3690
3691 /**
3692 * 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.
3693 */
3694 fun markNeedsPniSignature(recipientId: RecipientId) {
3695 if (update(recipientId, contentValuesOf(NEEDS_PNI_SIGNATURE to 1))) {
3696 Log.i(TAG, "Marked $recipientId as needing a PNI signature message.")
3697 Recipient.live(recipientId).refresh()
3698 }
3699 }
3700
3701 /**
3702 * 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.
3703 */
3704 fun clearNeedsPniSignature(recipientId: RecipientId) {
3705 if (update(recipientId, contentValuesOf(NEEDS_PNI_SIGNATURE to 0))) {
3706 Recipient.live(recipientId).refresh()
3707 }
3708 }
3709
3710 fun setHasGroupsInCommon(recipientIds: List<RecipientId?>) {
3711 if (recipientIds.isEmpty()) {
3712 return
3713 }
3714
3715 var query = SqlUtil.buildSingleCollectionQuery(ID, recipientIds)
3716 val db = writableDatabase
3717
3718 db.query(TABLE_NAME, arrayOf(ID), "${query.where} AND $GROUPS_IN_COMMON = 0", query.whereArgs, null, null, null).use { cursor ->
3719 val idsToUpdate: MutableList<Long> = ArrayList(cursor.count)
3720
3721 while (cursor.moveToNext()) {
3722 idsToUpdate.add(cursor.requireLong(ID))
3723 }
3724
3725 if (Util.hasItems(idsToUpdate)) {
3726 query = SqlUtil.buildSingleCollectionQuery(ID, idsToUpdate)
3727 val values = ContentValues().apply {
3728 put(GROUPS_IN_COMMON, 1)
3729 }
3730
3731 val count = db.update(TABLE_NAME, values, query.where, query.whereArgs)
3732 if (count > 0) {
3733 for (id in idsToUpdate) {
3734 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(RecipientId.from(id))
3735 }
3736 }
3737 }
3738 }
3739 }
3740
3741 fun manuallyShowAvatar(recipientId: RecipientId) {
3742 updateExtras(recipientId) { b: RecipientExtras.Builder -> b.manuallyShownAvatar(true) }
3743 }
3744
3745 fun getCapabilities(id: RecipientId): RecipientRecord.Capabilities? {
3746 readableDatabase
3747 .select(CAPABILITIES)
3748 .from(TABLE_NAME)
3749 .where("$ID = ?", id)
3750 .run()
3751 .use { cursor ->
3752 return if (cursor.moveToFirst()) {
3753 RecipientTableCursorUtil.readCapabilities(cursor)
3754 } else {
3755 null
3756 }
3757 }
3758 }
3759
3760 fun updatePhoneNumberDiscoverability(presentInCds: Set<RecipientId>, missingFromCds: Set<RecipientId>) {
3761 SqlUtil.buildCollectionQuery(ID, presentInCds).forEach { query ->
3762 writableDatabase
3763 .update(TABLE_NAME)
3764 .values(PHONE_NUMBER_DISCOVERABLE to PhoneNumberDiscoverableState.DISCOVERABLE.id)
3765 .where(query.where, query.whereArgs)
3766 .run()
3767 }
3768
3769 SqlUtil.buildCollectionQuery(ID, missingFromCds).forEach { query ->
3770 writableDatabase
3771 .update(TABLE_NAME)
3772 .values(PHONE_NUMBER_DISCOVERABLE to PhoneNumberDiscoverableState.NOT_DISCOVERABLE.id)
3773 .where(query.where, query.whereArgs)
3774 .run()
3775 }
3776 }
3777
3778 private fun updateExtras(recipientId: RecipientId, updater: java.util.function.Function<RecipientExtras.Builder, RecipientExtras.Builder>) {
3779 val db = writableDatabase
3780 db.beginTransaction()
3781 try {
3782 db.query(TABLE_NAME, arrayOf(ID, EXTRAS), ID_WHERE, SqlUtil.buildArgs(recipientId), null, null, null).use { cursor ->
3783 if (cursor.moveToNext()) {
3784 val state = getRecipientExtras(cursor)
3785 val builder = state?.newBuilder() ?: RecipientExtras.Builder()
3786 val updatedState = updater.apply(builder).build().encode()
3787 val values = ContentValues(1).apply {
3788 put(EXTRAS, updatedState)
3789 }
3790 db.update(TABLE_NAME, values, ID_WHERE, SqlUtil.buildArgs(cursor.requireLong(ID)))
3791 }
3792 }
3793 db.setTransactionSuccessful()
3794 } finally {
3795 db.endTransaction()
3796 }
3797 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(recipientId)
3798 }
3799
3800 /**
3801 * Does not trigger any recipient refreshes -- it is assumed the caller handles this.
3802 * Will *not* give storageIds to those that shouldn't get them (e.g. MMS groups, unregistered
3803 * users).
3804 */
3805 fun rotateStorageId(recipientId: RecipientId) {
3806 val selfId = Recipient.self().id
3807
3808 val values = ContentValues(1).apply {
3809 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
3810 }
3811
3812 val query = "$ID = ? AND ($TYPE IN (?, ?, ?) OR $REGISTERED = ? OR $ID = ?)"
3813 val args = SqlUtil.buildArgs(recipientId, RecipientType.GV1.id, RecipientType.GV2.id, RecipientType.DISTRIBUTION_LIST.id, RegisteredState.REGISTERED.id, selfId.toLong())
3814
3815 writableDatabase.update(TABLE_NAME, values, query, args).also { updateCount ->
3816 Log.d(TAG, "[rotateStorageId] updateCount: $updateCount")
3817 }
3818 }
3819
3820 /**
3821 * Does not trigger any recipient refreshes -- it is assumed the caller handles this.
3822 */
3823 fun setStorageIdIfNotSet(recipientId: RecipientId) {
3824 val values = ContentValues(1).apply {
3825 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey()))
3826 }
3827
3828 val query = "$ID = ? AND $STORAGE_SERVICE_ID IS NULL"
3829 val args = SqlUtil.buildArgs(recipientId)
3830 writableDatabase.update(TABLE_NAME, values, query, args)
3831 }
3832
3833 /**
3834 * Updates a group recipient with a new V2 group ID. Should only be done as a part of GV1->GV2
3835 * migration.
3836 */
3837 fun updateGroupId(v1Id: V1, v2Id: V2) {
3838 val values = ContentValues().apply {
3839 put(GROUP_ID, v2Id.toString())
3840 put(TYPE, RecipientType.GV2.id)
3841 }
3842
3843 val query = SqlUtil.buildTrueUpdateQuery("$GROUP_ID = ?", SqlUtil.buildArgs(v1Id), values)
3844 if (update(query, values)) {
3845 val id = getByGroupId(v2Id).get()
3846 rotateStorageId(id)
3847 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
3848 }
3849 }
3850
3851 fun getExpiresInSeconds(id: RecipientId): Long {
3852 return readableDatabase
3853 .select(MESSAGE_EXPIRATION_TIME)
3854 .from(TABLE_NAME)
3855 .where(ID_WHERE, id)
3856 .run()
3857 .readToSingleLong(0L)
3858 }
3859
3860 /**
3861 * Will update the database with the content values you specified. It will make an intelligent
3862 * query such that this will only return true if a row was *actually* updated.
3863 */
3864 private fun update(id: RecipientId, contentValues: ContentValues): Boolean {
3865 val updateQuery = SqlUtil.buildTrueUpdateQuery(ID_WHERE, SqlUtil.buildArgs(id), contentValues)
3866 return update(updateQuery, contentValues)
3867 }
3868
3869 /**
3870 * Will update the database with the {@param contentValues} you specified.
3871 *
3872 *
3873 * This will only return true if a row was *actually* updated with respect to the where clause of the {@param updateQuery}.
3874 */
3875 private fun update(updateQuery: SqlUtil.Query, contentValues: ContentValues): Boolean {
3876 return writableDatabase.update(TABLE_NAME, contentValues, updateQuery.where, updateQuery.whereArgs) > 0
3877 }
3878
3879 private fun getByColumn(column: String, value: String): Optional<RecipientId> {
3880 val query = "$column = ?"
3881 val args = arrayOf(value)
3882
3883 readableDatabase.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null).use { cursor ->
3884 return if (cursor != null && cursor.moveToFirst()) {
3885 Optional.of(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))))
3886 } else {
3887 Optional.empty()
3888 }
3889 }
3890 }
3891
3892 private fun getOrInsertByColumn(column: String, value: String, contentValues: ContentValues = contentValuesOf(column to value)): GetOrInsertResult {
3893 if (TextUtils.isEmpty(value)) {
3894 throw AssertionError("$column cannot be empty.")
3895 }
3896
3897 var existing = getByColumn(column, value)
3898
3899 if (existing.isPresent) {
3900 return GetOrInsertResult(existing.get(), false)
3901 } else {
3902 val id = writableDatabase.insert(TABLE_NAME, null, contentValues)
3903 if (id < 0) {
3904 existing = getByColumn(column, value)
3905 if (existing.isPresent) {
3906 return GetOrInsertResult(existing.get(), false)
3907 } else {
3908 throw AssertionError("Failed to insert recipient!")
3909 }
3910 } else {
3911 return GetOrInsertResult(RecipientId.from(id), true)
3912 }
3913 }
3914 }
3915
3916 /**
3917 * Merges one ACI recipient with an E164 recipient. It is assumed that the E164 recipient does
3918 * *not* have an ACI.
3919 */
3920 private fun merge(primaryId: RecipientId, secondaryId: RecipientId, newPni: PNI? = null, pniVerified: Boolean): MergeResult {
3921 ensureInTransaction()
3922 val db = writableDatabase
3923 val primaryRecord = getRecord(primaryId)
3924 val secondaryRecord = getRecord(secondaryId)
3925
3926 // Clean up any E164-based identities (legacy stuff)
3927 if (secondaryRecord.e164 != null) {
3928 ApplicationDependencies.getProtocolStore().aci().identities().delete(secondaryRecord.e164)
3929 }
3930
3931 // Threads
3932 val threadMerge: ThreadTable.MergeResult = threads.merge(primaryId, secondaryId)
3933 threads.setLastScrolled(threadMerge.threadId, 0)
3934 threads.update(threadMerge.threadId, false, false)
3935
3936 // Recipient remaps
3937 for (table in recipientIdDatabaseTables) {
3938 table.remapRecipient(secondaryId, primaryId)
3939 }
3940
3941 // Thread Merge Event (remaps happen inside ThreadTable#merge)
3942 if (threadMerge.neededMerge) {
3943 val mergeEvent: ThreadMergeEvent.Builder = ThreadMergeEvent.Builder()
3944
3945 if (secondaryRecord.e164 != null) {
3946 mergeEvent.previousE164 = secondaryRecord.e164
3947 }
3948
3949 SignalDatabase.messages.insertThreadMergeEvent(primaryRecord.id, threadMerge.threadId, mergeEvent.build())
3950 }
3951
3952 // Recipient
3953 Log.w(TAG, "Deleting recipient $secondaryId", true)
3954 db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(secondaryId))
3955 RemappedRecords.getInstance().addRecipient(secondaryId, primaryId)
3956
3957 val uuidValues = contentValuesOf(
3958 E164 to (secondaryRecord.e164 ?: primaryRecord.e164),
3959 ACI_COLUMN to (primaryRecord.aci ?: secondaryRecord.aci)?.toString(),
3960 PNI_COLUMN to (newPni ?: secondaryRecord.pni ?: primaryRecord.pni)?.toString(),
3961 BLOCKED to (secondaryRecord.isBlocked || primaryRecord.isBlocked),
3962 MESSAGE_RINGTONE to Optional.ofNullable(primaryRecord.messageRingtone).or(Optional.ofNullable(secondaryRecord.messageRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null),
3963 MESSAGE_VIBRATE to if (primaryRecord.messageVibrateState != VibrateState.DEFAULT) primaryRecord.messageVibrateState.id else secondaryRecord.messageVibrateState.id,
3964 CALL_RINGTONE to Optional.ofNullable(primaryRecord.callRingtone).or(Optional.ofNullable(secondaryRecord.callRingtone)).map { obj: Uri? -> obj.toString() }.orElse(null),
3965 CALL_VIBRATE to if (primaryRecord.callVibrateState != VibrateState.DEFAULT) primaryRecord.callVibrateState.id else secondaryRecord.callVibrateState.id,
3966 NOTIFICATION_CHANNEL to (primaryRecord.notificationChannel ?: secondaryRecord.notificationChannel),
3967 MUTE_UNTIL to if (primaryRecord.muteUntil > 0) primaryRecord.muteUntil else secondaryRecord.muteUntil,
3968 CHAT_COLORS to Optional.ofNullable(primaryRecord.chatColors).or(Optional.ofNullable(secondaryRecord.chatColors)).map { colors: ChatColors? -> colors!!.serialize().encode() }.orElse(null),
3969 AVATAR_COLOR to primaryRecord.avatarColor.serialize(),
3970 CUSTOM_CHAT_COLORS_ID to Optional.ofNullable(primaryRecord.chatColors).or(Optional.ofNullable(secondaryRecord.chatColors)).map { colors: ChatColors? -> colors!!.id.longValue }.orElse(null),
3971 MESSAGE_EXPIRATION_TIME to if (primaryRecord.expireMessages > 0) primaryRecord.expireMessages else secondaryRecord.expireMessages,
3972 REGISTERED to RegisteredState.REGISTERED.id,
3973 SYSTEM_GIVEN_NAME to secondaryRecord.systemProfileName.givenName,
3974 SYSTEM_FAMILY_NAME to secondaryRecord.systemProfileName.familyName,
3975 SYSTEM_JOINED_NAME to secondaryRecord.systemProfileName.toString(),
3976 SYSTEM_PHOTO_URI to secondaryRecord.systemContactPhotoUri,
3977 SYSTEM_PHONE_LABEL to secondaryRecord.systemPhoneLabel,
3978 SYSTEM_CONTACT_URI to secondaryRecord.systemContactUri,
3979 PROFILE_SHARING to (primaryRecord.profileSharing || secondaryRecord.profileSharing),
3980 CAPABILITIES to max(primaryRecord.capabilities.rawBits, secondaryRecord.capabilities.rawBits),
3981 MENTION_SETTING to if (primaryRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) primaryRecord.mentionSetting.id else secondaryRecord.mentionSetting.id,
3982 PNI_SIGNATURE_VERIFIED to pniVerified.toInt()
3983 )
3984
3985 if (primaryRecord.profileSharing || secondaryRecord.profileSharing) {
3986 uuidValues.put(HIDDEN, 0)
3987 }
3988
3989 if (primaryRecord.profileKey != null) {
3990 updateProfileValuesForMerge(uuidValues, primaryRecord)
3991 } else if (secondaryRecord.profileKey != null) {
3992 updateProfileValuesForMerge(uuidValues, secondaryRecord)
3993 }
3994
3995 db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(primaryId))
3996
3997 return MergeResult(
3998 finalId = primaryId,
3999 neededThreadMerge = threadMerge.neededMerge
4000 )
4001 }
4002
4003 private fun ensureInTransaction() {
4004 check(writableDatabase.inTransaction()) { "Must be in a transaction!" }
4005 }
4006
4007 private fun buildContentValuesForNewUser(e164: String?, pni: PNI?, aci: ACI?, pniVerified: Boolean): ContentValues {
4008 check(e164 != null || pni != null || aci != null) { "Must provide some sort of identifier!" }
4009
4010 val values = contentValuesOf(
4011 E164 to e164,
4012 ACI_COLUMN to aci?.toString(),
4013 PNI_COLUMN to pni?.toString(),
4014 PNI_SIGNATURE_VERIFIED to pniVerified.toInt(),
4015 STORAGE_SERVICE_ID to Base64.encodeWithPadding(StorageSyncHelper.generateKey()),
4016 AVATAR_COLOR to AvatarColorHash.forAddress((aci ?: pni)?.toString(), e164).serialize()
4017 )
4018
4019 if (pni != null || aci != null) {
4020 values.put(REGISTERED, RegisteredState.REGISTERED.id)
4021 values.put(UNREGISTERED_TIMESTAMP, 0)
4022 }
4023
4024 return values
4025 }
4026
4027 private fun getValuesForStorageContact(contact: SignalContactRecord, isInsert: Boolean): ContentValues {
4028 return ContentValues().apply {
4029 val profileName = ProfileName.fromParts(contact.profileGivenName.orElse(null), contact.profileFamilyName.orElse(null))
4030 val systemName = ProfileName.fromParts(contact.systemGivenName.orElse(null), contact.systemFamilyName.orElse(null))
4031 val username = contact.username.orElse(null)
4032 val nickname = ProfileName.fromParts(contact.nicknameGivenName.orNull(), contact.nicknameFamilyName.orNull())
4033
4034 put(ACI_COLUMN, contact.aci.orElse(null)?.toString())
4035 put(PNI_COLUMN, contact.pni.orElse(null)?.toString())
4036 put(E164, contact.number.orElse(null))
4037 put(PROFILE_GIVEN_NAME, profileName.givenName)
4038 put(PROFILE_FAMILY_NAME, profileName.familyName)
4039 put(PROFILE_JOINED_NAME, profileName.toString())
4040 put(SYSTEM_GIVEN_NAME, systemName.givenName)
4041 put(SYSTEM_FAMILY_NAME, systemName.familyName)
4042 put(SYSTEM_JOINED_NAME, systemName.toString())
4043 put(SYSTEM_NICKNAME, contact.systemNickname.orElse(null))
4044 put(PROFILE_KEY, contact.profileKey.map { source -> Base64.encodeWithPadding(source) }.orElse(null))
4045 put(USERNAME, if (TextUtils.isEmpty(username)) null else username)
4046 put(PROFILE_SHARING, if (contact.isProfileSharingEnabled) "1" else "0")
4047 put(BLOCKED, if (contact.isBlocked) "1" else "0")
4048 put(MUTE_UNTIL, contact.muteUntil)
4049 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(contact.id.raw))
4050 put(HIDDEN, contact.isHidden)
4051 put(PNI_SIGNATURE_VERIFIED, contact.isPniSignatureVerified.toInt())
4052 put(NICKNAME_GIVEN_NAME, nickname.givenName.nullIfBlank())
4053 put(NICKNAME_FAMILY_NAME, nickname.familyName.nullIfBlank())
4054 put(NICKNAME_JOINED_NAME, nickname.toString().nullIfBlank())
4055 put(NOTE, contact.note.orNull().nullIfBlank())
4056
4057 if (contact.hasUnknownFields()) {
4058 put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(Objects.requireNonNull(contact.serializeUnknownFields())))
4059 } else {
4060 putNull(STORAGE_SERVICE_PROTO)
4061 }
4062
4063 put(UNREGISTERED_TIMESTAMP, contact.unregisteredTimestamp)
4064 if (contact.unregisteredTimestamp > 0L) {
4065 put(REGISTERED, RegisteredState.NOT_REGISTERED.id)
4066 } else if (contact.aci.isPresent) {
4067 put(REGISTERED, RegisteredState.REGISTERED.id)
4068 } else {
4069 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()})")
4070 }
4071
4072 if (isInsert) {
4073 put(AVATAR_COLOR, AvatarColorHash.forAddress(contact.aci.map { it.toString() }.or(contact.pni.map { it.toString() }).orNull(), contact.number.orNull()).serialize())
4074 }
4075 }
4076 }
4077
4078 private fun getValuesForStorageGroupV1(groupV1: SignalGroupV1Record, isInsert: Boolean): ContentValues {
4079 return ContentValues().apply {
4080 val groupId = GroupId.v1orThrow(groupV1.groupId)
4081
4082 put(GROUP_ID, groupId.toString())
4083 put(TYPE, RecipientType.GV1.id)
4084 put(PROFILE_SHARING, if (groupV1.isProfileSharingEnabled) "1" else "0")
4085 put(BLOCKED, if (groupV1.isBlocked) "1" else "0")
4086 put(MUTE_UNTIL, groupV1.muteUntil)
4087 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(groupV1.id.raw))
4088
4089 if (groupV1.hasUnknownFields()) {
4090 put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(groupV1.serializeUnknownFields()))
4091 } else {
4092 putNull(STORAGE_SERVICE_PROTO)
4093 }
4094
4095 if (isInsert) {
4096 put(AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize())
4097 }
4098 }
4099 }
4100
4101 private fun getValuesForStorageGroupV2(groupV2: SignalGroupV2Record, isInsert: Boolean): ContentValues {
4102 return ContentValues().apply {
4103 val groupId = GroupId.v2(groupV2.masterKeyOrThrow)
4104
4105 put(GROUP_ID, groupId.toString())
4106 put(TYPE, RecipientType.GV2.id)
4107 put(PROFILE_SHARING, if (groupV2.isProfileSharingEnabled) "1" else "0")
4108 put(BLOCKED, if (groupV2.isBlocked) "1" else "0")
4109 put(MUTE_UNTIL, groupV2.muteUntil)
4110 put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(groupV2.id.raw))
4111 put(MENTION_SETTING, if (groupV2.notifyForMentionsWhenMuted()) MentionSetting.ALWAYS_NOTIFY.id else MentionSetting.DO_NOT_NOTIFY.id)
4112
4113 if (groupV2.hasUnknownFields()) {
4114 put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(groupV2.serializeUnknownFields()))
4115 } else {
4116 putNull(STORAGE_SERVICE_PROTO)
4117 }
4118
4119 if (isInsert) {
4120 put(AVATAR_COLOR, AvatarColorHash.forGroupId(groupId).serialize())
4121 }
4122 }
4123 }
4124
4125 /**
4126 * Should be called immediately after we create a recipient for self.
4127 * This clears up any placeholders we put in the database for the local user, which is typically only done in database migrations.
4128 */
4129 fun updatePendingSelfData(selfId: RecipientId) {
4130 SignalDatabase.messages.updatePendingSelfData(RecipientId.from(PLACEHOLDER_SELF_ID), selfId)
4131
4132 val deletes = writableDatabase
4133 .delete(TABLE_NAME)
4134 .where("$ID = ?", PLACEHOLDER_SELF_ID)
4135 .run()
4136
4137 if (deletes > 0) {
4138 Log.w(TAG, "Deleted a PLACEHOLDER_SELF from the table.")
4139 } else {
4140 Log.i(TAG, "No PLACEHOLDER_SELF in the table.")
4141 }
4142 }
4143
4144 /**
4145 * 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
4146 * get them back through CDS).
4147 */
4148 fun debugClearServiceIds(recipientId: RecipientId? = null) {
4149 check(FeatureFlags.internalUser())
4150
4151 writableDatabase
4152 .update(TABLE_NAME)
4153 .values(
4154 ACI_COLUMN to null,
4155 PNI_COLUMN to null
4156 )
4157 .run {
4158 if (recipientId == null) {
4159 where("$ID != ? AND $E164 NOT NULL", Recipient.self().id)
4160 } else {
4161 where("$ID = ? AND $E164 NOT NULL", recipientId)
4162 }
4163 }
4164 .run()
4165
4166 ApplicationDependencies.getRecipientCache().clear()
4167 RecipientId.clearCache()
4168 }
4169
4170 /**
4171 * Should only be used for debugging! A very destructive action that clears all known profile keys and credentials.
4172 */
4173 fun debugClearProfileData(recipientId: RecipientId? = null) {
4174 check(FeatureFlags.internalUser())
4175
4176 writableDatabase
4177 .update(TABLE_NAME)
4178 .values(
4179 PROFILE_KEY to null,
4180 EXPIRING_PROFILE_KEY_CREDENTIAL to null,
4181 PROFILE_GIVEN_NAME to null,
4182 PROFILE_FAMILY_NAME to null,
4183 PROFILE_JOINED_NAME to null,
4184 LAST_PROFILE_FETCH to 0,
4185 PROFILE_AVATAR to null,
4186 PROFILE_SHARING to 0
4187 )
4188 .run {
4189 if (recipientId == null) {
4190 where("$ID != ?", Recipient.self().id)
4191 } else {
4192 where("$ID = ?", recipientId)
4193 }
4194 }
4195 .run()
4196
4197 ApplicationDependencies.getRecipientCache().clear()
4198 RecipientId.clearCache()
4199 }
4200
4201 /**
4202 * Should only be used for debugging! Clears the E164 and PNI from a recipient.
4203 */
4204 fun debugClearE164AndPni(recipientId: RecipientId) {
4205 check(FeatureFlags.internalUser())
4206
4207 writableDatabase
4208 .update(TABLE_NAME)
4209 .values(
4210 E164 to null,
4211 PNI_COLUMN to null
4212 )
4213 .where(ID_WHERE, recipientId)
4214 .run()
4215
4216 ApplicationDependencies.getRecipientCache().clear()
4217 RecipientId.clearCache()
4218 }
4219
4220 /**
4221 * Should only be used for debugging! Clears the ACI from a contact.
4222 * Only works if the recipient has a PNI.
4223 */
4224 fun debugRemoveAci(recipientId: RecipientId) {
4225 check(FeatureFlags.internalUser())
4226
4227 writableDatabase.execSQL(
4228 """
4229 UPDATE $TABLE_NAME
4230 SET $ACI_COLUMN = $PNI_COLUMN
4231 WHERE $ID = ? AND $PNI_COLUMN NOT NULL
4232 """,
4233 SqlUtil.buildArgs(recipientId)
4234 )
4235
4236 ApplicationDependencies.getRecipientCache().clear()
4237 RecipientId.clearCache()
4238 }
4239
4240 private fun updateProfileValuesForMerge(values: ContentValues, record: RecipientRecord) {
4241 values.apply {
4242 put(PROFILE_KEY, if (record.profileKey != null) Base64.encodeWithPadding(record.profileKey) else null)
4243 putNull(EXPIRING_PROFILE_KEY_CREDENTIAL)
4244 put(PROFILE_AVATAR, record.signalProfileAvatar)
4245 put(PROFILE_GIVEN_NAME, record.signalProfileName.givenName)
4246 put(PROFILE_FAMILY_NAME, record.signalProfileName.familyName)
4247 put(PROFILE_JOINED_NAME, record.signalProfileName.toString())
4248 }
4249 }
4250
4251 /**
4252 * By default, SQLite will prefer numbers over letters when sorting. e.g. (b, a, 1) is sorted as (1, a, b).
4253 * This order by will using a GLOB pattern to instead sort it as (a, b, 1).
4254 *
4255 * @param column The name of the column to sort by
4256 */
4257 private fun orderByPreferringAlphaOverNumeric(column: String): String {
4258 return "CASE WHEN $column GLOB '[0-9]*' THEN 1 ELSE 0 END, $column"
4259 }
4260
4261 private fun <T> Optional<T>.isAbsent(): Boolean {
4262 return !this.isPresent
4263 }
4264
4265 private data class MergeResult(
4266 val finalId: RecipientId,
4267 val neededThreadMerge: Boolean
4268 )
4269
4270 inner class BulkOperationsHandle internal constructor(private val database: SQLiteDatabase) {
4271 private val pendingRecipients: MutableSet<RecipientId> = mutableSetOf()
4272
4273 fun setSystemContactInfo(
4274 id: RecipientId,
4275 systemProfileName: ProfileName,
4276 systemDisplayName: String?,
4277 photoUri: String?,
4278 systemPhoneLabel: String?,
4279 systemPhoneType: Int,
4280 systemContactUri: String?
4281 ) {
4282 val joinedName = Util.firstNonNull(systemDisplayName, systemProfileName.toString())
4283 val refreshQualifyingValues = ContentValues().apply {
4284 put(SYSTEM_GIVEN_NAME, systemProfileName.givenName)
4285 put(SYSTEM_FAMILY_NAME, systemProfileName.familyName)
4286 put(SYSTEM_JOINED_NAME, joinedName)
4287 put(SYSTEM_PHOTO_URI, photoUri)
4288 put(SYSTEM_PHONE_LABEL, systemPhoneLabel)
4289 put(SYSTEM_PHONE_TYPE, systemPhoneType)
4290 put(SYSTEM_CONTACT_URI, systemContactUri)
4291 }
4292
4293 val updateQuery = SqlUtil.buildTrueUpdateQuery("$ID = ? AND $PHONE_NUMBER_DISCOVERABLE != ?", SqlUtil.buildArgs(id, PhoneNumberDiscoverableState.NOT_DISCOVERABLE.id), refreshQualifyingValues)
4294 if (update(updateQuery, refreshQualifyingValues)) {
4295 pendingRecipients.add(id)
4296 }
4297
4298 writableDatabase
4299 .update(TABLE_NAME)
4300 .values(SYSTEM_INFO_PENDING to 0)
4301 .where("$ID = ? AND $PHONE_NUMBER_DISCOVERABLE != ?", id, PhoneNumberDiscoverableState.NOT_DISCOVERABLE.id)
4302 .run()
4303 }
4304
4305 fun finish() {
4306 markAllRelevantEntriesDirty()
4307 clearSystemDataForPendingInfo()
4308 database.setTransactionSuccessful()
4309 database.endTransaction()
4310 pendingRecipients.forEach { id -> ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) }
4311 }
4312
4313 private fun markAllRelevantEntriesDirty() {
4314 val query = "$SYSTEM_INFO_PENDING = ? AND $STORAGE_SERVICE_ID NOT NULL"
4315 val args = SqlUtil.buildArgs("1")
4316
4317 database.query(TABLE_NAME, ID_PROJECTION, query, args, null, null, null).use { cursor ->
4318 while (cursor.moveToNext()) {
4319 val id = RecipientId.from(cursor.requireNonNullString(ID))
4320 rotateStorageId(id)
4321 }
4322 }
4323
4324 pendingRecipients.forEach { id -> rotateStorageId(id) }
4325 }
4326
4327 private fun clearSystemDataForPendingInfo() {
4328 writableDatabase.rawQuery(
4329 """
4330 UPDATE $TABLE_NAME
4331 SET
4332 $SYSTEM_INFO_PENDING = 0,
4333 $SYSTEM_GIVEN_NAME = NULL,
4334 $SYSTEM_FAMILY_NAME = NULL,
4335 $SYSTEM_JOINED_NAME = NULL,
4336 $SYSTEM_PHOTO_URI = NULL,
4337 $SYSTEM_PHONE_LABEL = NULL,
4338 $SYSTEM_CONTACT_URI = NULL
4339 WHERE $SYSTEM_INFO_PENDING = 1
4340 RETURNING $ID
4341 """,
4342 null
4343 ).forEach { cursor ->
4344 val id = RecipientId.from(cursor.requireLong(ID))
4345 ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id)
4346 }
4347 }
4348 }
4349
4350 interface ColorUpdater {
4351 fun update(name: String, materialColor: MaterialColor?): ChatColors?
4352 }
4353
4354 class RecipientReader internal constructor(private val cursor: Cursor) : Closeable {
4355
4356 fun getCurrent(): Recipient {
4357 val id = RecipientId.from(cursor.requireLong(ID))
4358 return Recipient.resolved(id)
4359 }
4360
4361 fun getNext(): Recipient? {
4362 return if (cursor.moveToNext()) {
4363 getCurrent()
4364 } else {
4365 null
4366 }
4367 }
4368
4369 val count: Int
4370 get() = cursor.count
4371
4372 override fun close() {
4373 cursor.close()
4374 }
4375 }
4376
4377 class RecipientIterator(
4378 private val context: Context,
4379 private val cursor: Cursor
4380 ) : Iterator<RecipientRecord>, Closeable {
4381
4382 override fun hasNext(): Boolean {
4383 return cursor.count != 0 && !cursor.isLast
4384 }
4385
4386 override fun next(): RecipientRecord {
4387 if (!cursor.moveToNext()) {
4388 throw NoSuchElementException()
4389 }
4390
4391 return RecipientTableCursorUtil.getRecord(context, cursor)
4392 }
4393
4394 override fun close() {
4395 cursor.close()
4396 }
4397 }
4398
4399 class MissingRecipientException(id: RecipientId?) : IllegalStateException("Failed to find recipient with ID: $id")
4400
4401 private class GetOrInsertResult(val recipientId: RecipientId, val neededInsert: Boolean)
4402
4403 data class ContactSearchQuery(
4404 val query: String,
4405 val includeSelf: Boolean,
4406 val contactSearchSortOrder: ContactSearchSortOrder = ContactSearchSortOrder.NATURAL
4407 )
4408
4409 @VisibleForTesting
4410 internal class ContactSearchSelection private constructor(val where: String, val args: Array<String>) {
4411
4412 @VisibleForTesting
4413 internal class Builder {
4414 private var includeRegistered = false
4415 private var includeNonRegistered = false
4416 private var includeGroupMembers = false
4417 private var excludeId: RecipientId? = null
4418 private var excludeGroups = false
4419 private var searchQuery: String? = null
4420
4421 fun withRegistered(includeRegistered: Boolean): Builder {
4422 this.includeRegistered = includeRegistered
4423 return this
4424 }
4425
4426 fun withNonRegistered(includeNonRegistered: Boolean): Builder {
4427 this.includeNonRegistered = includeNonRegistered
4428 return this
4429 }
4430
4431 fun withGroupMembers(includeGroupMembers: Boolean): Builder {
4432 this.includeGroupMembers = includeGroupMembers
4433 return this
4434 }
4435
4436 fun excludeId(recipientId: RecipientId?): Builder {
4437 excludeId = recipientId
4438 return this
4439 }
4440
4441 fun withGroups(includeGroups: Boolean): Builder {
4442 excludeGroups = !includeGroups
4443 return this
4444 }
4445
4446 fun withSearchQuery(searchQuery: String): Builder {
4447 this.searchQuery = searchQuery
4448 return this
4449 }
4450
4451 fun build(): ContactSearchSelection {
4452 check(!(!includeRegistered && !includeNonRegistered && !includeGroupMembers)) { "Must include either registered, non-registered, or group member recipients in search" }
4453 val stringBuilder = StringBuilder("(")
4454 val args: MutableList<Any?> = LinkedList()
4455 var hasPreceedingSection = false
4456
4457 if (includeRegistered) {
4458 hasPreceedingSection = true
4459 stringBuilder.append("(")
4460 args.add(RegisteredState.REGISTERED.id)
4461 args.add(1)
4462 if (Util.isEmpty(searchQuery)) {
4463 stringBuilder.append(SIGNAL_CONTACT)
4464 } else {
4465 stringBuilder.append(QUERY_SIGNAL_CONTACT)
4466 args.add(searchQuery)
4467 args.add(searchQuery)
4468 args.add(searchQuery)
4469 }
4470 stringBuilder.append(")")
4471 }
4472
4473 if (hasPreceedingSection && includeNonRegistered) {
4474 stringBuilder.append(" OR ")
4475 }
4476
4477 if (includeNonRegistered) {
4478 hasPreceedingSection = true
4479 stringBuilder.append("(")
4480 args.add(RegisteredState.REGISTERED.id)
4481
4482 if (Util.isEmpty(searchQuery)) {
4483 stringBuilder.append(NON_SIGNAL_CONTACT)
4484 } else {
4485 stringBuilder.append(QUERY_NON_SIGNAL_CONTACT)
4486 args.add(searchQuery)
4487 args.add(searchQuery)
4488 args.add(searchQuery)
4489 }
4490
4491 stringBuilder.append(")")
4492 }
4493
4494 if (hasPreceedingSection && includeGroupMembers) {
4495 stringBuilder.append(" OR ")
4496 }
4497
4498 if (includeGroupMembers) {
4499 stringBuilder.append("(")
4500 args.add(RegisteredState.REGISTERED.id)
4501 args.add(1)
4502 if (Util.isEmpty(searchQuery)) {
4503 stringBuilder.append(GROUP_MEMBER_CONTACT)
4504 } else {
4505 stringBuilder.append(QUERY_GROUP_MEMBER_CONTACT)
4506 args.add(searchQuery)
4507 args.add(searchQuery)
4508 args.add(searchQuery)
4509 }
4510
4511 stringBuilder.append(")")
4512 }
4513
4514 stringBuilder.append(")")
4515 stringBuilder.append(FILTER_BLOCKED)
4516 args.add(0)
4517
4518 stringBuilder.append(FILTER_HIDDEN)
4519 args.add(0)
4520
4521 if (excludeGroups) {
4522 stringBuilder.append(FILTER_GROUPS)
4523 }
4524
4525 if (excludeId != null) {
4526 stringBuilder.append(FILTER_ID)
4527 args.add(excludeId!!.serialize())
4528 }
4529
4530 return ContactSearchSelection(stringBuilder.toString(), args.map { obj: Any? -> obj.toString() }.toTypedArray())
4531 }
4532 }
4533
4534 companion object {
4535 //language=sql
4536 private val HAS_GROUP_IN_COMMON = """
4537 EXISTS (
4538 SELECT 1
4539 FROM ${GroupTable.MembershipTable.TABLE_NAME}
4540 INNER JOIN ${GroupTable.TABLE_NAME} ON ${GroupTable.TABLE_NAME}.${GroupTable.GROUP_ID} = ${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.GROUP_ID}
4541 WHERE ${GroupTable.MembershipTable.TABLE_NAME}.${GroupTable.MembershipTable.RECIPIENT_ID} = $TABLE_NAME.$ID AND ${GroupTable.TABLE_NAME}.${GroupTable.ACTIVE} = 1 AND ${GroupTable.TABLE_NAME}.${GroupTable.MMS} = 0
4542 )
4543 """
4544 val E164_SEARCH = "(($PHONE_NUMBER_SHARING != ${PhoneNumberSharingState.DISABLED.id} OR $SYSTEM_CONTACT_URI NOT NULL) AND $E164 GLOB ?)"
4545 const val FILTER_GROUPS = " AND $GROUP_ID IS NULL"
4546 const val FILTER_ID = " AND $ID != ?"
4547 const val FILTER_BLOCKED = " AND $BLOCKED = ?"
4548 const val FILTER_HIDDEN = " AND $HIDDEN = ?"
4549 const val NON_SIGNAL_CONTACT = "$REGISTERED != ? AND $SYSTEM_CONTACT_URI NOT NULL AND ($E164 NOT NULL OR $EMAIL NOT NULL)"
4550 val QUERY_NON_SIGNAL_CONTACT = "$NON_SIGNAL_CONTACT AND ($E164_SEARCH OR $EMAIL GLOB ? OR $SYSTEM_JOINED_NAME GLOB ?)"
4551 const val SIGNAL_CONTACT = "$REGISTERED = ? AND (NULLIF($SYSTEM_JOINED_NAME, '') NOT NULL OR $PROFILE_SHARING = ?) AND ($SORT_NAME NOT NULL OR $USERNAME NOT NULL)"
4552 val QUERY_SIGNAL_CONTACT = "$SIGNAL_CONTACT AND ($E164_SEARCH OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)"
4553 val GROUP_MEMBER_CONTACT = "$REGISTERED = ? AND $HAS_GROUP_IN_COMMON AND NOT (NULLIF($SYSTEM_JOINED_NAME, '') NOT NULL OR $PROFILE_SHARING = ?) AND ($SORT_NAME NOT NULL OR $USERNAME NOT NULL)"
4554 val QUERY_GROUP_MEMBER_CONTACT = "$GROUP_MEMBER_CONTACT AND ($E164_SEARCH OR $SORT_NAME GLOB ? OR $USERNAME GLOB ?)"
4555 }
4556 }
4557
4558 /**
4559 * Values that represent the index in the capabilities bitmask. Each index can store a 2-bit
4560 * value, which in this case is the value of [Recipient.Capability].
4561 */
4562 internal object Capabilities {
4563 const val BIT_LENGTH = 2
4564
4565// const val GROUPS_V2 = 0
4566// const val GROUPS_V1_MIGRATION = 1
4567// const val SENDER_KEY = 2
4568// const val ANNOUNCEMENT_GROUPS = 3
4569// const val CHANGE_NUMBER = 4
4570// const val STORIES = 5
4571// const val GIFT_BADGES = 6
4572 const val PNP = 7
4573 const val PAYMENT_ACTIVATION = 8
4574 }
4575
4576 enum class VibrateState(val id: Int) {
4577 DEFAULT(0), ENABLED(1), DISABLED(2);
4578
4579 companion object {
4580 fun fromId(id: Int): VibrateState {
4581 return values()[id]
4582 }
4583
4584 fun fromBoolean(enabled: Boolean): VibrateState {
4585 return if (enabled) ENABLED else DISABLED
4586 }
4587 }
4588 }
4589
4590 enum class RegisteredState(val id: Int) {
4591 UNKNOWN(0), REGISTERED(1), NOT_REGISTERED(2);
4592
4593 companion object {
4594 fun fromId(id: Int): RegisteredState {
4595 return values()[id]
4596 }
4597 }
4598 }
4599
4600 enum class UnidentifiedAccessMode(val mode: Int) {
4601 UNKNOWN(0), DISABLED(1), ENABLED(2), UNRESTRICTED(3);
4602
4603 companion object {
4604 fun fromMode(mode: Int): UnidentifiedAccessMode {
4605 return values()[mode]
4606 }
4607 }
4608 }
4609
4610 enum class InsightsBannerTier(val id: Int) {
4611 NO_TIER(0), TIER_ONE(1), TIER_TWO(2);
4612
4613 fun seen(tier: InsightsBannerTier): Boolean {
4614 return tier.id <= id
4615 }
4616
4617 companion object {
4618 fun fromId(id: Int): InsightsBannerTier {
4619 return values()[id]
4620 }
4621 }
4622 }
4623
4624 enum class RecipientType(val id: Int) {
4625 INDIVIDUAL(0), MMS(1), GV1(2), GV2(3), DISTRIBUTION_LIST(4), CALL_LINK(5);
4626
4627 companion object {
4628 fun fromId(id: Int): RecipientType {
4629 return values()[id]
4630 }
4631 }
4632 }
4633
4634 enum class MentionSetting(val id: Int) {
4635 ALWAYS_NOTIFY(0), DO_NOT_NOTIFY(1);
4636
4637 companion object {
4638 fun fromId(id: Int): MentionSetting {
4639 return values()[id]
4640 }
4641 }
4642 }
4643
4644 enum class PhoneNumberSharingState(val id: Int) {
4645 UNKNOWN(0), ENABLED(1), DISABLED(2);
4646
4647 val enabled
4648 get() = this == ENABLED || this == UNKNOWN
4649
4650 companion object {
4651 fun fromId(id: Int): PhoneNumberSharingState {
4652 return values()[id]
4653 }
4654 }
4655 }
4656
4657 enum class PhoneNumberDiscoverableState(val id: Int) {
4658 UNKNOWN(0), DISCOVERABLE(1), NOT_DISCOVERABLE(2);
4659
4660 companion object {
4661 fun fromId(id: Int): PhoneNumberDiscoverableState {
4662 return PhoneNumberDiscoverableState.values()[id]
4663 }
4664 }
4665 }
4666
4667 data class CdsV2Result(
4668 val pni: PNI,
4669 val aci: ACI?
4670 )
4671
4672 data class ProcessPnpTupleResult(
4673 val finalId: RecipientId,
4674 val requiredInsert: Boolean,
4675 val affectedIds: Set<RecipientId>,
4676 val oldIds: Set<RecipientId>,
4677 val changedNumberId: RecipientId?,
4678 val operations: List<PnpOperation>,
4679 val breadCrumbs: List<String>
4680 )
4681
4682 class SseWithSelfAci(cause: Exception) : IllegalStateException(cause)
4683 class SseWithSelfAciNoSession(cause: Exception) : IllegalStateException(cause)
4684 class SseWithSelfPni(cause: Exception) : IllegalStateException(cause)
4685 class SseWithSelfPniNoSession(cause: Exception) : IllegalStateException(cause)
4686 class SseWithSelfE164(cause: Exception) : IllegalStateException(cause)
4687 class SseWithSelfE164NoSession(cause: Exception) : IllegalStateException(cause)
4688 class SseWithNoPniSessionsException(cause: Exception) : IllegalStateException(cause)
4689 class SseWithASinglePniSessionForSelfException(cause: Exception) : IllegalStateException(cause)
4690 class SseWithASinglePniSessionException(cause: Exception) : IllegalStateException(cause)
4691 class SseWithMultiplePniSessionsException(cause: Exception) : IllegalStateException(cause)
4692}