202 lines
7.7 KiB
Swift
202 lines
7.7 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Provides data on system contacts.
|
|
///
|
|
/// On primary devices, this is populated from the local address book. On
|
|
/// linked devices, this is populated from the primary's address book (via
|
|
/// contact syncs/storage service).
|
|
protocol SystemContactsDataProvider {
|
|
/// Fetches a single contact from the underlying data store.
|
|
func fetchSystemContact(for phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact?
|
|
|
|
/// Fetches all contacts from the underlying data store.
|
|
func fetchAllSystemContacts(transaction: SDSAnyReadTransaction) -> [Contact]
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
final class PrimaryDeviceSystemContactsDataProvider: SystemContactsDataProvider {
|
|
|
|
private static let deprecated_uniqueIdStore = SDSKeyValueStore(collection: "ContactsManagerCache.uniqueIdStore")
|
|
private static let phoneNumberStore = SDSKeyValueStore(collection: "ContactsManagerCache.phoneNumberStore")
|
|
private static let contactsStore = SDSKeyValueStore(collection: "ContactsManagerCache.allContacts")
|
|
|
|
private static func serializeContact(_ contact: Contact, label: String) -> Data? {
|
|
do {
|
|
return try NSKeyedArchiver.archivedData(withRootObject: contact, requiringSecureCoding: false)
|
|
} catch {
|
|
owsFailDebug("Could not serialize contact (\(label)): \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private static func serializeContacts(_ contacts: [Contact]) -> Data? {
|
|
do {
|
|
return try NSKeyedArchiver.archivedData(withRootObject: contacts, requiringSecureCoding: false)
|
|
} catch {
|
|
owsFailDebug("Could not serialize contacts: \(error)")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
private static func deserializeContacts(data: Data) -> [Contact]? {
|
|
do {
|
|
guard let contacts = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [Contact] else {
|
|
owsFailDebug("Invalid contacts array value.")
|
|
return nil
|
|
}
|
|
return contacts
|
|
} catch {
|
|
owsFailDebug("Deserialize contacts array failed: \(error).")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func fetchSystemContact(for phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact? {
|
|
guard let data = Self.phoneNumberStore.getData(phoneNumber, transaction: transaction) else {
|
|
return nil
|
|
}
|
|
return Self.deserializeContact(data: data, label: phoneNumber)
|
|
}
|
|
|
|
func fetchAllSystemContacts(transaction: SDSAnyReadTransaction) -> [Contact] {
|
|
var contacts = [Contact]()
|
|
// reading and materializing all entries at once is *much* faster
|
|
Self.contactsStore.enumerateKeys(transaction: transaction) { (key, _) in
|
|
// the entries may be written in chunks to reduce memory footprint
|
|
if let data = Self.contactsStore.getData(key, transaction: transaction), let array = Self.deserializeContacts(data: data) {
|
|
contacts.append(contentsOf: array)
|
|
}
|
|
}
|
|
return contacts
|
|
}
|
|
|
|
func setContactsMaps(
|
|
_ newContactsMaps: ContactsMaps,
|
|
oldContactsMaps: () -> ContactsMaps,
|
|
localNumber: String?,
|
|
transaction: SDSAnyWriteTransaction
|
|
) {
|
|
// This store is deprecated, so clean it up during other modifications.
|
|
Self.deprecated_uniqueIdStore.removeAll(transaction: transaction)
|
|
|
|
guard !newContactsMaps.uniqueIdToContactMap.isEmpty else {
|
|
Logger.info("wiping contacts info in db")
|
|
Self.phoneNumberStore.removeAll(transaction: transaction)
|
|
Self.contactsStore.removeAll(transaction: transaction)
|
|
return
|
|
}
|
|
|
|
var shallPersist = true
|
|
let oldContactsMaps = oldContactsMaps()
|
|
|
|
// If there aren't any changes, we don't need to modify anything.
|
|
if oldContactsMaps.isEqualForCache(newContactsMaps) {
|
|
shallPersist = false
|
|
return
|
|
}
|
|
|
|
func removePhoneNumbers(for contact: Contact) {
|
|
let phoneNumbers = ContactsMaps.phoneNumbers(forContact: contact, localNumber: localNumber)
|
|
Self.phoneNumberStore.removeValues(forKeys: phoneNumbers, transaction: transaction)
|
|
}
|
|
|
|
func addPhoneNumbers(for contact: Contact) {
|
|
guard let contactData = Self.serializeContact(contact, label: contact.uniqueId) else {
|
|
return
|
|
}
|
|
for phoneNumber in ContactsMaps.phoneNumbers(forContact: contact, localNumber: localNumber) {
|
|
Self.phoneNumberStore.setData(contactData, key: phoneNumber, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
for (contactUUID, oldContact) in oldContactsMaps.uniqueIdToContactMap {
|
|
guard newContactsMaps.uniqueIdToContactMap[contactUUID] == nil else {
|
|
continue
|
|
}
|
|
// The contact no longer exists -- remove it.
|
|
removePhoneNumbers(for: oldContact)
|
|
}
|
|
|
|
for (contactUUID, newContact) in newContactsMaps.uniqueIdToContactMap {
|
|
switch oldContactsMaps.uniqueIdToContactMap[contactUUID] {
|
|
case .some(let oldContact) where oldContact.isEqualForCache(newContact):
|
|
break
|
|
case .some(let oldContact):
|
|
removePhoneNumbers(for: oldContact)
|
|
addPhoneNumbers(for: newContact)
|
|
case .none:
|
|
addPhoneNumbers(for: newContact)
|
|
}
|
|
}
|
|
if shallPersist {
|
|
persist(contacts: newContactsMaps.allContacts, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
private func persist(contacts: [Contact], transaction: SDSAnyWriteTransaction) {
|
|
let chunkSize = 500
|
|
Logger.info("persisting \(contacts.count) entries using chunk size of \(chunkSize)")
|
|
Self.contactsStore.removeAll(transaction: transaction)
|
|
var contacts = contacts[...]
|
|
var chunkNr = 0
|
|
while !contacts.isEmpty {
|
|
chunkNr += 1
|
|
let chunk = Array(contacts.prefix(chunkSize))
|
|
contacts = contacts.dropFirst(chunkSize)
|
|
Logger.info("writing chunk#\(chunkNr) with \(chunk.count) entries")
|
|
if let data = Self.serializeContacts(chunk) {
|
|
Self.contactsStore.setData(data, key: "\(chunkNr)", transaction: transaction)
|
|
} else {
|
|
owsFailDebug("chunk#\(chunkNr) can not be serialized")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
final class LinkedDeviceSystemContactsDataProvider {
|
|
private let modelReadCaches: ModelReadCaches
|
|
|
|
init(modelReadCaches: ModelReadCaches) {
|
|
self.modelReadCaches = modelReadCaches
|
|
}
|
|
|
|
convenience init() {
|
|
struct GlobalDependencies: Dependencies {}
|
|
self.init(modelReadCaches: GlobalDependencies.modelReadCaches)
|
|
}
|
|
}
|
|
|
|
extension LinkedDeviceSystemContactsDataProvider: SystemContactsDataProvider {
|
|
func fetchSystemContact(for phoneNumber: String, transaction: SDSAnyReadTransaction) -> Contact? {
|
|
modelReadCaches.signalAccountReadCache.getSignalAccount(
|
|
phoneNumber: phoneNumber,
|
|
transaction: transaction
|
|
)?.contact
|
|
}
|
|
|
|
func fetchAllSystemContacts(transaction: SDSAnyReadTransaction) -> [Contact] {
|
|
SignalAccount.anyFetchAll(transaction: transaction).compactMap { $0.contact }
|
|
}
|
|
}
|