session-ios/Signal/src/contact/OWSContactsManager.m

717 lines
29 KiB
Mathematica
Raw Normal View History

2017-02-08 20:25:31 +01:00
//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
#import "OWSContactsManager.h"
2014-05-06 19:41:08 +02:00
#import "Environment.h"
2017-05-01 18:51:59 +02:00
#import "SignalAccount.h"
2014-05-06 19:41:08 +02:00
#import "Util.h"
#import <SignalServiceKit/ContactsUpdater.h>
2017-04-13 21:38:32 +02:00
#import <SignalServiceKit/OWSError.h>
2014-05-06 19:41:08 +02:00
#define ADDRESSBOOK_QUEUE dispatch_get_main_queue()
typedef BOOL (^ContactSearchBlock)(id, NSUInteger, BOOL *);
2014-05-06 19:41:08 +02:00
2017-05-01 18:51:59 +02:00
NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
@"OWSContactsManagerSignalAccountsDidChangeNotification";
@interface OWSContactsManager ()
2017-05-01 18:51:59 +02:00
@property (atomic, nullable) CNContactStore *contactStore;
@property (atomic) id addressBookReference;
@property (atomic) TOCFuture *futureAddressBook;
@property (nonatomic) BOOL isContactsUpdateInFlight;
2017-05-01 18:51:59 +02:00
// This reflects the contents of the device phone book and includes
// contacts that do not correspond to any signal account.
@property (atomic) NSArray<Contact *> *allContacts;
@property (atomic) NSDictionary<NSString *, Contact *> *allContactsMap;
@property (atomic) NSArray<SignalAccount *> *signalAccounts;
@property (atomic) NSDictionary<NSString *, SignalAccount *> *signalAccountMap;
2014-05-06 19:41:08 +02:00
@end
@implementation OWSContactsManager
2014-05-06 19:41:08 +02:00
- (id)init {
self = [super init];
if (!self) {
return self;
2014-05-06 19:41:08 +02:00
}
_avatarCache = [NSCache new];
2017-05-01 18:51:59 +02:00
_allContacts = @[];
_signalAccountMap = @{};
_signalAccounts = @[];
OWSSingletonAssert();
2014-05-06 19:41:08 +02:00
return self;
}
- (void)doAfterEnvironmentInitSetup {
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(9, 0) &&
!self.contactStore) {
OWSAssert(!self.contactStore);
2015-10-31 13:27:07 +01:00
self.contactStore = [[CNContactStore alloc] init];
[self.contactStore requestAccessForEntityType:CNEntityTypeContacts
completionHandler:^(BOOL granted, NSError *_Nullable error) {
if (!granted) {
// We're still using the old addressbook API.
// User warned if permission not granted in that setup.
}
}];
2015-10-31 13:27:07 +01:00
}
[self setupAddressBookIfNecessary];
2014-05-06 19:41:08 +02:00
}
- (void)verifyABPermission {
[self setupAddressBookIfNecessary];
}
2014-05-06 19:41:08 +02:00
#pragma mark - Address Book callbacks
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context);
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context) {
OWSContactsManager *contactsManager = (__bridge OWSContactsManager *)context;
2014-05-06 19:41:08 +02:00
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[contactsManager handleAddressBookChanged];
2014-05-06 19:41:08 +02:00
});
}
- (void)handleAddressBookChanged
{
[self pullLatestAddressBook];
}
2014-05-06 19:41:08 +02:00
#pragma mark - Setup
- (void)setupAddressBookIfNecessary
{
dispatch_async(ADDRESSBOOK_QUEUE, ^{
// De-bounce address book setup.
if (self.isContactsUpdateInFlight) {
return;
}
// We only need to set up our address book once;
// after that we only need to respond to onAddressBookChanged.
if (self.addressBookReference) {
return;
}
self.isContactsUpdateInFlight = YES;
TOCFuture *future = [OWSContactsManager asyncGetAddressBook];
[future thenDo:^(id addressBook) {
// Success.
OWSAssert(self.isContactsUpdateInFlight);
OWSAssert(!self.addressBookReference);
self.addressBookReference = addressBook;
self.isContactsUpdateInFlight = NO;
ABAddressBookRef cfAddressBook = (__bridge ABAddressBookRef)addressBook;
ABAddressBookRegisterExternalChangeCallback(cfAddressBook, onAddressBookChanged, (__bridge void *)self);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self handleAddressBookChanged];
});
}];
[future catchDo:^(id failure) {
// Failure.
OWSAssert(self.isContactsUpdateInFlight);
OWSAssert(!self.addressBookReference);
self.isContactsUpdateInFlight = NO;
}];
2014-05-06 19:41:08 +02:00
});
}
- (void)intersectContacts
2017-04-13 21:38:32 +02:00
{
[self intersectContactsWithRetryDelay:1];
2017-04-13 21:38:32 +02:00
}
- (void)intersectContactsWithRetryDelay:(double)retryDelaySeconds
{
void (^success)() = ^{
DDLogInfo(@"%@ Successfully intersected contacts.", self.tag);
[self updateSignalAccounts];
};
void (^failure)(NSError *error) = ^(NSError *error) {
2017-04-13 21:38:32 +02:00
if ([error.domain isEqualToString:OWSSignalServiceKitErrorDomain]
&& error.code == OWSErrorCodeContactsUpdaterRateLimit) {
DDLogError(@"Contact intersection hit rate limit with error: %@", error);
return;
}
DDLogWarn(@"%@ Failed to intersect contacts with error: %@. Rescheduling", self.tag, error);
// Retry with exponential backoff.
//
// TODO: Abort if another contact intersection succeeds in the meantime.
dispatch_after(
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryDelaySeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self intersectContactsWithRetryDelay:retryDelaySeconds * 2];
});
};
[[ContactsUpdater sharedUpdater] updateSignalContactIntersectionWithABContacts:self.allContacts
success:success
failure:failure];
}
- (void)pullLatestAddressBook {
dispatch_async(ADDRESSBOOK_QUEUE, ^{
CFErrorRef creationError = nil;
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
checkOperationDescribe(nil == creationError, [((__bridge NSError *)creationError)localizedDescription]);
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
if (!granted) {
[OWSContactsManager blockingContactDialog];
}
});
NSArray<Contact *> *contacts = [self getContactsFromAddressBook:addressBookRef];
[self updateWithContacts:contacts];
});
2014-05-06 19:41:08 +02:00
}
- (void)updateWithContacts:(NSArray<Contact *> *)contacts
2017-05-01 18:51:59 +02:00
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSMutableDictionary<NSString *, Contact *> *allContactsMap = [NSMutableDictionary new];
for (Contact *contact in contacts) {
for (PhoneNumber *phoneNumber in contact.parsedPhoneNumbers) {
NSString *phoneNumberE164 = phoneNumber.toE164;
if (phoneNumberE164.length > 0) {
allContactsMap[phoneNumberE164] = contact;
}
2017-05-01 18:51:59 +02:00
}
}
dispatch_async(dispatch_get_main_queue(), ^{
self.allContacts = contacts;
self.allContactsMap = [allContactsMap copy];
2017-05-02 18:30:53 +02:00
[self.avatarCache removeAllObjects];
[self intersectContacts];
[self updateSignalAccounts];
});
});
}
- (void)updateSignalAccounts
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSMutableDictionary<NSString *, SignalAccount *> *signalAccountMap = [NSMutableDictionary new];
NSMutableArray<SignalAccount *> *signalAccounts = [NSMutableArray new];
NSArray<Contact *> *contacts = self.allContacts;
// We use a transaction only to load the SignalRecipients for each contact,
// in order to avoid database deadlock.
NSMutableDictionary<NSString *, NSArray<SignalRecipient *> *> *contactIdToSignalRecipientsMap =
[NSMutableDictionary new];
[[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
for (Contact *contact in contacts) {
NSArray<SignalRecipient *> *signalRecipients = [contact signalRecipientsWithTransaction:transaction];
contactIdToSignalRecipientsMap[contact.uniqueId] = signalRecipients;
2017-05-01 18:51:59 +02:00
}
}];
2017-05-01 18:51:59 +02:00
for (Contact *contact in contacts) {
NSArray<SignalRecipient *> *signalRecipients = contactIdToSignalRecipientsMap[contact.uniqueId];
for (SignalRecipient *signalRecipient in [signalRecipients sortedArrayUsingSelector:@selector(compare:)]) {
SignalAccount *signalAccount = [[SignalAccount alloc] initWithSignalRecipient:signalRecipient];
signalAccount.contact = contact;
if (signalRecipients.count > 1) {
signalAccount.hasMultipleAccountContact = YES;
signalAccount.multipleAccountLabelText =
[[self class] accountLabelForContact:contact recipientId:signalRecipient.recipientId];
}
if (signalAccountMap[signalAccount.recipientId]) {
DDLogInfo(@"Ignoring duplicate contact: %@, %@", signalAccount.recipientId, contact.fullName);
continue;
}
signalAccountMap[signalAccount.recipientId] = signalAccount;
[signalAccounts addObject:signalAccount];
}
}
dispatch_async(dispatch_get_main_queue(), ^{
self.signalAccountMap = [signalAccountMap copy];
self.signalAccounts = [signalAccounts copy];
2017-05-01 18:51:59 +02:00
[[NSNotificationCenter defaultCenter]
postNotificationName:OWSContactsManagerSignalAccountsDidChangeNotification
object:nil];
});
2017-05-01 18:51:59 +02:00
});
}
+ (NSString *)accountLabelForContact:(Contact *)contact recipientId:(NSString *)recipientId
{
OWSAssert(contact);
OWSAssert(recipientId.length > 0);
OWSAssert([contact.textSecureIdentifiers containsObject:recipientId]);
if (contact.textSecureIdentifiers.count <= 1) {
return nil;
}
// 1. Find the phone number type of this account.
OWSPhoneNumberType phoneNumberType = [contact phoneNumberTypeForPhoneNumber:recipientId];
NSString *phoneNumberLabel;
switch (phoneNumberType) {
case OWSPhoneNumberTypeMobile:
phoneNumberLabel = NSLocalizedString(@"PHONE_NUMBER_TYPE_MOBILE", @"Label for 'Mobile' phone numbers.");
break;
case OWSPhoneNumberTypeIPhone:
phoneNumberLabel = NSLocalizedString(@"PHONE_NUMBER_TYPE_IPHONE", @"Label for 'IPhone' phone numbers.");
break;
case OWSPhoneNumberTypeMain:
phoneNumberLabel = NSLocalizedString(@"PHONE_NUMBER_TYPE_MAIN", @"Label for 'Main' phone numbers.");
break;
case OWSPhoneNumberTypeHomeFAX:
phoneNumberLabel = NSLocalizedString(@"PHONE_NUMBER_TYPE_HOME_FAX", @"Label for 'HomeFAX' phone numbers.");
break;
case OWSPhoneNumberTypeWorkFAX:
phoneNumberLabel = NSLocalizedString(@"PHONE_NUMBER_TYPE_WORK_FAX", @"Label for 'Work FAX' phone numbers.");
break;
case OWSPhoneNumberTypeOtherFAX:
phoneNumberLabel
= NSLocalizedString(@"PHONE_NUMBER_TYPE_OTHER_FAX", @"Label for 'Other FAX' phone numbers.");
break;
case OWSPhoneNumberTypePager:
phoneNumberLabel = NSLocalizedString(@"PHONE_NUMBER_TYPE_PAGER", @"Label for 'Pager' phone numbers.");
break;
case OWSPhoneNumberTypeUnknown:
2017-05-02 18:30:53 +02:00
phoneNumberLabel = NSLocalizedString(@"PHONE_NUMBER_TYPE_UNKNOWN",
@"Label used when we don't what kind of phone number it is (e.g. mobile/work/home).");
2017-05-01 18:51:59 +02:00
break;
}
// 2. Find all phone numbers for this contact of the same type.
NSMutableArray *phoneNumbersOfTheSameType = [NSMutableArray new];
for (NSString *textSecureIdentifier in contact.textSecureIdentifiers) {
if (phoneNumberType == [contact phoneNumberTypeForPhoneNumber:textSecureIdentifier]) {
[phoneNumbersOfTheSameType addObject:textSecureIdentifier];
}
}
OWSAssert([phoneNumbersOfTheSameType containsObject:recipientId]);
if (phoneNumbersOfTheSameType.count > 0) {
NSUInteger index =
[[phoneNumbersOfTheSameType sortedArrayUsingSelector:@selector(compare:)] indexOfObject:recipientId];
phoneNumberLabel =
[NSString stringWithFormat:NSLocalizedString(@"PHONE_NUMBER_TYPE_AND_INDEX_FORMAT",
@"Format for phone number label with an index. Embeds {{Phone number label "
@"(e.g. 'home')}} and {{index, e.g. 2}}."),
phoneNumberLabel,
(int)index];
}
return phoneNumberLabel;
2014-05-06 19:41:08 +02:00
}
+ (void)blockingContactDialog {
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
switch (ABAddressBookGetAuthorizationStatus()) {
case kABAuthorizationStatusRestricted: {
UIAlertController *controller =
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"AB_PERMISSION_MISSING_TITLE", nil)
message:NSLocalizedString(@"ADDRESSBOOK_RESTRICTED_ALERT_BODY", nil)
preferredStyle:UIAlertControllerStyleAlert];
[controller
addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"ADDRESSBOOK_RESTRICTED_ALERT_BUTTON", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[DDLog flushLog];
exit(0);
}]];
[[UIApplication sharedApplication]
.keyWindow.rootViewController presentViewController:controller
animated:YES
completion:nil];
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
break;
}
case kABAuthorizationStatusDenied: {
UIAlertController *controller =
[UIAlertController alertControllerWithTitle:NSLocalizedString(@"AB_PERMISSION_MISSING_TITLE", nil)
message:NSLocalizedString(@"AB_PERMISSION_MISSING_BODY", nil)
preferredStyle:UIAlertControllerStyleAlert];
[controller addAction:[UIAlertAction
actionWithTitle:NSLocalizedString(@"AB_PERMISSION_MISSING_ACTION", nil)
style:UIAlertActionStyleDefault
handler:^(UIAlertAction *action) {
[[UIApplication sharedApplication]
openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
}]];
[[[UIApplication sharedApplication] keyWindow]
.rootViewController presentViewController:controller
animated:YES
completion:nil];
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
break;
}
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
case kABAuthorizationStatusNotDetermined: {
DDLogInfo(@"AddressBook access not granted but status undetermined.");
[[Environment getCurrent].contactsManager pullLatestAddressBook];
break;
}
case kABAuthorizationStatusAuthorized: {
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
DDLogInfo(@"AddressBook access not granted but status authorized.");
break;
}
iOS 9 Support - Fixing size classes rendering bugs. - Supporting native iOS San Francisco font. - Quick Reply - Settings now slide to the left as suggested in original designed opposed to modal. - Simplification of restraints on many screens. - Full-API compatiblity with iOS 9 and iOS 8 legacy support. - Customized AddressBook Permission prompt when restrictions are enabled. If user installed Signal previously and already approved access to Contacts, don't bugg him again. - Fixes crash in migration for users who installed Signal <2.1.3 but hadn't signed up yet. - Xcode 7 / iOS 9 Travis Support - Bitcode Support is disabled until it is better understood how exactly optimizations are performed. In a first time, we will split out the crypto code into a separate binary to make it easier to optimize the non-sensitive code. Blog post with more details coming. - Partial ATS support. We are running our own Certificate Authority at Open Whisper Systems. Signal is doing certificate pinning to verify that certificates were signed by our own CA. Unfortunately Apple's App Transport Security requires to hand over chain verification to their framework with no control over the trust store. We have filed a radar to get ATS features with pinned certificates. In the meanwhile, ATS is disabled on our domain. We also followed Amazon's recommendations for our S3 domain we use to upload/download attachments. (#891) - Implement a unified `AFSecurityOWSPolicy` pinning strategy accross libraries (AFNetworking RedPhone/TextSecure & SocketRocket).
2015-09-01 19:22:08 +02:00
default:
break;
}
}
2014-05-06 19:41:08 +02:00
#pragma mark - Address Book utils
+ (TOCFuture *)asyncGetAddressBook {
CFErrorRef creationError = nil;
2014-05-06 19:41:08 +02:00
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
assert((addressBookRef == nil) == (creationError != nil));
if (creationError != nil) {
[self blockingContactDialog];
return [TOCFuture futureWithFailure:(__bridge_transfer id)creationError];
2014-05-06 19:41:08 +02:00
}
TOCFutureSource *futureAddressBookSource = [TOCFutureSource new];
2014-05-06 19:41:08 +02:00
id addressBook = (__bridge_transfer id)addressBookRef;
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef requestAccessError) {
if (granted && ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
dispatch_async(ADDRESSBOOK_QUEUE, ^{
[futureAddressBookSource trySetResult:addressBook];
});
} else {
[self blockingContactDialog];
[futureAddressBookSource trySetFailure:(__bridge id)requestAccessError];
}
2014-05-06 19:41:08 +02:00
});
return futureAddressBookSource.future;
2014-05-06 19:41:08 +02:00
}
- (NSArray<Contact *> *)getContactsFromAddressBook:(ABAddressBookRef _Nonnull)addressBook
{
2014-07-31 15:50:24 +02:00
CFArrayRef allPeople = ABAddressBookCopyArrayOfAllPeople(addressBook);
CFMutableArrayRef allPeopleMutable =
CFArrayCreateMutableCopy(kCFAllocatorDefault, CFArrayGetCount(allPeople), allPeople);
CFArraySortValues(allPeopleMutable,
CFRangeMake(0, CFArrayGetCount(allPeopleMutable)),
2014-07-31 15:50:24 +02:00
(CFComparatorFunction)ABPersonComparePeopleByName,
(void *)(unsigned long)ABPersonGetSortOrdering());
2014-07-31 15:50:24 +02:00
NSArray *sortedPeople = (__bridge_transfer NSArray *)allPeopleMutable;
// This predicate returns all contacts from the addressbook having at least one phone number
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(id record, NSDictionary *bindings) {
ABMultiValueRef phoneNumbers = ABRecordCopyValue((__bridge ABRecordRef)record, kABPersonPhoneProperty);
BOOL result = NO;
for (CFIndex i = 0; i < ABMultiValueGetCount(phoneNumbers); i++) {
NSString *phoneNumber = (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumbers, i);
if (phoneNumber.length > 0) {
result = YES;
break;
}
}
CFRelease(phoneNumbers);
return result;
}];
2014-07-31 15:50:24 +02:00
CFRelease(allPeople);
NSArray *filteredContacts = [sortedPeople filteredArrayUsingPredicate:predicate];
return [filteredContacts map:^id(id item) {
2017-05-01 19:37:20 +02:00
Contact *contact = [self contactForRecord:(__bridge ABRecordRef)item];
return contact;
2014-05-06 19:41:08 +02:00
}];
}
#pragma mark - Contact/Phone Number util
- (Contact *)contactForRecord:(ABRecordRef)record {
ABRecordID recordID = ABRecordGetRecordID(record);
NSString *firstName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonFirstNameProperty);
NSString *lastName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonLastNameProperty);
NSDictionary<NSString *, NSNumber *> *phoneNumberTypeMap = [self phoneNumbersForRecord:record];
NSArray *phoneNumbers = [phoneNumberTypeMap.allKeys sortedArrayUsingSelector:@selector(compare:)];
2014-05-06 19:41:08 +02:00
if (!firstName && !lastName) {
NSString *companyName = (__bridge_transfer NSString *)ABRecordCopyValue(record, kABPersonOrganizationProperty);
2014-05-06 19:41:08 +02:00
if (companyName) {
firstName = companyName;
2014-08-14 03:13:24 +02:00
} else if (phoneNumbers.count) {
firstName = phoneNumbers.firstObject;
2014-05-06 19:41:08 +02:00
}
}
NSData *imageData
= (__bridge_transfer NSData *)ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail);
UIImage *img = [UIImage imageWithData:imageData];
return [[Contact alloc] initWithContactWithFirstName:firstName
andLastName:lastName
andUserTextPhoneNumbers:phoneNumbers
phoneNumberTypeMap:phoneNumberTypeMap
andImage:img
andContactID:recordID];
}
2014-05-06 19:41:08 +02:00
- (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2 {
return [phoneNumber1.toE164 isEqualToString:phoneNumber2.toE164];
2014-05-06 19:41:08 +02:00
}
- (NSDictionary<NSString *, NSNumber *> *)phoneNumbersForRecord:(ABRecordRef)record
{
ABMultiValueRef phoneNumberRefs = NULL;
2014-05-06 19:41:08 +02:00
@try {
phoneNumberRefs = ABRecordCopyValue(record, kABPersonPhoneProperty);
CFIndex phoneNumberCount = ABMultiValueGetCount(phoneNumberRefs);
NSMutableDictionary<NSString *, NSNumber *> *result = [NSMutableDictionary new];
for (int i = 0; i < phoneNumberCount; i++) {
NSString *phoneNumberLabel = (__bridge_transfer NSString *)ABMultiValueCopyLabelAtIndex(phoneNumberRefs, i);
NSString *phoneNumber = (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phoneNumberRefs, i);
if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneMobileLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeMobile);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneIPhoneLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeIPhone);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneMainLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeMain);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneHomeFAXLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeHomeFAX);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneWorkFAXLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeWorkFAX);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhoneOtherFAXLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypeOtherFAX);
} else if ([phoneNumberLabel isEqualToString:(NSString *)kABPersonPhonePagerLabel]) {
result[phoneNumber] = @(OWSPhoneNumberTypePager);
} else {
result[phoneNumber] = @(OWSPhoneNumberTypeUnknown);
}
2014-05-06 19:41:08 +02:00
}
return [result copy];
2014-05-06 19:41:08 +02:00
} @finally {
if (phoneNumberRefs) {
CFRelease(phoneNumberRefs);
2014-05-06 19:41:08 +02:00
}
}
}
#pragma mark - Whisper User Management
- (NSArray *)getSignalUsersFromContactsArray:(NSArray *)contacts {
NSMutableDictionary *signalContacts = [NSMutableDictionary new];
for (Contact *contact in contacts) {
if ([contact isSignalContact]) {
signalContacts[contact.textSecureIdentifiers.firstObject] = contact;
}
}
return [signalContacts.allValues sortedArrayUsingComparator:[[self class] contactComparator]];
}
+ (NSComparator)contactComparator
{
BOOL firstNameOrdering = ABPersonGetSortOrdering() == kABPersonCompositeNameFormatFirstNameFirst ? YES : NO;
return [Contact comparatorSortingNamesByFirstThenLast:firstNameOrdering];
2014-05-06 19:41:08 +02:00
}
- (NSArray<Contact *> * _Nonnull)signalContacts {
return [self getSignalUsersFromContactsArray:[self allContacts]];
}
- (NSString *)unknownContactName
{
return NSLocalizedString(@"UNKNOWN_CONTACT_NAME",
@"Displayed if for some reason we can't determine a contacts phone number *or* name");
}
- (NSString * _Nonnull)displayNameForPhoneIdentifier:(NSString * _Nullable)identifier {
if (!identifier) {
return self.unknownContactName;
}
2017-05-01 18:51:59 +02:00
// TODO: There's some overlap here with displayNameForSignalAccount.
SignalAccount *signalAccount = [self signalAccountForRecipientId:identifier];
NSString *displayName = (signalAccount.contact.fullName.length > 0) ? signalAccount.contact.fullName : identifier;
return displayName;
}
2017-04-04 16:19:47 +02:00
- (NSString *_Nonnull)displayNameForContact:(Contact *)contact
{
OWSAssert(contact);
NSString *displayName = (contact.fullName.length > 0) ? contact.fullName : self.unknownContactName;
return displayName;
}
2017-05-01 18:51:59 +02:00
- (NSString *_Nonnull)displayNameForSignalAccount:(SignalAccount *)signalAccount
{
2017-05-01 18:51:59 +02:00
OWSAssert(signalAccount);
2017-05-01 18:51:59 +02:00
NSString *baseName = (signalAccount.contact ? [self displayNameForContact:signalAccount.contact]
: [self displayNameForPhoneIdentifier:signalAccount.recipientId]);
2017-05-02 18:30:53 +02:00
OWSAssert(signalAccount.hasMultipleAccountContact == (signalAccount.multipleAccountLabelText != nil));
if (signalAccount.multipleAccountLabelText) {
return [NSString stringWithFormat:@"%@ (%@)", baseName, signalAccount.multipleAccountLabelText];
} else {
return baseName;
}
}
2017-05-01 18:51:59 +02:00
- (NSAttributedString *_Nonnull)formattedDisplayNameForSignalAccount:(SignalAccount *)signalAccount
font:(UIFont *_Nonnull)font
{
2017-05-01 18:51:59 +02:00
OWSAssert(signalAccount);
OWSAssert(font);
2017-05-01 18:51:59 +02:00
NSAttributedString *baseName = [self formattedFullNameForContact:signalAccount.contact font:font];
2017-05-02 18:30:53 +02:00
OWSAssert(signalAccount.hasMultipleAccountContact == (signalAccount.multipleAccountLabelText != nil));
if (signalAccount.multipleAccountLabelText) {
NSMutableAttributedString *result = [NSMutableAttributedString new];
[result appendAttributedString:baseName];
[result appendAttributedString:[[NSAttributedString alloc] initWithString:@" ("
attributes:@{
NSFontAttributeName : font,
}]];
2017-05-02 18:30:53 +02:00
[result
appendAttributedString:[[NSAttributedString alloc] initWithString:signalAccount.multipleAccountLabelText]];
[result appendAttributedString:[[NSAttributedString alloc] initWithString:@")"
attributes:@{
NSFontAttributeName : font,
}]];
return result;
} else {
return baseName;
}
}
- (NSAttributedString *_Nonnull)formattedFullNameForContact:(Contact *)contact font:(UIFont *_Nonnull)font
{
UIFont *boldFont = [UIFont ows_mediumFontWithSize:font.pointSize];
NSDictionary<NSString *, id> *boldFontAttributes =
@{ NSFontAttributeName : boldFont, NSForegroundColorAttributeName : [UIColor blackColor] };
NSDictionary<NSString *, id> *normalFontAttributes =
@{ NSFontAttributeName : font, NSForegroundColorAttributeName : [UIColor ows_darkGrayColor] };
NSAttributedString *_Nullable firstName, *_Nullable lastName;
if (ABPersonGetSortOrdering() == kABPersonSortByFirstName) {
if (contact.firstName) {
firstName = [[NSAttributedString alloc] initWithString:contact.firstName attributes:boldFontAttributes];
}
if (contact.lastName) {
lastName = [[NSAttributedString alloc] initWithString:contact.lastName attributes:normalFontAttributes];
}
} else {
if (contact.firstName) {
firstName = [[NSAttributedString alloc] initWithString:contact.firstName attributes:normalFontAttributes];
}
if (contact.lastName) {
lastName = [[NSAttributedString alloc] initWithString:contact.lastName attributes:boldFontAttributes];
}
}
NSAttributedString *_Nullable leftName, *_Nullable rightName;
if (ABPersonGetCompositeNameFormat() == kABPersonCompositeNameFormatFirstNameFirst) {
leftName = firstName;
rightName = lastName;
} else {
leftName = lastName;
rightName = firstName;
}
NSMutableAttributedString *fullNameString = [NSMutableAttributedString new];
if (leftName) {
[fullNameString appendAttributedString:leftName];
}
if (leftName && rightName) {
[fullNameString appendAttributedString:[[NSAttributedString alloc] initWithString:@" "]];
}
if (rightName) {
[fullNameString appendAttributedString:rightName];
}
return fullNameString;
}
2017-04-18 22:08:01 +02:00
- (NSAttributedString *)formattedFullNameForRecipientId:(NSString *)recipientId font:(UIFont *)font
{
NSDictionary<NSString *, id> *normalFontAttributes =
@{ NSFontAttributeName : font, NSForegroundColorAttributeName : [UIColor ows_darkGrayColor] };
return [[NSAttributedString alloc]
initWithString:[PhoneNumber bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:recipientId]
attributes:normalFontAttributes];
}
2017-05-02 18:30:53 +02:00
- (nullable SignalAccount *)signalAccountForRecipientId:(NSString *)recipientId
2017-05-01 18:51:59 +02:00
{
OWSAssert(recipientId.length > 0);
return self.signalAccountMap[recipientId];
2014-11-25 19:06:09 +01:00
}
- (Contact *)getOrBuildContactForPhoneIdentifier:(NSString *)identifier
{
2017-05-01 18:51:59 +02:00
Contact *savedContact = self.allContactsMap[identifier];
if (savedContact) {
return savedContact;
} else {
return [[Contact alloc] initWithContactWithFirstName:self.unknownContactName
andLastName:nil
andUserTextPhoneNumbers:@[ identifier ]
phoneNumberTypeMap:nil
andImage:nil
andContactID:0];
}
}
- (UIImage * _Nullable)imageForPhoneIdentifier:(NSString * _Nullable)identifier {
2017-05-01 18:51:59 +02:00
Contact *contact = self.allContactsMap[identifier];
return contact.image;
}
- (BOOL)hasAddressBook
{
2017-04-19 16:28:24 +02:00
return (BOOL)self.addressBookReference;
}
#pragma mark - Logging
+ (NSString *)tag
{
return [NSString stringWithFormat:@"[%@]", self.class];
}
- (NSString *)tag
{
return self.class.tag;
}
2014-05-06 19:41:08 +02:00
@end