That fuck shit the fascists are using
at master 4692 lines 170 kB view raw
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}