2017-02-08 20:25:31 +01:00
|
|
|
//
|
|
|
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
|
|
|
//
|
|
|
|
|
2016-06-27 23:32:35 +02:00
|
|
|
#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"
|
2017-04-28 18:18:42 +02:00
|
|
|
#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()
|
|
|
|
|
2015-12-22 12:45:09 +01:00
|
|
|
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";
|
2017-02-08 17:59:02 +01:00
|
|
|
|
2016-06-27 23:32:35 +02:00
|
|
|
@interface OWSContactsManager ()
|
2016-06-16 22:54:33 +02:00
|
|
|
|
2017-05-01 18:51:59 +02:00
|
|
|
@property (atomic, nullable) CNContactStore *contactStore;
|
2017-04-13 15:43:37 +02:00
|
|
|
@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
|
|
|
|
|
2016-06-27 23:32:35 +02:00
|
|
|
@implementation OWSContactsManager
|
2014-05-06 19:41:08 +02:00
|
|
|
|
|
|
|
- (id)init {
|
|
|
|
self = [super init];
|
2016-11-18 23:11:56 +01:00
|
|
|
if (!self) {
|
|
|
|
return self;
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
2016-11-18 23:11:56 +01:00
|
|
|
|
|
|
|
_avatarCache = [NSCache new];
|
2017-05-01 18:51:59 +02:00
|
|
|
_allContacts = @[];
|
|
|
|
_signalAccountMap = @{};
|
|
|
|
_signalAccounts = @[];
|
2016-11-18 23:11:56 +01:00
|
|
|
|
2017-04-01 00:45:51 +02:00
|
|
|
OWSSingletonAssert();
|
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
return self;
|
|
|
|
}
|
2015-08-24 01:47:25 +02:00
|
|
|
|
2015-12-22 12:45:09 +01:00
|
|
|
- (void)doAfterEnvironmentInitSetup {
|
2017-04-14 21:58:39 +02:00
|
|
|
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(9, 0) &&
|
|
|
|
!self.contactStore) {
|
2017-04-13 15:43:37 +02:00
|
|
|
OWSAssert(!self.contactStore);
|
2015-10-31 13:27:07 +01:00
|
|
|
self.contactStore = [[CNContactStore alloc] init];
|
2015-12-22 12:45:09 +01:00
|
|
|
[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
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2017-04-13 15:43:37 +02:00
|
|
|
[self setupAddressBookIfNecessary];
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
|
2015-08-24 01:47:25 +02:00
|
|
|
- (void)verifyABPermission {
|
2017-04-13 15:43:37 +02:00
|
|
|
[self setupAddressBookIfNecessary];
|
2015-08-24 01:47:25 +02:00
|
|
|
}
|
|
|
|
|
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) {
|
2016-06-27 23:32:35 +02:00
|
|
|
OWSContactsManager *contactsManager = (__bridge OWSContactsManager *)context;
|
2014-05-06 19:41:08 +02:00
|
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
2016-11-18 23:11:56 +01:00
|
|
|
[contactsManager handleAddressBookChanged];
|
2014-05-06 19:41:08 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-11-18 23:11:56 +01:00
|
|
|
- (void)handleAddressBookChanged
|
|
|
|
{
|
2017-05-01 20:10:53 +02:00
|
|
|
[self pullLatestAddressBook];
|
2016-11-18 23:11:56 +01:00
|
|
|
}
|
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
#pragma mark - Setup
|
|
|
|
|
2017-04-13 15:43:37 +02:00
|
|
|
- (void)setupAddressBookIfNecessary
|
|
|
|
{
|
2017-04-13 15:48:15 +02:00
|
|
|
dispatch_async(ADDRESSBOOK_QUEUE, ^{
|
|
|
|
// De-bounce address book setup.
|
|
|
|
if (self.isContactsUpdateInFlight) {
|
|
|
|
return;
|
|
|
|
}
|
2017-04-13 15:43:37 +02:00
|
|
|
// 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.
|
2017-04-13 15:48:15 +02:00
|
|
|
OWSAssert(self.isContactsUpdateInFlight);
|
|
|
|
OWSAssert(!self.addressBookReference);
|
|
|
|
|
|
|
|
self.addressBookReference = addressBook;
|
|
|
|
self.isContactsUpdateInFlight = NO;
|
2017-04-13 15:43:37 +02:00
|
|
|
|
|
|
|
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.
|
2017-04-13 15:48:15 +02:00
|
|
|
OWSAssert(self.isContactsUpdateInFlight);
|
|
|
|
OWSAssert(!self.addressBookReference);
|
2017-04-13 15:43:37 +02:00
|
|
|
|
2017-04-13 15:48:15 +02:00
|
|
|
self.isContactsUpdateInFlight = NO;
|
2017-04-13 15:43:37 +02:00
|
|
|
}];
|
2014-05-06 19:41:08 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-04-13 16:11:01 +02:00
|
|
|
- (void)intersectContacts
|
2017-04-13 21:38:32 +02:00
|
|
|
{
|
2017-04-27 22:18:11 +02:00
|
|
|
[self intersectContactsWithRetryDelay:1];
|
2017-04-13 21:38:32 +02:00
|
|
|
}
|
|
|
|
|
2017-04-27 22:18:11 +02:00
|
|
|
- (void)intersectContactsWithRetryDelay:(double)retryDelaySeconds
|
2017-04-13 15:43:37 +02:00
|
|
|
{
|
|
|
|
void (^success)() = ^{
|
|
|
|
DDLogInfo(@"%@ Successfully intersected contacts.", self.tag);
|
2017-05-01 20:10:53 +02:00
|
|
|
[self updateSignalAccounts];
|
2017-04-13 15:43:37 +02:00
|
|
|
};
|
|
|
|
void (^failure)(NSError *error) = ^(NSError *error) {
|
2017-04-13 21:38:32 +02:00
|
|
|
if ([error.domain isEqualToString:OWSSignalServiceKitErrorDomain]
|
|
|
|
&& error.code == OWSErrorCodeContactsUpdaterRateLimit) {
|
2017-04-13 16:11:01 +02:00
|
|
|
DDLogError(@"Contact intersection hit rate limit with error: %@", error);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-04-13 15:43:37 +02:00
|
|
|
DDLogWarn(@"%@ Failed to intersect contacts with error: %@. Rescheduling", self.tag, error);
|
|
|
|
|
|
|
|
// Retry with exponential backoff.
|
|
|
|
//
|
2017-04-13 16:11:01 +02:00
|
|
|
// TODO: Abort if another contact intersection succeeds in the meantime.
|
2017-04-27 22:18:11 +02:00
|
|
|
dispatch_after(
|
|
|
|
dispatch_time(DISPATCH_TIME_NOW, (int64_t)(retryDelaySeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
|
|
[self intersectContactsWithRetryDelay:retryDelaySeconds * 2];
|
|
|
|
});
|
2017-04-13 15:43:37 +02:00
|
|
|
};
|
2015-12-22 12:45:09 +01:00
|
|
|
[[ContactsUpdater sharedUpdater] updateSignalContactIntersectionWithABContacts:self.allContacts
|
2017-04-13 15:43:37 +02:00
|
|
|
success:success
|
|
|
|
failure:failure];
|
2017-02-08 17:59:02 +01:00
|
|
|
}
|
|
|
|
|
2015-12-22 12:45:09 +01:00
|
|
|
- (void)pullLatestAddressBook {
|
2017-05-01 20:10:53 +02:00
|
|
|
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];
|
2015-03-22 12:38:15 +01:00
|
|
|
});
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
|
2017-05-01 20:10:53 +02:00
|
|
|
- (void)updateWithContacts:(NSArray<Contact *> *)contacts
|
2017-05-01 18:51:59 +02:00
|
|
|
{
|
2017-05-01 20:10:53 +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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-01 20:10:53 +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];
|
|
|
|
|
2017-05-01 20:10:53 +02:00
|
|
|
[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;
|
2017-05-02 19:59:35 +02:00
|
|
|
|
|
|
|
// 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];
|
2017-05-01 20:10:53 +02:00
|
|
|
[[TSStorageManager sharedManager].dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
for (Contact *contact in contacts) {
|
|
|
|
NSArray<SignalRecipient *> *signalRecipients = [contact signalRecipientsWithTransaction:transaction];
|
2017-05-02 19:59:35 +02:00
|
|
|
contactIdToSignalRecipientsMap[contact.uniqueId] = signalRecipients;
|
2017-05-01 18:51:59 +02:00
|
|
|
}
|
2017-05-01 20:10:53 +02:00
|
|
|
}];
|
2017-05-01 18:51:59 +02:00
|
|
|
|
2017-05-02 19:59:35 +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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-01 20:10:53 +02:00
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
self.signalAccountMap = [signalAccountMap copy];
|
|
|
|
self.signalAccounts = [signalAccounts copy];
|
2017-05-01 18:51:59 +02:00
|
|
|
|
2017-05-01 20:10:53 +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
|
|
|
}
|
|
|
|
|
2015-12-22 12:45:09 +01:00
|
|
|
+ (void)blockingContactDialog {
|
2015-09-01 19:22:08 +02:00
|
|
|
switch (ABAddressBookGetAuthorizationStatus()) {
|
2015-12-22 12:45:09 +01:00
|
|
|
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) {
|
2017-02-14 17:05:59 +01:00
|
|
|
[DDLog flushLog];
|
|
|
|
exit(0);
|
2015-12-22 12:45:09 +01:00
|
|
|
}]];
|
|
|
|
|
|
|
|
[[UIApplication sharedApplication]
|
|
|
|
.keyWindow.rootViewController presentViewController:controller
|
|
|
|
animated:YES
|
|
|
|
completion:nil];
|
|
|
|
|
2015-09-01 19:22:08 +02:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case kABAuthorizationStatusDenied: {
|
2015-12-22 12:45:09 +01:00
|
|
|
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];
|
2015-09-01 19:22:08 +02:00
|
|
|
break;
|
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2015-09-01 19:22:08 +02:00
|
|
|
case kABAuthorizationStatusNotDetermined: {
|
|
|
|
DDLogInfo(@"AddressBook access not granted but status undetermined.");
|
|
|
|
[[Environment getCurrent].contactsManager pullLatestAddressBook];
|
|
|
|
break;
|
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
|
|
case kABAuthorizationStatusAuthorized: {
|
2015-09-01 19:22:08 +02:00
|
|
|
DDLogInfo(@"AddressBook access not granted but status authorized.");
|
|
|
|
break;
|
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2015-09-01 19:22:08 +02:00
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
2015-08-24 01:47:25 +02:00
|
|
|
}
|
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
#pragma mark - Address Book utils
|
|
|
|
|
2015-12-22 12:45:09 +01:00
|
|
|
+ (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) {
|
2015-08-24 01:47:25 +02:00
|
|
|
[self blockingContactDialog];
|
2014-08-20 14:08:32 +02:00
|
|
|
return [TOCFuture futureWithFailure:(__bridge_transfer id)creationError];
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2014-08-20 14:08:32 +02:00
|
|
|
TOCFutureSource *futureAddressBookSource = [TOCFutureSource new];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
id addressBook = (__bridge_transfer id)addressBookRef;
|
|
|
|
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef requestAccessError) {
|
2015-12-22 12:45:09 +01:00
|
|
|
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
|
|
|
});
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2014-08-20 14:08:32 +02:00
|
|
|
return futureAddressBookSource.future;
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
|
2017-05-01 20:10:53 +02:00
|
|
|
- (NSArray<Contact *> *)getContactsFromAddressBook:(ABAddressBookRef _Nonnull)addressBook
|
|
|
|
{
|
2014-07-31 15:50:24 +02:00
|
|
|
CFArrayRef allPeople = ABAddressBookCopyArrayOfAllPeople(addressBook);
|
2017-05-01 20:10:53 +02:00
|
|
|
|
2015-12-22 12:45:09 +01:00
|
|
|
CFMutableArrayRef allPeopleMutable =
|
|
|
|
CFArrayCreateMutableCopy(kCFAllocatorDefault, CFArrayGetCount(allPeople), allPeople);
|
|
|
|
|
|
|
|
CFArraySortValues(allPeopleMutable,
|
|
|
|
CFRangeMake(0, CFArrayGetCount(allPeopleMutable)),
|
2014-07-31 15:50:24 +02:00
|
|
|
(CFComparatorFunction)ABPersonComparePeopleByName,
|
2015-12-22 12:45:09 +01:00
|
|
|
(void *)(unsigned long)ABPersonGetSortOrdering());
|
|
|
|
|
2014-07-31 15:50:24 +02:00
|
|
|
NSArray *sortedPeople = (__bridge_transfer NSArray *)allPeopleMutable;
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2014-08-09 01:01:05 +02:00
|
|
|
// This predicate returns all contacts from the addressbook having at least one phone number
|
2015-12-22 12:45:09 +01:00
|
|
|
|
|
|
|
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-09-07 20:43:53 +02:00
|
|
|
}];
|
2014-07-31 15:50:24 +02:00
|
|
|
CFRelease(allPeople);
|
2015-12-22 12:45:09 +01:00
|
|
|
NSArray *filteredContacts = [sortedPeople filteredArrayUsingPredicate:predicate];
|
|
|
|
|
2014-08-02 06:51:10 +02:00
|
|
|
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);
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2017-04-29 20:53:28 +02:00
|
|
|
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:)];
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
if (!firstName && !lastName) {
|
2015-12-22 12:45:09 +01:00
|
|
|
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) {
|
2015-12-22 12:45:09 +01:00
|
|
|
firstName = phoneNumbers.firstObject;
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
}
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2017-04-29 20:53:28 +02:00
|
|
|
NSData *imageData
|
|
|
|
= (__bridge_transfer NSData *)ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail);
|
|
|
|
UIImage *img = [UIImage imageWithData:imageData];
|
|
|
|
|
2015-12-22 12:45:09 +01:00
|
|
|
return [[Contact alloc] initWithContactWithFirstName:firstName
|
|
|
|
andLastName:lastName
|
|
|
|
andUserTextPhoneNumbers:phoneNumbers
|
2017-04-29 20:53:28 +02:00
|
|
|
phoneNumberTypeMap:phoneNumberTypeMap
|
2016-05-26 22:49:34 +02:00
|
|
|
andImage:img
|
2015-12-22 12:45:09 +01:00
|
|
|
andContactID:recordID];
|
|
|
|
}
|
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
- (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2 {
|
2014-09-07 20:43:53 +02:00
|
|
|
return [phoneNumber1.toE164 isEqualToString:phoneNumber2.toE164];
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
|
2017-04-29 20:53:28 +02:00
|
|
|
- (NSDictionary<NSString *, NSNumber *> *)phoneNumbersForRecord:(ABRecordRef)record
|
|
|
|
{
|
|
|
|
ABMultiValueRef phoneNumberRefs = NULL;
|
2015-12-22 12:45:09 +01:00
|
|
|
|
2014-05-06 19:41:08 +02:00
|
|
|
@try {
|
2017-04-29 20:53:28 +02:00
|
|
|
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
|
|
|
}
|
2017-04-29 20:53:28 +02:00
|
|
|
return [result copy];
|
2014-05-06 19:41:08 +02:00
|
|
|
} @finally {
|
2017-04-29 20:53:28 +02:00
|
|
|
if (phoneNumberRefs) {
|
|
|
|
CFRelease(phoneNumberRefs);
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#pragma mark - Whisper User Management
|
|
|
|
|
2015-12-22 12:45:09 +01:00
|
|
|
- (NSArray *)getSignalUsersFromContactsArray:(NSArray *)contacts {
|
2016-11-18 23:11:56 +01:00
|
|
|
NSMutableDictionary *signalContacts = [NSMutableDictionary new];
|
|
|
|
for (Contact *contact in contacts) {
|
|
|
|
if ([contact isSignalContact]) {
|
|
|
|
signalContacts[contact.textSecureIdentifiers.firstObject] = contact;
|
|
|
|
}
|
|
|
|
}
|
2015-02-17 00:14:50 +01:00
|
|
|
|
2016-12-01 23:36:57 +01:00
|
|
|
return [signalContacts.allValues sortedArrayUsingComparator:[[self class] contactComparator]];
|
|
|
|
}
|
|
|
|
|
|
|
|
+ (NSComparator)contactComparator
|
|
|
|
{
|
2016-11-23 20:30:21 +01:00
|
|
|
BOOL firstNameOrdering = ABPersonGetSortOrdering() == kABPersonCompositeNameFormatFirstNameFirst ? YES : NO;
|
2016-12-01 23:36:57 +01:00
|
|
|
return [Contact comparatorSortingNamesByFirstThenLast:firstNameOrdering];
|
2014-05-06 19:41:08 +02:00
|
|
|
}
|
|
|
|
|
2016-12-01 22:17:57 +01:00
|
|
|
- (NSArray<Contact *> * _Nonnull)signalContacts {
|
2016-06-28 02:07:18 +02:00
|
|
|
return [self getSignalUsersFromContactsArray:[self allContacts]];
|
2015-01-31 10:01:05 +01:00
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:29 +01:00
|
|
|
- (NSString *)unknownContactName
|
|
|
|
{
|
|
|
|
return NSLocalizedString(@"UNKNOWN_CONTACT_NAME",
|
|
|
|
@"Displayed if for some reason we can't determine a contacts phone number *or* name");
|
|
|
|
}
|
|
|
|
|
2016-12-01 22:17:57 +01:00
|
|
|
- (NSString * _Nonnull)displayNameForPhoneIdentifier:(NSString * _Nullable)identifier {
|
2016-09-11 22:53:12 +02:00
|
|
|
if (!identifier) {
|
2016-11-12 18:22:29 +01:00
|
|
|
return self.unknownContactName;
|
2016-09-11 22:53:12 +02:00
|
|
|
}
|
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;
|
|
|
|
|
2016-12-01 22:17:57 +01:00
|
|
|
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-04-28 18:18:42 +02:00
|
|
|
{
|
2017-05-01 18:51:59 +02:00
|
|
|
OWSAssert(signalAccount);
|
2017-04-28 18:18:42 +02:00
|
|
|
|
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];
|
2017-04-30 16:34:28 +02:00
|
|
|
} else {
|
|
|
|
return baseName;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-01 18:51:59 +02:00
|
|
|
- (NSAttributedString *_Nonnull)formattedDisplayNameForSignalAccount:(SignalAccount *)signalAccount
|
|
|
|
font:(UIFont *_Nonnull)font
|
2017-04-30 16:34:28 +02:00
|
|
|
{
|
2017-05-01 18:51:59 +02:00
|
|
|
OWSAssert(signalAccount);
|
2017-04-30 16:34:28 +02:00
|
|
|
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) {
|
2017-04-30 16:34:28 +02:00
|
|
|
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]];
|
2017-04-30 16:34:28 +02:00
|
|
|
[result appendAttributedString:[[NSAttributedString alloc] initWithString:@")"
|
|
|
|
attributes:@{
|
|
|
|
NSFontAttributeName : font,
|
|
|
|
}]];
|
|
|
|
return result;
|
|
|
|
} else {
|
|
|
|
return baseName;
|
|
|
|
}
|
2017-04-28 18:18:42 +02:00
|
|
|
}
|
|
|
|
|
2016-12-05 04:51:31 +01:00
|
|
|
- (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
|
|
|
}
|
|
|
|
|
2016-11-12 18:22:29 +01:00
|
|
|
- (Contact *)getOrBuildContactForPhoneIdentifier:(NSString *)identifier
|
|
|
|
{
|
2017-05-01 18:51:59 +02:00
|
|
|
Contact *savedContact = self.allContactsMap[identifier];
|
2016-11-12 18:22:29 +01:00
|
|
|
if (savedContact) {
|
|
|
|
return savedContact;
|
|
|
|
} else {
|
|
|
|
return [[Contact alloc] initWithContactWithFirstName:self.unknownContactName
|
|
|
|
andLastName:nil
|
|
|
|
andUserTextPhoneNumbers:@[ identifier ]
|
2017-04-29 20:53:28 +02:00
|
|
|
phoneNumberTypeMap:nil
|
2016-11-12 18:22:29 +01:00
|
|
|
andImage:nil
|
|
|
|
andContactID:0];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-01 22:17:57 +01:00
|
|
|
- (UIImage * _Nullable)imageForPhoneIdentifier:(NSString * _Nullable)identifier {
|
2017-05-01 18:51:59 +02:00
|
|
|
Contact *contact = self.allContactsMap[identifier];
|
2016-12-01 22:17:57 +01:00
|
|
|
|
|
|
|
return contact.image;
|
|
|
|
}
|
|
|
|
|
2017-04-18 23:06:21 +02:00
|
|
|
- (BOOL)hasAddressBook
|
|
|
|
{
|
2017-04-19 16:28:24 +02:00
|
|
|
return (BOOL)self.addressBookReference;
|
2017-04-18 23:06:21 +02:00
|
|
|
}
|
|
|
|
|
2016-10-14 22:59:58 +02:00
|
|
|
#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
|