Merge branch 'mkirk/ios8-contacts'

pull/1/head
Michael Kirk 7 years ago
commit 92466aaf8d

@ -1,4 +1,4 @@
platform :ios, '9.0'
platform :ios, '8.0'
source 'https://github.com/CocoaPods/Specs.git'
target 'Signal' do

@ -161,6 +161,6 @@ SPEC CHECKSUMS:
UnionFind: c33be5adb12983981d6e827ea94fc7f9e370f52d
YapDatabase: cd911121580ff16675f65ad742a9eb0ab4d9e266
PODFILE CHECKSUM: 549de6756fe8eab98647be8561b3988361f62e85
PODFILE CHECKSUM: cb2cbbe74dab34123e1cb527417ef658aa60bd26
COCOAPODS: 1.2.1

@ -2474,7 +2474,7 @@
"\"$(SRCROOT)/Libraries\"/**",
);
INFOPLIST_FILE = "$(SRCROOT)/Signal/Signal-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
@ -2534,7 +2534,7 @@
"\"$(SRCROOT)/Libraries\"/**",
);
INFOPLIST_FILE = "$(SRCROOT)/Signal/Signal-Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 8.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
"$(inherited)",

@ -303,6 +303,13 @@ NS_ASSUME_NONNULL_BEGIN
{
SignalAccount *signalAccount = [self signalAccountForRecipientId:recipientId];
if (!self.contactsManager.supportsContactEditing) {
DDLogError(@"%@ Contact editing not supported.", self.tag);
// Should not expose UI that lets the user get here.
OWSAssert(NO);
return;
}
if (!self.contactsManager.isSystemContactsAuthorized) {
UIAlertController *alertController = [UIAlertController
alertControllerWithTitle:NSLocalizedString(@"EDIT_CONTACT_WITHOUT_CONTACTS_PERMISSION_ALERT_TITLE", comment
@ -384,6 +391,18 @@ NS_ASSUME_NONNULL_BEGIN
[UIUtil applyDefaultSystemAppearence];
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
@end
NS_ASSUME_NONNULL_END

@ -136,7 +136,7 @@ NS_ASSUME_NONNULL_BEGIN
{
OWSAssert(self.thread);
if ([self.thread isKindOfClass:[TSContactThread class]]) {
if ([self.thread isKindOfClass:[TSContactThread class]] && self.contactsManager.supportsContactEditing) {
self.navigationItem.rightBarButtonItem =
[[UIBarButtonItem alloc] initWithTitle:NSLocalizedString(@"EDIT_TXT", nil)
style:UIBarButtonItemStylePlain
@ -604,7 +604,9 @@ NS_ASSUME_NONNULL_BEGIN
[self showUpdateGroupView:UpdateGroupMode_EditGroupName];
}
} else {
[self presentContactViewController];
if (self.contactsManager.supportsContactEditing) {
[self presentContactViewController];
}
}
}
}
@ -677,6 +679,11 @@ NS_ASSUME_NONNULL_BEGIN
- (void)presentContactViewController
{
if (!self.contactsManager.supportsContactEditing) {
DDLogError(@"%@ Contact editing not supported", self.tag);
OWSAssert(NO);
return;
}
if (![self.thread isKindOfClass:[TSContactThread class]]) {
DDLogError(@"%@ unexpected thread: %@ in %s", self.tag, self.thread, __PRETTY_FUNCTION__);
OWSAssert(NO);

@ -156,15 +156,18 @@ NS_ASSUME_NONNULL_BEGIN
UIAlertController *actionSheetController =
[UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
NSString *contactInfoTitle = signalAccount
? NSLocalizedString(@"GROUP_MEMBERS_VIEW_CONTACT_INFO", @"Button label for the 'show contact info' button")
: NSLocalizedString(
@"GROUP_MEMBERS_ADD_CONTACT_INFO", @"Button label to add information to an unknown contact");
[actionSheetController addAction:[UIAlertAction actionWithTitle:contactInfoTitle
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
[self showContactInfoViewForRecipientId:recipientId];
}]];
if (self.contactsViewHelper.contactsManager.supportsContactEditing) {
NSString *contactInfoTitle = signalAccount
? NSLocalizedString(@"GROUP_MEMBERS_VIEW_CONTACT_INFO", @"Button label for the 'show contact info' button")
: NSLocalizedString(
@"GROUP_MEMBERS_ADD_CONTACT_INFO", @"Button label to add information to an unknown contact");
[actionSheetController addAction:[UIAlertAction actionWithTitle:contactInfoTitle
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *_Nonnull action) {
[self
showContactInfoViewForRecipientId:recipientId];
}]];
}
BOOL isBlocked;
if (signalAccount) {

@ -36,6 +36,8 @@ extern NSString *const OWSContactsManagerSignalAccountsDidChangeNotification;
// Must call `requestSystemContactsOnce` before accessing this method
@property (nonatomic, readonly) BOOL isSystemContactsAuthorized;
@property (nonatomic, readonly) BOOL supportsContactEditing;
// Request systems contacts and start syncing changes. The user will see an alert
// if they haven't previously.
- (void)requestSystemContactsOnce;

@ -73,6 +73,11 @@ NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
return self.systemContactsFetcher.isAuthorized;
}
- (BOOL)supportsContactEditing
{
return self.systemContactsFetcher.supportsContactEditing;
}
#pragma mark SystemContactsFetcherDelegate
- (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher

@ -6,6 +6,299 @@ import Foundation
import Contacts
import ContactsUI
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 startObservingChanges(changeHandler: @escaping () -> Void)
}
@available(iOS 9.0, *)
class ContactsFrameworkContactStoreAdaptee: ContactStoreAdaptee {
let TAG = "[ContactsFrameworkContactStoreAdaptee]"
private let contactStore = CNContactStore()
private var changeHandler: (() -> Void)?
private var initializedObserver = false
let supportsContactEditing = true
private let allowedContactKeys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactThumbnailImageDataKey as CNKeyDescriptor, // TODO full image instead of thumbnail?
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactViewController.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
NotificationCenter.default.addObserver(self, selector: #selector(runChangeHandler), name: .CNContactStoreDidChange, object: nil)
}
@objc
func runChangeHandler() {
guard let changeHandler = self.changeHandler else {
Logger.error("\(TAG) trying to run change handler before it was registered")
assertionFailure()
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: self.allowedContactKeys)
try self.contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
systemContacts.append(contact)
}
} catch let error as NSError {
Logger.error("\(self.TAG) Failed to fetch contacts with error:\(error)")
assertionFailure()
return .error(error)
}
let contacts = systemContacts.map { Contact(systemContact: $0) }
return .success(contacts)
}
}
let kAddressBookContactyStoreDidChangeNotificationName = NSNotification.Name("AddressBookContactStoreAdapteeDidChange")
/**
* System contact fetching compatible with iOS8
*/
class AddressBookContactStoreAdaptee: ContactStoreAdaptee {
let TAG = "[AddressBookContactStoreAdaptee]"
private var addressBook: ABAddressBook = ABAddressBookCreateWithOptions(nil, nil).takeRetainedValue()
private var changeHandler: (() -> Void)?
let supportsContactEditing = false
var authorizationStatus: ContactStoreAuthorizationStatus {
switch ABAddressBookGetAuthorizationStatus() {
case .notDetermined:
return .notDetermined
case .restricted:
return .restricted
case .denied:
return .denied
case .authorized:
return .authorized
}
}
@objc
func runChangeHandler() {
guard let changeHandler = self.changeHandler else {
Logger.error("\(TAG) trying to run change handler before it was registered")
assertionFailure()
return
}
changeHandler()
}
func startObservingChanges(changeHandler: @escaping () -> Void) {
// should only call once
assert(self.changeHandler == nil)
self.changeHandler = changeHandler
NotificationCenter.default.addObserver(self, selector: #selector(runChangeHandler), name: kAddressBookContactyStoreDidChangeNotificationName, object: nil)
let callback: ABExternalChangeCallback = { (_, _, _) in
// Ideally we'd just call the changeHandler here, but because this is a C style callback in swift,
// we can't capture any state in the closure, so we use a notification as a trampoline
NotificationCenter.default.post(name: kAddressBookContactyStoreDidChangeNotificationName, object: nil)
}
ABAddressBookRegisterExternalChangeCallback(addressBook, callback, nil)
}
func requestAccess(completionHandler: @escaping (Bool, Error?) -> Void) {
ABAddressBookRequestAccessWithCompletion(addressBook, completionHandler)
}
func fetchContacts() -> Result<[Contact], Error> {
// Changes are not reflected unless we create a new address book
self.addressBook = ABAddressBookCreateWithOptions(nil, nil).takeRetainedValue()
let allPeople = ABAddressBookCopyArrayOfAllPeopleInSourceWithSortOrdering(addressBook, nil, ABPersonGetSortOrdering()).takeRetainedValue() as [ABRecord]
let contacts = allPeople.map { self.buildContact(abRecord: $0) }
return .success(contacts)
}
private func buildContact(abRecord: ABRecord) -> Contact {
let addressBookRecord = OWSABRecord(abRecord: abRecord)
var firstName = addressBookRecord.firstName
let lastName = addressBookRecord.lastName
let phoneNumbers = addressBookRecord.phoneNumbers
if (firstName == nil && lastName == nil) {
if let companyName = addressBookRecord.companyName {
firstName = companyName
} else {
firstName = phoneNumbers.first
}
}
return Contact(contactWithFirstName: firstName,
andLastName: lastName,
andUserTextPhoneNumbers: phoneNumbers,
andImage: addressBookRecord.image,
andContactID: addressBookRecord.recordId)
}
}
/**
* Wrapper around ABRecord for easy property extraction.
* Some code lifted from:
* https://github.com/SocialbitGmbH/SwiftAddressBook/blob/c1993fa/Pod/Classes/SwiftAddressBookPerson.swift
*/
struct OWSABRecord {
public struct MultivalueEntry<T> {
public var value: T
public var label: String?
public let id: Int
public init(value: T, label: String?, id: Int) {
self.value = value
self.label = label
self.id = id
}
}
let abRecord: ABRecord
init(abRecord: ABRecord) {
self.abRecord = abRecord
}
var firstName: String? {
return self.extractProperty(kABPersonFirstNameProperty)
}
var lastName: String? {
return self.extractProperty(kABPersonLastNameProperty)
}
var companyName: String? {
return self.extractProperty(kABPersonOrganizationProperty)
}
var recordId: ABRecordID {
return ABRecordGetRecordID(abRecord)
}
// We don't yet support labels for our iOS8 users.
var phoneNumbers: [String] {
if let result: [MultivalueEntry<String>] = extractMultivalueProperty(kABPersonPhoneProperty) {
return result.map { $0.value }
} else {
return []
}
}
var image: UIImage? {
guard ABPersonHasImageData(abRecord) else {
return nil
}
guard let data = ABPersonCopyImageData(abRecord)?.takeRetainedValue() else {
return nil
}
return UIImage(data: data as Data)
}
private func extractProperty<T>(_ propertyName: ABPropertyID) -> T? {
let value: AnyObject? = ABRecordCopyValue(self.abRecord, propertyName)?.takeRetainedValue()
return value as? T
}
fileprivate func extractMultivalueProperty<T>(_ propertyName: ABPropertyID) -> Array<MultivalueEntry<T>>? {
guard let multivalue: ABMultiValue = extractProperty(propertyName) else { return nil }
var array = Array<MultivalueEntry<T>>()
for i: Int in 0..<(ABMultiValueGetCount(multivalue)) {
let value: T? = ABMultiValueCopyValueAtIndex(multivalue, i).takeRetainedValue() as? T
if let v: T = value {
let id: Int = Int(ABMultiValueGetIdentifierAtIndex(multivalue, i))
let optionalLabel = ABMultiValueCopyLabelAtIndex(multivalue, i)?.takeRetainedValue()
array.append(MultivalueEntry(value: v,
label: optionalLabel == nil ? nil : optionalLabel! as String,
id: id))
}
}
return !array.isEmpty ? array : nil
}
}
enum ContactStoreAuthorizationStatus {
case notDetermined,
restricted,
denied,
authorized
}
class ContactStoreAdapter: ContactStoreAdaptee {
let adaptee: ContactStoreAdaptee
init() {
if #available(iOS 9.0, *) {
self.adaptee = ContactsFrameworkContactStoreAdaptee()
} else {
self.adaptee = AddressBookContactStoreAdaptee()
}
}
var supportsContactEditing: Bool {
return self.adaptee.supportsContactEditing
}
var authorizationStatus: ContactStoreAuthorizationStatus {
return self.adaptee.authorizationStatus
}
func requestAccess(completionHandler: @escaping (Bool, Error?) -> Void) {
return self.adaptee.requestAccess(completionHandler: completionHandler)
}
func fetchContacts() -> Result<[Contact], Error> {
return self.adaptee.fetchContacts()
}
func startObservingChanges(changeHandler: @escaping () -> Void) {
self.adaptee.startObservingChanges(changeHandler: changeHandler)
}
}
@objc protocol SystemContactsFetcherDelegate: class {
func systemContactsFetcher(_ systemContactsFetcher: SystemContactsFetcher, updatedContacts contacts: [Contact])
}
@ -16,11 +309,12 @@ class SystemContactsFetcher: NSObject {
private let TAG = "[SystemContactsFetcher]"
var lastContactUpdateHash: Int?
var lastDelegateNotificationDate: Date?
let contactStoreAdapter: ContactStoreAdapter
public weak var delegate: SystemContactsFetcherDelegate?
public var authorizationStatus: CNAuthorizationStatus {
return CNContactStore.authorizationStatus(for: CNEntityType.contacts)
public var authorizationStatus: ContactStoreAuthorizationStatus {
return contactStoreAdapter.authorizationStatus
}
public var isAuthorized: Bool {
@ -33,15 +327,27 @@ class SystemContactsFetcher: NSObject {
return self.authorizationStatus == .authorized
}
private let contactStore = CNContactStore()
private var systemContactsHaveBeenRequestedAtLeastOnce = false
private let allowedContactKeys: [CNKeyDescriptor] = [
CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
CNContactThumbnailImageDataKey as CNKeyDescriptor, // TODO full image instead of thumbnail?
CNContactPhoneNumbersKey as CNKeyDescriptor,
CNContactEmailAddressesKey as CNKeyDescriptor,
CNContactViewController.descriptorForRequiredKeys()
]
private var hasSetupObservation = false
override init() {
self.contactStoreAdapter = ContactStoreAdapter()
}
var supportsContactEditing: Bool {
return self.contactStoreAdapter.supportsContactEditing
}
private func setupObservationIfNecessary() {
AssertIsOnMainThread()
guard !hasSetupObservation else {
return
}
hasSetupObservation = true
self.contactStoreAdapter.startObservingChanges {
self.updateContacts(completion: nil)
}
}
/**
* Ensures we've requested access for system contacts. This can be used in multiple places,
@ -59,11 +365,11 @@ class SystemContactsFetcher: NSObject {
return
}
systemContactsHaveBeenRequestedAtLeastOnce = true
self.startObservingContactChanges()
setupObservationIfNecessary()
switch authorizationStatus {
case .notDetermined:
contactStore.requestAccess(for: .contacts) { (granted, error) in
self.contactStoreAdapter.requestAccess { (granted, error) in
if let error = error {
Logger.error("\(self.TAG) error fetching contacts: \(error)")
DispatchQueue.main.async {
@ -109,24 +415,26 @@ class SystemContactsFetcher: NSObject {
AssertIsOnMainThread()
systemContactsHaveBeenRequestedAtLeastOnce = true
setupObservationIfNecessary()
DispatchQueue.global().async {
var systemContacts = [CNContact]()
do {
let contactFetchRequest = CNContactFetchRequest(keysToFetch: self.allowedContactKeys)
try self.contactStore.enumerateContacts(with: contactFetchRequest) { (contact, _) -> Void in
systemContacts.append(contact)
}
} catch let error as NSError {
Logger.error("\(self.TAG) Failed to fetch contacts with error:\(error)")
assertionFailure()
DispatchQueue.main.async {
completion?(error)
}
var fetchedContacts: [Contact]?
switch self.contactStoreAdapter.fetchContacts() {
case .success(let result):
fetchedContacts = result
case .error(let error):
completion?(error)
return
}
let contacts = systemContacts.map { Contact(systemContact: $0) }
guard let contacts = fetchedContacts else {
Logger.error("\(self.TAG) contacts was unexpectedly not set.")
assertionFailure()
completion?(nil)
}
let contactsHash = HashableArray(contacts).hashValue
DispatchQueue.main.async {
@ -172,19 +480,6 @@ class SystemContactsFetcher: NSObject {
}
}
}
private func startObservingContactChanges() {
NotificationCenter.default.addObserver(self,
selector: #selector(contactStoreDidChange),
name: .CNContactStoreDidChange,
object: nil)
}
@objc
private func contactStoreDidChange() {
updateContacts(completion: nil)
}
}
struct HashableArray<Element: Hashable>: Hashable {

Loading…
Cancel
Save