Swiftgram/TelegramUI/DeviceContactDataManager.swift
2018-08-03 23:23:02 +03:00

453 lines
18 KiB
Swift

import Foundation
import SwiftSignalKit
import TelegramCore
import Contacts
import AddressBook
public typealias DeviceContactStableId = String
private protocol DeviceContactDataContext {
func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData?
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData?
func createContactWithData(_ contactData: DeviceContactExtendedData) -> (DeviceContactStableId, DeviceContactExtendedData)?
}
@available(iOSApplicationExtension 9.0, *)
private final class DeviceContactDataModernContext: DeviceContactDataContext {
let store = CNContactStore()
var updateHandle: NSObjectProtocol?
var currentContacts: [DeviceContactStableId: DeviceContactBasicData] = [:]
init(queue: Queue, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void) {
self.currentContacts = self.retrieveContacts()
updated(self.currentContacts)
let handle = NotificationCenter.default.addObserver(forName: NSNotification.Name.CNContactStoreDidChange, object: nil, queue: nil, using: { [weak self] _ in
queue.async {
guard let strongSelf = self else {
return
}
let contacts = strongSelf.retrieveContacts()
if strongSelf.currentContacts != contacts {
strongSelf.currentContacts = contacts
updated(strongSelf.currentContacts)
}
}
})
self.updateHandle = handle
}
deinit {
if let updateHandle = updateHandle {
NotificationCenter.default.removeObserver(updateHandle)
}
}
private func retrieveContacts() -> [DeviceContactStableId: DeviceContactBasicData] {
let keysToFetch: [CNKeyDescriptor] = [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor]
let request = CNContactFetchRequest(keysToFetch: keysToFetch)
request.unifyResults = true
var result: [DeviceContactStableId: DeviceContactBasicData] = [:]
let _ = try? self.store.enumerateContacts(with: request, usingBlock: { contact, _ in
let stableIdAndContact = DeviceContactDataModernContext.parseContact(contact)
result[stableIdAndContact.0] = stableIdAndContact.1
})
return result
}
private static func parseContact(_ contact: CNContact) -> (DeviceContactStableId, DeviceContactBasicData) {
var phoneNumbers: [DeviceContactPhoneNumberData] = []
for number in contact.phoneNumbers {
phoneNumbers.append(DeviceContactPhoneNumberData(label: number.label ?? "", value: number.value.stringValue))
}
return (contact.identifier, DeviceContactBasicData(firstName: contact.givenName, lastName: contact.familyName, phoneNumbers: phoneNumbers))
}
func getExtendedContactData(stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
let keysToFetch: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactBirthdayKey as CNKeyDescriptor,
CNContactSocialProfilesKey as CNKeyDescriptor,
CNContactInstantMessageAddressesKey as CNKeyDescriptor,
CNContactPostalAddressesKey as CNKeyDescriptor,
CNContactUrlAddressesKey as CNKeyDescriptor,
CNContactOrganizationNameKey as CNKeyDescriptor,
CNContactJobTitleKey as CNKeyDescriptor,
CNContactDepartmentNameKey as CNKeyDescriptor
]
guard let contact = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else {
return nil
}
return DeviceContactExtendedData(contact: contact)
}
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> DeviceContactExtendedData? {
let keysToFetch: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactBirthdayKey as CNKeyDescriptor,
CNContactSocialProfilesKey as CNKeyDescriptor,
CNContactInstantMessageAddressesKey as CNKeyDescriptor,
CNContactPostalAddressesKey as CNKeyDescriptor,
CNContactUrlAddressesKey as CNKeyDescriptor,
CNContactOrganizationNameKey as CNKeyDescriptor,
CNContactJobTitleKey as CNKeyDescriptor,
CNContactDepartmentNameKey as CNKeyDescriptor
]
guard let current = try? self.store.unifiedContact(withIdentifier: stableId, keysToFetch: keysToFetch) else {
return nil
}
let contact = contactData.asMutableCNContact()
let mutableContact = current.mutableCopy() as! CNMutableContact
mutableContact.givenName = contact.givenName
mutableContact.familyName = contact.familyName
var phoneNumbers = mutableContact.phoneNumbers
for phoneNumber in contact.phoneNumbers.reversed() {
var found = false
inner: for n in phoneNumbers {
if n.value.stringValue == phoneNumber.value.stringValue {
found = true
break inner
}
}
if !found {
phoneNumbers.insert(phoneNumber, at: 0)
}
}
mutableContact.phoneNumbers = phoneNumbers
var urlAddresses = mutableContact.urlAddresses
for urlAddress in contact.urlAddresses.reversed() {
var found = false
inner: for n in urlAddresses {
if n.value.isEqual(urlAddress.value) {
found = true
break inner
}
}
if !found {
urlAddresses.insert(urlAddress, at: 0)
}
}
mutableContact.urlAddresses = urlAddresses
var emailAddresses = mutableContact.emailAddresses
for emailAddress in contact.emailAddresses.reversed() {
var found = false
inner: for n in emailAddresses {
if n.value.isEqual(emailAddress.value) {
found = true
break inner
}
}
if !found {
emailAddresses.insert(emailAddress, at: 0)
}
}
mutableContact.emailAddresses = emailAddresses
var postalAddresses = mutableContact.postalAddresses
for postalAddress in contact.postalAddresses.reversed() {
var found = false
inner: for n in postalAddresses {
if n.value.isEqual(postalAddress.value) {
found = true
break inner
}
}
if !found {
postalAddresses.insert(postalAddress, at: 0)
}
}
mutableContact.postalAddresses = postalAddresses
if contact.birthday != nil {
mutableContact.birthday = contact.birthday
}
var socialProfiles = mutableContact.socialProfiles
for socialProfile in contact.socialProfiles.reversed() {
var found = false
inner: for n in socialProfiles {
if n.value.username.lowercased() == socialProfile.value.username.lowercased() && n.value.service.lowercased() == socialProfile.value.service.lowercased() {
found = true
break inner
}
}
if !found {
socialProfiles.insert(socialProfile, at: 0)
}
}
mutableContact.socialProfiles = socialProfiles
var instantMessageAddresses = mutableContact.instantMessageAddresses
for instantMessageAddress in contact.instantMessageAddresses.reversed() {
var found = false
inner: for n in instantMessageAddresses {
if n.value.isEqual(instantMessageAddress.value) {
found = true
break inner
}
}
if !found {
instantMessageAddresses.insert(instantMessageAddress, at: 0)
}
}
mutableContact.instantMessageAddresses = instantMessageAddresses
let saveRequest = CNSaveRequest()
saveRequest.update(mutableContact)
let _ = try? self.store.execute(saveRequest)
return DeviceContactExtendedData(contact: mutableContact)
}
func createContactWithData(_ contactData: DeviceContactExtendedData) -> (DeviceContactStableId, DeviceContactExtendedData)? {
let saveRequest = CNSaveRequest()
let mutableContact = contactData.asMutableCNContact()
saveRequest.add(mutableContact, toContainerWithIdentifier: nil)
let _ = try? self.store.execute(saveRequest)
return (mutableContact.identifier, contactData)
}
}
private final class ExtendedContactDataContext {
var value: DeviceContactExtendedData?
let subscribers = Bag<(DeviceContactExtendedData) -> Void>()
}
private final class DeviceContactDataManagerImpl {
private let queue: Queue
private var dataContext: DeviceContactDataContext?
private var extendedContexts: [DeviceContactStableId: ExtendedContactDataContext] = [:]
private var stableIdToBasicContactData: [DeviceContactStableId: DeviceContactBasicData] = [:]
private var normalizedPhoneNumberToStableId: [DeviceContactNormalizedPhoneNumber: [DeviceContactStableId]] = [:]
private var importableContacts = Set<ImportableDeviceContact>()
private var accessDisposable: Disposable?
private let dataDisposable = MetaDisposable()
private let basicDataSubscribers = Bag<([DeviceContactStableId: DeviceContactBasicData]) -> Void>()
private let importableContactsSubscribers = Bag<(Set<ImportableDeviceContact>) -> Void>()
init(queue: Queue) {
self.queue = queue
self.accessDisposable = (DeviceAccess.contacts
|> deliverOn(self.queue)).start(next: { [weak self] authorizationStatus in
guard let strongSelf = self else {
return
}
if let authorizationStatus = authorizationStatus, authorizationStatus {
if #available(iOSApplicationExtension 9.0, *) {
strongSelf.dataContext = DeviceContactDataModernContext(queue: strongSelf.queue, updated: { stableIdToBasicContactData in
guard let strongSelf = self else {
return
}
strongSelf.updateAll(stableIdToBasicContactData)
})
} else {
}
} else {
strongSelf.updateAll([:])
}
})
}
deinit {
self.accessDisposable?.dispose()
}
private func updateAll(_ stableIdToBasicContactData: [DeviceContactStableId: DeviceContactBasicData]) {
self.stableIdToBasicContactData = stableIdToBasicContactData
var normalizedPhoneNumberToStableId: [DeviceContactNormalizedPhoneNumber: [DeviceContactStableId]] = [:]
for (stableId, basicData) in self.stableIdToBasicContactData {
for phoneNumber in basicData.phoneNumbers {
let normalizedPhoneNumber = DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phoneNumber.value))
if normalizedPhoneNumberToStableId[normalizedPhoneNumber] == nil {
normalizedPhoneNumberToStableId[normalizedPhoneNumber] = []
}
normalizedPhoneNumberToStableId[normalizedPhoneNumber]!.append(stableId)
}
}
self.normalizedPhoneNumberToStableId = normalizedPhoneNumberToStableId
for f in self.basicDataSubscribers.copyItems() {
f(self.stableIdToBasicContactData)
}
}
func basicData(updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void) -> Disposable {
let queue = self.queue
let index = self.basicDataSubscribers.add({ data in
updated(data)
})
updated(self.stableIdToBasicContactData)
return ActionDisposable { [weak self] in
queue.async {
guard let strongSelf = self else {
return
}
strongSelf.basicDataSubscribers.remove(index)
}
}
}
func extendedData(stableId: String, updated: @escaping (DeviceContactExtendedData?) -> Void) -> Disposable {
let queue = self.queue
let current = self.dataContext?.getExtendedContactData(stableId: stableId)
updated(current)
return ActionDisposable { [weak self] in
queue.async {
}
}
}
func importable(updated: @escaping (Set<ImportableDeviceContact>) -> Void) -> Disposable {
let queue = self.queue
let index = self.importableContactsSubscribers.add({ data in
updated(data)
})
updated(self.importableContacts)
return ActionDisposable { [weak self] in
queue.async {
guard let strongSelf = self else {
return
}
strongSelf.importableContactsSubscribers.remove(index)
}
}
}
func search(query: String, updated: @escaping ([DeviceContactStableId: DeviceContactBasicData]) -> Void) -> Disposable {
let normalizedQuery = query.lowercased()
var result: [DeviceContactStableId: DeviceContactBasicData] = [:]
for (stableId, basicData) in self.stableIdToBasicContactData {
if basicData.firstName.lowercased().hasPrefix(normalizedQuery) || basicData.lastName.lowercased().hasPrefix(normalizedQuery) {
result[stableId] = basicData
}
}
updated(result)
return EmptyDisposable
}
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId, completion: @escaping (DeviceContactExtendedData?) -> Void) {
let result = self.dataContext?.appendContactData(contactData, to: stableId)
completion(result)
}
func createContactWithData(_ contactData: DeviceContactExtendedData, completion: @escaping ((DeviceContactStableId, DeviceContactExtendedData)?) -> Void) {
let result = self.dataContext?.createContactWithData(contactData)
completion(result)
}
}
public final class DeviceContactDataManager {
private let queue = Queue()
private let impl: QueueLocalObject<DeviceContactDataManagerImpl>
init() {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return DeviceContactDataManagerImpl(queue: queue)
})
}
public func basicData() -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.basicData(updated: { value in
subscriber.putNext(value)
}))
})
return disposable
}
}
public func extendedData(stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.extendedData(stableId: stableId, updated: { value in
subscriber.putNext(value)
}))
})
return disposable
}
}
public func importable() -> Signal<Set<ImportableDeviceContact>, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.importable(updated: { value in
subscriber.putNext(value)
}))
})
return disposable
}
}
public func search(query: String) -> Signal<[DeviceContactStableId: DeviceContactBasicData], NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
disposable.set(impl.search(query: query, updated: { value in
subscriber.putNext(value)
subscriber.putCompletion()
}))
})
return disposable
}
}
func appendContactData(_ contactData: DeviceContactExtendedData, to stableId: DeviceContactStableId) -> Signal<DeviceContactExtendedData?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
impl.appendContactData(contactData, to: stableId, completion: { next in
subscriber.putNext(next)
subscriber.putCompletion()
})
})
return disposable
}
}
func createContactWithData(_ contactData: DeviceContactExtendedData) -> Signal<(DeviceContactStableId, DeviceContactExtendedData)?, NoError> {
return Signal { subscriber in
let disposable = MetaDisposable()
self.impl.with({ impl in
impl.createContactWithData(contactData, completion: { next in
subscriber.putNext(next)
subscriber.putCompletion()
})
})
return disposable
}
}
}