// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation import Contacts import ContactsUI import SignalServiceKit enum Result { case success(T) case error(ErrorType) } protocol ContactStoreAdaptee { var authorizationStatus: ContactStoreAuthorizationStatus { get } var supportsContactEditing: Bool { get } func requestAccess(completionHandler: @escaping (Bool, Error?) -> Void) func fetchContacts() -> Result<[Contact], Error> func fetchCNContact(contactId: String) -> CNContact? func startObservingChanges(changeHandler: @escaping () -> Void) } public class ContactsFrameworkContactStoreAdaptee: NSObject, ContactStoreAdaptee { private let contactStore = CNContactStore() private var changeHandler: (() -> Void)? private var initializedObserver = false private var lastSortOrder: CNContactSortOrder? let supportsContactEditing = true public static let allowedContactKeys: [CNKeyDescriptor] = [ CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactThumbnailImageDataKey as CNKeyDescriptor, // TODO full image instead of thumbnail? CNContactPhoneNumbersKey as CNKeyDescriptor, CNContactEmailAddressesKey as CNKeyDescriptor, CNContactPostalAddressesKey as CNKeyDescriptor, CNContactViewController.descriptorForRequiredKeys(), CNContactVCardSerialization.descriptorForRequiredKeys() ] var authorizationStatus: ContactStoreAuthorizationStatus { switch CNContactStore.authorizationStatus(for: CNEntityType.contacts) { case .notDetermined: return .notDetermined case .restricted: return .restricted case .denied: return .denied case .authorized: return .authorized } } func startObservingChanges(changeHandler: @escaping () -> Void) { // should only call once assert(self.changeHandler == nil) self.changeHandler = changeHandler self.lastSortOrder = CNContactsUserDefaults.shared().sortOrder NotificationCenter.default.addObserver(self, selector: #selector(runChangeHandler), name: .CNContactStoreDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: .OWSApplicationDidBecomeActive, object: nil) } @objc func didBecomeActive() { AppReadiness.runNowOrWhenAppDidBecomeReady { let currentSortOrder = CNContactsUserDefaults.shared().sortOrder guard currentSortOrder != self.lastSortOrder else { // sort order unchanged return } Logger.info("sort order changed: \(String(describing: self.lastSortOrder)) -> \(String(describing: currentSortOrder))") self.lastSortOrder = currentSortOrder self.runChangeHandler() } } @objc func runChangeHandler() { guard let changeHandler = self.changeHandler else { owsFailDebug("trying to run change handler before it was registered") return } changeHandler() } func requestAccess(completionHandler: @escaping (Bool, Error?) -> Void) { self.contactStore.requestAccess(for: .contacts, completionHandler: completionHandler) } func fetchContacts() -> Result<[Contact], Error> { var systemContacts = [CNContact]() do { let contactFetchRequest = CNContactFetchRequest(keysToFetch: ContactsFrameworkContactStoreAdaptee.allowedContactKeys) contactFetchRequest.sortOrder = .userDefault try self.contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in systemContacts.append(contact) } } catch let error as NSError { owsFailDebug("Failed to fetch contacts with error:\(error)") return .error(error) } let contacts = systemContacts.map { Contact(systemContact: $0) } return .success(contacts) } func fetchCNContact(contactId: String) -> CNContact? { var result: CNContact? do { let contactFetchRequest = CNContactFetchRequest(keysToFetch: ContactsFrameworkContactStoreAdaptee.allowedContactKeys) contactFetchRequest.sortOrder = .userDefault contactFetchRequest.predicate = CNContact.predicateForContacts(withIdentifiers: [contactId]) try self.contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in guard result == nil else { owsFailDebug("More than one contact with contact id.") return } result = contact } } catch let error as NSError { owsFailDebug("Failed to fetch contact with error:\(error)") return nil } return result } } @objc public enum ContactStoreAuthorizationStatus: UInt { case notDetermined, restricted, denied, authorized } @objc public protocol SystemContactsFetcherDelegate: class { func systemContactsFetcher(_ systemContactsFetcher: SystemContactsFetcher, updatedContacts contacts: [Contact], isUserRequested: Bool) func systemContactsFetcher(_ systemContactsFetcher: SystemContactsFetcher, hasAuthorizationStatus authorizationStatus: ContactStoreAuthorizationStatus) } @objc public class SystemContactsFetcher: NSObject { private let serialQueue = DispatchQueue(label: "SystemContactsFetcherQueue") var lastContactUpdateHash: Int? var lastDelegateNotificationDate: Date? let contactStoreAdapter: ContactsFrameworkContactStoreAdaptee @objc public weak var delegate: SystemContactsFetcherDelegate? public var authorizationStatus: ContactStoreAuthorizationStatus { return contactStoreAdapter.authorizationStatus } @objc public var isAuthorized: Bool { guard self.authorizationStatus != .notDetermined else { owsFailDebug("should have called `requestOnce` before checking authorization status.") return false } return self.authorizationStatus == .authorized } @objc public var isDenied: Bool { return self.authorizationStatus == .denied } @objc public private(set) var systemContactsHaveBeenRequestedAtLeastOnce = false private var hasSetupObservation = false override init() { self.contactStoreAdapter = ContactsFrameworkContactStoreAdaptee() super.init() SwiftSingletons.register(self) } @objc public var supportsContactEditing: Bool { return self.contactStoreAdapter.supportsContactEditing } private func setupObservationIfNecessary() { AssertIsOnMainThread() guard !hasSetupObservation else { return } hasSetupObservation = true self.contactStoreAdapter.startObservingChanges { [weak self] in DispatchQueue.main.async { self?.refreshAfterContactsChange() } } } /** * Ensures we've requested access for system contacts. This can be used in multiple places, * where we might need contact access, but will ensure we don't wastefully reload contacts * if we have already fetched contacts. * * @param completionParam completion handler is called on main thread. */ @objc public func requestOnce(completion completionParam: ((Error?) -> Void)?) { AssertIsOnMainThread() // Ensure completion is invoked on main thread. let completion = { error in DispatchMainThreadSafe({ completionParam?(error) }) } guard !systemContactsHaveBeenRequestedAtLeastOnce else { completion(nil) return } setupObservationIfNecessary() switch authorizationStatus { case .notDetermined: return completion(nil) // Loki: Original code // ======== // if CurrentAppContext().isInBackground() { // Logger.error("do not request contacts permission when app is in background") // completion(nil) // return // } // self.contactStoreAdapter.requestAccess { (granted, error) in // if let error = error { // Logger.error("error fetching contacts: \(error)") // completion(error) // return // } // // guard granted else { // // This case should have been caught by the error guard a few lines up. // owsFailDebug("declined contact access.") // completion(nil) // return // } // // DispatchQueue.main.async { // self.updateContacts(completion: completion) // } // } // ======== case .authorized: self.updateContacts(completion: completion) case .denied, .restricted: Logger.debug("contacts were \(self.authorizationStatus)") self.delegate?.systemContactsFetcher(self, hasAuthorizationStatus: authorizationStatus) completion(nil) } } @objc public func fetchOnceIfAlreadyAuthorized() { AssertIsOnMainThread() guard authorizationStatus == .authorized else { self.delegate?.systemContactsFetcher(self, hasAuthorizationStatus: authorizationStatus) return } guard !systemContactsHaveBeenRequestedAtLeastOnce else { return } updateContacts(completion: nil, isUserRequested: false) } @objc public func userRequestedRefresh(completion: @escaping (Error?) -> Void) { AssertIsOnMainThread() guard authorizationStatus == .authorized else { owsFailDebug("should have already requested contact access") self.delegate?.systemContactsFetcher(self, hasAuthorizationStatus: authorizationStatus) completion(nil) return } updateContacts(completion: completion, isUserRequested: true) } @objc public func refreshAfterContactsChange() { AssertIsOnMainThread() guard authorizationStatus == .authorized else { Logger.info("ignoring contacts change; no access.") self.delegate?.systemContactsFetcher(self, hasAuthorizationStatus: authorizationStatus) return } updateContacts(completion: nil, isUserRequested: false) } private func updateContacts(completion completionParam: ((Error?) -> Void)?, isUserRequested: Bool = false) { AssertIsOnMainThread() var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: "\(#function)", completionBlock: { [weak self] status in AssertIsOnMainThread() guard status == .expired else { return } guard let _ = self else { return } Logger.error("background task time ran out before contacts fetch completed.") }) // Ensure completion is invoked on main thread. let completion: (Error?) -> Void = { error in DispatchMainThreadSafe({ completionParam?(error) assert(backgroundTask != nil) backgroundTask = nil }) } systemContactsHaveBeenRequestedAtLeastOnce = true setupObservationIfNecessary() serialQueue.async { Logger.info("fetching contacts") var fetchedContacts: [Contact]? switch self.contactStoreAdapter.fetchContacts() { case .success(let result): fetchedContacts = result case .error(let error): completion(error) return } guard let contacts = fetchedContacts else { owsFailDebug("contacts was unexpectedly not set.") return completion(nil) } Logger.info("fetched \(contacts.count) contacts.") let contactsHash = HashableArray(contacts).hashValue DispatchQueue.main.async { var shouldNotifyDelegate = false if self.lastContactUpdateHash != contactsHash { Logger.info("contact hash changed. new contactsHash: \(contactsHash)") shouldNotifyDelegate = true } else if isUserRequested { Logger.info("ignoring debounce due to user request") shouldNotifyDelegate = true } else { // If nothing has changed, only notify delegate (to perform contact intersection) every N hours if let lastDelegateNotificationDate = self.lastDelegateNotificationDate { let kDebounceInterval = TimeInterval(12 * 60 * 60) let expiresAtDate = Date(timeInterval: kDebounceInterval, since: lastDelegateNotificationDate) if Date() > expiresAtDate { Logger.info("debounce interval expired at: \(expiresAtDate)") shouldNotifyDelegate = true } else { Logger.info("ignoring since debounce interval hasn't expired") } } else { Logger.info("first contact fetch. contactsHash: \(contactsHash)") shouldNotifyDelegate = true } } guard shouldNotifyDelegate else { Logger.info("no reason to notify delegate.") completion(nil) return } self.lastDelegateNotificationDate = Date() self.lastContactUpdateHash = contactsHash self.delegate?.systemContactsFetcher(self, updatedContacts: contacts, isUserRequested: isUserRequested) completion(nil) } } } @objc public func fetchCNContact(contactId: String) -> CNContact? { guard authorizationStatus == .authorized else { Logger.error("contact fetch failed; no access.") return nil } return contactStoreAdapter.fetchCNContact(contactId: contactId) } } struct HashableArray: Hashable { var elements: [Element] init(_ elements: [Element]) { self.elements = elements } var hashValue: Int { // random generated 32bit number let base = 224712574 var position = 0 return elements.reduce(base) { (result, element) -> Int in // Make sure change in sort order invalidates hash position += 1 return result ^ element.hashValue + position } } static func == (lhs: HashableArray, rhs: HashableArray) -> Bool { return lhs.hashValue == rhs.hashValue } }