mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			438 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			438 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			Swift
		
	
//
 | 
						|
//  Copyright (c) 2018 Open Whisper Systems. All rights reserved.
 | 
						|
//
 | 
						|
 | 
						|
import Foundation
 | 
						|
import Contacts
 | 
						|
import ContactsUI
 | 
						|
import SignalServiceKit
 | 
						|
 | 
						|
enum Result<T, ErrorType> {
 | 
						|
    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.")
 | 
						|
                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<Element: Hashable>: 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
 | 
						|
    }
 | 
						|
}
 |