381 lines
14 KiB
Swift
381 lines
14 KiB
Swift
//
|
|
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SignalServiceKit
|
|
|
|
@objc
|
|
public protocol ContactsManagerCache: AnyObject {
|
|
func unsortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount]
|
|
|
|
// Order respects the systems contact sorting preference.
|
|
func sortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount]
|
|
|
|
func setSortedSignalAccounts(_ signalAccounts: [SignalAccount])
|
|
|
|
func contact(forPhoneNumber phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact?
|
|
|
|
func allContacts(transaction: SDSAnyReadTransaction) -> [Contact]
|
|
|
|
func contactsMaps(transaction: SDSAnyReadTransaction) -> ContactsMaps
|
|
|
|
func setContactsMaps(_ contactsMaps: ContactsMaps,
|
|
localNumber: String?,
|
|
transaction: SDSAnyWriteTransaction)
|
|
|
|
func warmCaches(transaction: SDSAnyReadTransaction) -> ContactsManagerCacheSummary
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class ContactsManagerCacheSummary: NSObject {
|
|
@objc
|
|
public let contactCount: UInt
|
|
@objc
|
|
public let signalAccountCount: UInt
|
|
|
|
required init(contactCount: UInt, signalAccountCount: UInt) {
|
|
self.contactCount = contactCount
|
|
self.signalAccountCount = signalAccountCount
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class ContactsMaps: NSObject {
|
|
@objc
|
|
public let uniqueIdToContactMap: [String: Contact]
|
|
@objc
|
|
public let phoneNumberToContactMap: [String: Contact]
|
|
|
|
@objc
|
|
public var allContacts: [Contact] { Array(uniqueIdToContactMap.values) }
|
|
|
|
required init(uniqueIdToContactMap: [String: Contact],
|
|
phoneNumberToContactMap: [String: Contact]) {
|
|
self.uniqueIdToContactMap = uniqueIdToContactMap
|
|
self.phoneNumberToContactMap = phoneNumberToContactMap
|
|
}
|
|
|
|
static let empty: ContactsMaps = ContactsMaps(uniqueIdToContactMap: [:], phoneNumberToContactMap: [:])
|
|
|
|
// Builds a map of phone number-to-Contact.
|
|
// A given Contact may have multiple phone numbers.
|
|
@objc
|
|
public static func build(contacts: [Contact],
|
|
localNumber: String?) -> ContactsMaps {
|
|
|
|
var uniqueIdToContactMap = [String: Contact]()
|
|
var phoneNumberToContactMap = [String: Contact]()
|
|
for contact in contacts {
|
|
let phoneNumbers = Self.phoneNumbers(forContact: contact, localNumber: localNumber)
|
|
guard !phoneNumbers.isEmpty else {
|
|
continue
|
|
}
|
|
|
|
uniqueIdToContactMap[contact.uniqueId] = contact
|
|
|
|
for phoneNumber in phoneNumbers {
|
|
phoneNumberToContactMap[phoneNumber] = contact
|
|
}
|
|
}
|
|
return ContactsMaps(uniqueIdToContactMap: uniqueIdToContactMap,
|
|
phoneNumberToContactMap: phoneNumberToContactMap)
|
|
}
|
|
|
|
fileprivate static func phoneNumbers(forContact contact: Contact, localNumber: String?) -> [String] {
|
|
return contact.parsedPhoneNumbers.compactMap { phoneNumber in
|
|
guard let phoneNumberE164 = phoneNumber.toE164().nilIfEmpty else {
|
|
return nil
|
|
}
|
|
|
|
// Ignore any system contact records for the local contact.
|
|
// For the local user we never want to show the avatar /
|
|
// name that you have entered for yourself in your system
|
|
// contacts. Instead, we always want to display your profile
|
|
// name and avatar.
|
|
let isLocalContact = phoneNumberE164 == localNumber
|
|
guard !isLocalContact else {
|
|
return nil
|
|
}
|
|
|
|
return phoneNumberE164
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func isSystemContact(address: SignalServiceAddress) -> Bool {
|
|
guard let phoneNumber = address.phoneNumber?.nilIfEmpty else {
|
|
return false
|
|
}
|
|
return isSystemContact(phoneNumber: phoneNumber)
|
|
}
|
|
|
|
@objc
|
|
public func isSystemContact(phoneNumber: String) -> Bool {
|
|
phoneNumberToContactMap[phoneNumber] != nil
|
|
}
|
|
|
|
public func isEqualForCache(_ other: ContactsMaps) -> Bool {
|
|
let mapSelf = self.uniqueIdToContactMap
|
|
let mapOther = other.uniqueIdToContactMap
|
|
let keysSelf = Set(mapSelf.keys)
|
|
let keysOther = Set(mapOther.keys)
|
|
guard keysSelf == keysOther else {
|
|
return false
|
|
}
|
|
for key in keysSelf {
|
|
guard let valueSelf = mapSelf[key],
|
|
let valueOther = mapOther[key],
|
|
valueSelf.isEqualForCache(valueOther) else {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class ContactsManagerCacheInDatabase: NSObject, ContactsManagerCache {
|
|
@objc
|
|
public func unsortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] {
|
|
SignalAccount.anyFetchAll(transaction: transaction)
|
|
}
|
|
|
|
// Order respects the systems contact sorting preference.
|
|
@objc
|
|
public func sortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] {
|
|
contactsManagerImpl.sortSignalAccounts(unsortedSignalAccounts(transaction: transaction),
|
|
transaction: transaction)
|
|
}
|
|
|
|
@objc
|
|
public func setSortedSignalAccounts(_ signalAccounts: [SignalAccount]) {
|
|
// Ignore.
|
|
}
|
|
|
|
private static let uniqueIdStore = SDSKeyValueStore(collection: "ContactsManagerCache.uniqueIdStore")
|
|
private static let phoneNumberStore = SDSKeyValueStore(collection: "ContactsManagerCache.phoneNumberStore")
|
|
|
|
private static func serializeContact(_ contact: Contact, label: String) -> Data? {
|
|
let data = NSKeyedArchiver.archivedData(withRootObject: contact)
|
|
guard !data.isEmpty else {
|
|
owsFailDebug("Could not serialize contact: \(label).")
|
|
return nil
|
|
}
|
|
return data
|
|
}
|
|
|
|
private static func deserializeContact(data: Data, label: String) -> Contact? {
|
|
do {
|
|
guard let contact = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? Contact else {
|
|
owsFailDebug("Invalid value: \(label).")
|
|
return nil
|
|
}
|
|
return contact
|
|
} catch {
|
|
owsFailDebug("Deserialize failed[\(label)]: \(error).")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func contact(forPhoneNumber phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact? {
|
|
guard let data = Self.phoneNumberStore.getData(phoneNumber, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return Self.deserializeContact(data: data, label: phoneNumber)
|
|
}
|
|
|
|
@objc
|
|
public func allContacts(transaction: SDSAnyReadTransaction) -> [Contact] {
|
|
var contacts = [Contact]()
|
|
Self.uniqueIdStore.enumerateKeys(transaction: transaction) { (key, _) in
|
|
guard let data = Self.uniqueIdStore.getData(key, transaction: transaction) else {
|
|
owsFailDebug("Missing data for key: \(key).")
|
|
return
|
|
}
|
|
guard let contact = Self.deserializeContact(data: data, label: key) else {
|
|
return
|
|
}
|
|
contacts.append(contact)
|
|
}
|
|
return contacts
|
|
}
|
|
|
|
@objc
|
|
public func contactsMaps(transaction: SDSAnyReadTransaction) -> ContactsMaps {
|
|
let contacts = allContacts(transaction: transaction)
|
|
let localNumber: String? = tsAccountManager.localNumber(with: transaction)
|
|
return ContactsMaps.build(contacts: contacts, localNumber: localNumber)
|
|
}
|
|
|
|
@objc
|
|
public func setContactsMaps(_ newContactsMaps: ContactsMaps,
|
|
localNumber: String?,
|
|
transaction: SDSAnyWriteTransaction) {
|
|
|
|
let oldContactsMaps = self.contactsMaps(transaction: transaction)
|
|
if oldContactsMaps.isEqualForCache(newContactsMaps) {
|
|
Logger.verbose("Ignoring redundant contactsMap update.")
|
|
return
|
|
}
|
|
|
|
Self.uniqueIdStore.removeAll(transaction: transaction)
|
|
Self.phoneNumberStore.removeAll(transaction: transaction)
|
|
|
|
for contact in newContactsMaps.uniqueIdToContactMap.values {
|
|
let phoneNumbers = ContactsMaps.phoneNumbers(forContact: contact, localNumber: localNumber)
|
|
guard !phoneNumbers.isEmpty else {
|
|
continue
|
|
}
|
|
guard let contactData = Self.serializeContact(contact, label: contact.uniqueId) else {
|
|
continue
|
|
}
|
|
|
|
Self.uniqueIdStore.setData(contactData, key: contact.uniqueId, transaction: transaction)
|
|
|
|
for phoneNumber in phoneNumbers {
|
|
Self.phoneNumberStore.setData(contactData, key: phoneNumber, transaction: transaction)
|
|
}
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func warmCaches(transaction: SDSAnyReadTransaction) -> ContactsManagerCacheSummary {
|
|
// Only load the summary.
|
|
let contactCount = Self.uniqueIdStore.numberOfKeys(transaction: transaction)
|
|
let signalAccountCount = SignalAccount.anyCount(transaction: transaction)
|
|
return ContactsManagerCacheSummary(contactCount: contactCount, signalAccountCount: signalAccountCount)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
public class ContactsManagerCacheInMemory: NSObject, ContactsManagerCache {
|
|
private let unfairLock = UnfairLock()
|
|
|
|
private let contactsManagerCacheInDatabase = ContactsManagerCacheInDatabase()
|
|
|
|
private var sortedSignalAccountsCache: [SignalAccount]?
|
|
|
|
private var contactsMapsCache: ContactsMaps?
|
|
|
|
@objc
|
|
public func unsortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] {
|
|
// The in-memory cache always maintains a sorted list;
|
|
// use that even if we don't need sorted results.
|
|
sortedSignalAccounts(transaction: transaction)
|
|
}
|
|
|
|
// Order respects the systems contact sorting preference.
|
|
@objc
|
|
public func sortedSignalAccounts(transaction: SDSAnyReadTransaction) -> [SignalAccount] {
|
|
// Prefer cache.
|
|
if let cachedValue = (unfairLock.withLock { sortedSignalAccountsCache }) {
|
|
return cachedValue
|
|
}
|
|
// Fail over.
|
|
let result = contactsManagerCacheInDatabase.sortedSignalAccounts(transaction: transaction)
|
|
unfairLock.withLock { sortedSignalAccountsCache = result }
|
|
return result
|
|
}
|
|
|
|
@objc
|
|
public func setSortedSignalAccounts(_ signalAccounts: [SignalAccount]) {
|
|
unfairLock.withLock {
|
|
// Update cache.
|
|
sortedSignalAccountsCache = signalAccounts
|
|
}
|
|
return contactsManagerCacheInDatabase.setSortedSignalAccounts(signalAccounts)
|
|
}
|
|
|
|
@objc
|
|
public func contact(forPhoneNumber phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact? {
|
|
unfairLock.withLock {
|
|
guard let contactsMaps = contactsMapsCache else {
|
|
owsFailDebug("Missing contactsMaps.")
|
|
// Don't bother failing over to contactsManagerCacheInDatabase.
|
|
return nil
|
|
}
|
|
return contactsMaps.phoneNumberToContactMap[phoneNumber]
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func allContacts(transaction: SDSAnyReadTransaction) -> [Contact] {
|
|
unfairLock.withLock {
|
|
guard let contactsMaps = contactsMapsCache else {
|
|
owsFailDebug("Missing contactsMaps.")
|
|
// Don't bother failing over to contactsManagerCacheInDatabase.
|
|
return []
|
|
}
|
|
return Array(contactsMaps.phoneNumberToContactMap.values)
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func contactsMaps(transaction: SDSAnyReadTransaction) -> ContactsMaps {
|
|
unfairLock.withLock {
|
|
guard let contactsMaps = contactsMapsCache else {
|
|
owsFailDebug("Missing contactsMaps.")
|
|
// Don't bother failing over to contactsManagerCacheInDatabase.
|
|
return .empty
|
|
}
|
|
return contactsMaps
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func setContactsMaps(_ contactsMaps: ContactsMaps,
|
|
localNumber: String?,
|
|
transaction: SDSAnyWriteTransaction) {
|
|
unfairLock.withLock {
|
|
// Update cache.
|
|
contactsMapsCache = contactsMaps
|
|
}
|
|
return contactsManagerCacheInDatabase.setContactsMaps(contactsMaps,
|
|
localNumber: localNumber,
|
|
transaction: transaction)
|
|
}
|
|
|
|
@objc
|
|
public func warmCaches(transaction: SDSAnyReadTransaction) -> ContactsManagerCacheSummary {
|
|
|
|
// We consult the contactsMapsCache when sorting the
|
|
// sortedSignalAccounts, so make sure it is set first.
|
|
let contactsMaps = contactsManagerCacheInDatabase.contactsMaps(transaction: transaction)
|
|
unfairLock.withLock {
|
|
contactsMapsCache = contactsMaps
|
|
}
|
|
|
|
let sortedSignalAccounts = contactsManagerCacheInDatabase.sortedSignalAccounts(transaction: transaction)
|
|
unfairLock.withLock {
|
|
sortedSignalAccountsCache = sortedSignalAccounts
|
|
}
|
|
|
|
// Don't call contactsManagerCacheInDatabase.warmCaches().
|
|
return ContactsManagerCacheSummary(contactCount: UInt(contactsMaps.uniqueIdToContactMap.count),
|
|
signalAccountCount: UInt(sortedSignalAccounts.count))
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@objc
|
|
extension OWSContactsManager {
|
|
public func contact(forPhoneNumber phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact? {
|
|
contactsManagerCache.contact(forPhoneNumber: phoneNumber, transaction: transaction)
|
|
}
|
|
|
|
public func contactsMaps(transaction: SDSAnyReadTransaction) -> ContactsMaps {
|
|
contactsManagerCache.contactsMaps(transaction: transaction)
|
|
}
|
|
}
|