2017-04-21 00:04:00 +02:00
|
|
|
//
|
|
|
|
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
|
|
|
|
//
|
|
|
|
|
2015-12-07 03:31:43 +01:00
|
|
|
#import "Contact.h"
|
2017-05-12 15:21:33 +02:00
|
|
|
#import "Cryptography.h"
|
2015-12-07 03:31:43 +01:00
|
|
|
#import "PhoneNumber.h"
|
|
|
|
#import "SignalRecipient.h"
|
2017-04-29 20:53:04 +02:00
|
|
|
#import "TSAccountManager.h"
|
2015-12-07 03:31:43 +01:00
|
|
|
#import "TSStorageManager.h"
|
|
|
|
|
2016-11-26 18:29:02 +01:00
|
|
|
@import Contacts;
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
2017-04-29 20:53:04 +02:00
|
|
|
@interface Contact ()
|
|
|
|
|
2017-05-04 17:07:58 +02:00
|
|
|
@property (readonly, nonatomic) NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap;
|
2017-04-29 20:53:04 +02:00
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
#pragma mark -
|
|
|
|
|
2015-12-07 03:31:43 +01:00
|
|
|
@implementation Contact
|
|
|
|
|
2016-11-29 16:02:46 +01:00
|
|
|
@synthesize fullName = _fullName;
|
|
|
|
@synthesize comparableNameFirstLast = _comparableNameFirstLast;
|
|
|
|
@synthesize comparableNameLastFirst = _comparableNameLastFirst;
|
|
|
|
|
2015-12-07 03:31:43 +01:00
|
|
|
#if TARGET_OS_IOS
|
2016-11-26 18:29:02 +01:00
|
|
|
- (instancetype)initWithContactWithFirstName:(nullable NSString *)firstName
|
|
|
|
andLastName:(nullable NSString *)lastName
|
2015-12-07 03:31:43 +01:00
|
|
|
andUserTextPhoneNumbers:(NSArray *)phoneNumbers
|
2016-11-26 18:29:02 +01:00
|
|
|
andImage:(nullable UIImage *)image
|
|
|
|
andContactID:(ABRecordID)record
|
|
|
|
{
|
|
|
|
self = [super init];
|
|
|
|
if (!self) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2017-04-21 00:04:00 +02:00
|
|
|
_firstName = [self trimName:firstName];
|
|
|
|
_lastName = [self trimName:lastName];
|
2016-11-26 18:29:02 +01:00
|
|
|
_uniqueId = [self.class uniqueIdFromABRecordId:record];
|
|
|
|
_recordID = record;
|
|
|
|
_userTextPhoneNumbers = phoneNumbers;
|
2017-05-04 17:07:58 +02:00
|
|
|
_phoneNumberNameMap = [NSMutableDictionary new];
|
|
|
|
_parsedPhoneNumbers = [self parsedPhoneNumbersFromUserTextPhoneNumbers:phoneNumbers phoneNumberNameMap:@{}];
|
2016-11-26 18:29:02 +01:00
|
|
|
_image = image;
|
|
|
|
// Not using emails for old AB style contacts.
|
|
|
|
_emails = [NSMutableArray new];
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2017-05-03 15:46:20 +02:00
|
|
|
- (instancetype)initWithSystemContact:(CNContact *)contact
|
2016-11-26 18:29:02 +01:00
|
|
|
{
|
2015-12-07 03:31:43 +01:00
|
|
|
self = [super init];
|
2016-11-26 18:29:02 +01:00
|
|
|
if (!self) {
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
_cnContact = contact;
|
2017-04-21 00:04:00 +02:00
|
|
|
_firstName = [self trimName:contact.givenName];
|
|
|
|
_lastName = [self trimName:contact.familyName];
|
2016-11-26 18:29:02 +01:00
|
|
|
_uniqueId = contact.identifier;
|
|
|
|
|
|
|
|
NSMutableArray<NSString *> *phoneNumbers = [NSMutableArray new];
|
2017-05-04 17:07:58 +02:00
|
|
|
NSMutableDictionary<NSString *, NSString *> *phoneNumberNameMap = [NSMutableDictionary new];
|
2016-11-26 18:29:02 +01:00
|
|
|
for (CNLabeledValue *phoneNumberField in contact.phoneNumbers) {
|
|
|
|
if ([phoneNumberField.value isKindOfClass:[CNPhoneNumber class]]) {
|
|
|
|
CNPhoneNumber *phoneNumber = (CNPhoneNumber *)phoneNumberField.value;
|
|
|
|
[phoneNumbers addObject:phoneNumber.stringValue];
|
2017-05-03 17:19:11 +02:00
|
|
|
if ([phoneNumberField.label isEqualToString:CNLabelHome]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_HOME", @"Label for 'Home' phone numbers.");
|
2017-05-03 17:19:11 +02:00
|
|
|
} else if ([phoneNumberField.label isEqualToString:CNLabelWork]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_WORK", @"Label for 'Work' phone numbers.");
|
2017-04-29 20:53:04 +02:00
|
|
|
} else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberiPhone]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_IPHONE", @"Label for 'IPhone' phone numbers.");
|
|
|
|
;
|
2017-05-03 17:19:11 +02:00
|
|
|
} else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberMobile]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_MOBILE", @"Label for 'Mobile' phone numbers.");
|
2017-04-29 20:53:04 +02:00
|
|
|
} else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberMain]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_MAIN", @"Label for 'Main' phone numbers.");
|
2017-04-29 20:53:04 +02:00
|
|
|
} else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberHomeFax]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_HOME_FAX", @"Label for 'HomeFAX' phone numbers.");
|
2017-04-29 20:53:04 +02:00
|
|
|
} else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberWorkFax]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_WORK_FAX", @"Label for 'Work FAX' phone numbers.");
|
2017-04-29 20:53:04 +02:00
|
|
|
} else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberOtherFax]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_OTHER_FAX", @"Label for 'Other FAX' phone numbers.");
|
2017-04-29 20:53:04 +02:00
|
|
|
} else if ([phoneNumberField.label isEqualToString:CNLabelPhoneNumberPager]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_PAGER", @"Label for 'Pager' phone numbers.");
|
2017-05-03 17:19:11 +02:00
|
|
|
} else if ([phoneNumberField.label isEqualToString:CNLabelOther]) {
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap[phoneNumber.stringValue]
|
|
|
|
= NSLocalizedString(@"PHONE_NUMBER_TYPE_OTHER", @"Label for 'Other' phone numbers.");
|
|
|
|
} else if (phoneNumberField.label.length > 0 && ![phoneNumberField.label hasPrefix:@"_$"]) {
|
|
|
|
// We'll reach this case for:
|
|
|
|
//
|
|
|
|
// * User-defined custom labels, which we want to display.
|
|
|
|
// * Labels like "_$!<CompanyMain>!$_", which I'm guessing are synced from other platforms.
|
|
|
|
// We don't want to display these labels. Even some of iOS' default labels (like Radio) show
|
|
|
|
// up this way.
|
|
|
|
phoneNumberNameMap[phoneNumber.stringValue] = phoneNumberField.label;
|
2017-04-29 20:53:04 +02:00
|
|
|
}
|
2015-12-07 03:31:43 +01:00
|
|
|
}
|
2016-11-26 18:29:02 +01:00
|
|
|
}
|
2017-04-29 20:53:04 +02:00
|
|
|
|
2016-11-26 18:29:02 +01:00
|
|
|
_userTextPhoneNumbers = [phoneNumbers copy];
|
2017-05-04 17:07:58 +02:00
|
|
|
_phoneNumberNameMap = [NSMutableDictionary new];
|
2017-04-29 20:53:04 +02:00
|
|
|
_parsedPhoneNumbers =
|
2017-05-04 17:07:58 +02:00
|
|
|
[self parsedPhoneNumbersFromUserTextPhoneNumbers:phoneNumbers phoneNumberNameMap:phoneNumberNameMap];
|
2016-11-26 18:29:02 +01:00
|
|
|
|
|
|
|
NSMutableArray<NSString *> *emailAddresses = [NSMutableArray new];
|
|
|
|
for (CNLabeledValue *emailField in contact.emailAddresses) {
|
|
|
|
if ([emailField.value isKindOfClass:[NSString class]]) {
|
|
|
|
[emailAddresses addObject:(NSString *)emailField.value];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_emails = [emailAddresses copy];
|
2015-12-07 03:31:43 +01:00
|
|
|
|
2016-11-26 18:29:02 +01:00
|
|
|
if (contact.thumbnailImageData) {
|
|
|
|
_image = [UIImage imageWithData:contact.thumbnailImageData];
|
2015-12-07 03:31:43 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
2016-11-26 18:29:02 +01:00
|
|
|
|
2017-04-21 00:04:00 +02:00
|
|
|
- (NSString *)trimName:(NSString *)name
|
|
|
|
{
|
|
|
|
return [name stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
|
|
|
|
}
|
|
|
|
|
2016-11-26 18:29:02 +01:00
|
|
|
+ (NSString *)uniqueIdFromABRecordId:(ABRecordID)recordId
|
|
|
|
{
|
|
|
|
return [NSString stringWithFormat:@"ABRecordId:%d", recordId];
|
|
|
|
}
|
|
|
|
|
|
|
|
#endif // TARGET_OS_IOS
|
|
|
|
|
2017-04-29 20:53:04 +02:00
|
|
|
- (NSArray<PhoneNumber *> *)parsedPhoneNumbersFromUserTextPhoneNumbers:(NSArray<NSString *> *)userTextPhoneNumbers
|
2017-05-04 17:07:58 +02:00
|
|
|
phoneNumberNameMap:(nullable NSDictionary<NSString *, NSString *> *)
|
|
|
|
phoneNumberNameMap
|
2016-11-26 18:29:02 +01:00
|
|
|
{
|
2017-05-04 17:07:58 +02:00
|
|
|
OWSAssert(self.phoneNumberNameMap);
|
2017-04-29 20:53:04 +02:00
|
|
|
|
|
|
|
NSMutableDictionary<NSString *, PhoneNumber *> *parsedPhoneNumberMap = [NSMutableDictionary new];
|
2017-05-10 18:39:40 +02:00
|
|
|
NSMutableArray<PhoneNumber *> *parsedPhoneNumbers = [NSMutableArray new];
|
2016-11-26 18:29:02 +01:00
|
|
|
for (NSString *phoneNumberString in userTextPhoneNumbers) {
|
2017-04-29 20:53:04 +02:00
|
|
|
for (PhoneNumber *phoneNumber in
|
|
|
|
[PhoneNumber tryParsePhoneNumbersFromsUserSpecifiedText:phoneNumberString
|
|
|
|
clientPhoneNumber:[TSAccountManager localNumber]]) {
|
2017-05-10 18:39:40 +02:00
|
|
|
[parsedPhoneNumbers addObject:phoneNumber];
|
2017-04-29 20:53:04 +02:00
|
|
|
parsedPhoneNumberMap[phoneNumber.toE164] = phoneNumber;
|
2017-05-04 17:07:58 +02:00
|
|
|
NSString *phoneNumberName = phoneNumberNameMap[phoneNumberString];
|
|
|
|
if (phoneNumberName) {
|
|
|
|
self.phoneNumberNameMap[phoneNumber.toE164] = phoneNumberName;
|
2017-04-29 20:53:04 +02:00
|
|
|
}
|
2016-11-26 18:29:02 +01:00
|
|
|
}
|
|
|
|
}
|
2017-05-10 18:39:40 +02:00
|
|
|
return [parsedPhoneNumbers sortedArrayUsingSelector:@selector(compare:)];
|
2016-11-26 18:29:02 +01:00
|
|
|
}
|
2015-12-07 03:31:43 +01:00
|
|
|
|
|
|
|
- (NSString *)fullName {
|
2016-11-29 16:02:46 +01:00
|
|
|
if (_fullName == nil) {
|
|
|
|
if (ABPersonGetCompositeNameFormat() == kABPersonCompositeNameFormatFirstNameFirst) {
|
|
|
|
_fullName = [self combineLeftName:_firstName withRightName:_lastName usingSeparator:@" "];
|
|
|
|
} else {
|
|
|
|
_fullName = [self combineLeftName:_lastName withRightName:_firstName usingSeparator:@" "];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return _fullName;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *)comparableNameFirstLast {
|
|
|
|
if (_comparableNameFirstLast == nil) {
|
|
|
|
// Combine the two names with a tab separator, which has a lower ascii code than space, so that first names
|
|
|
|
// that contain a space ("Mary Jo\tCatlett") will sort after those that do not ("Mary\tOliver")
|
|
|
|
_comparableNameFirstLast = [self combineLeftName:_firstName withRightName:_lastName usingSeparator:@"\t"];
|
|
|
|
}
|
|
|
|
|
|
|
|
return _comparableNameFirstLast;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *)comparableNameLastFirst {
|
|
|
|
if (_comparableNameLastFirst == nil) {
|
|
|
|
// Combine the two names with a tab separator, which has a lower ascii code than space, so that last names
|
|
|
|
// that contain a space ("Van Der Beek\tJames") will sort after those that do not ("Van\tJames")
|
|
|
|
_comparableNameLastFirst = [self combineLeftName:_lastName withRightName:_firstName usingSeparator:@"\t"];
|
|
|
|
}
|
|
|
|
|
|
|
|
return _comparableNameLastFirst;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *)combineLeftName:(NSString *)leftName withRightName:(NSString *)rightName usingSeparator:(NSString *)separator {
|
|
|
|
const BOOL leftNameNonEmpty = (leftName.length > 0);
|
|
|
|
const BOOL rightNameNonEmpty = (rightName.length > 0);
|
|
|
|
|
|
|
|
if (leftNameNonEmpty && rightNameNonEmpty) {
|
|
|
|
return [NSString stringWithFormat:@"%@%@%@", leftName, separator, rightName];
|
|
|
|
} else if (leftNameNonEmpty) {
|
|
|
|
return [leftName copy];
|
|
|
|
} else if (rightNameNonEmpty) {
|
|
|
|
return [rightName copy];
|
|
|
|
} else {
|
|
|
|
return @"";
|
2015-12-07 03:31:43 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSString *)description {
|
2016-11-29 16:02:46 +01:00
|
|
|
return [NSString stringWithFormat:@"%@: %@", self.fullName, self.userTextPhoneNumbers];
|
2015-12-07 03:31:43 +01:00
|
|
|
}
|
|
|
|
|
2016-06-28 04:48:37 +02:00
|
|
|
- (BOOL)isSignalContact {
|
2015-12-07 03:31:43 +01:00
|
|
|
NSArray *identifiers = [self textSecureIdentifiers];
|
|
|
|
|
2016-06-28 04:48:37 +02:00
|
|
|
return [identifiers count] > 0;
|
2015-12-07 03:31:43 +01:00
|
|
|
}
|
|
|
|
|
2017-05-01 20:10:03 +02:00
|
|
|
- (NSArray<SignalRecipient *> *)signalRecipientsWithTransaction:(YapDatabaseReadTransaction *)transaction
|
2017-05-01 18:44:58 +02:00
|
|
|
{
|
|
|
|
__block NSMutableArray *result = [NSMutableArray array];
|
|
|
|
|
2017-05-01 20:10:03 +02:00
|
|
|
for (PhoneNumber *number in [self.parsedPhoneNumbers sortedArrayUsingSelector:@selector(compare:)]) {
|
|
|
|
SignalRecipient *signalRecipient =
|
|
|
|
[SignalRecipient recipientWithTextSecureIdentifier:number.toE164 withTransaction:transaction];
|
|
|
|
if (signalRecipient) {
|
|
|
|
[result addObject:signalRecipient];
|
2017-05-01 18:44:58 +02:00
|
|
|
}
|
2017-05-01 20:10:03 +02:00
|
|
|
}
|
|
|
|
|
2017-05-01 18:44:58 +02:00
|
|
|
return [result copy];
|
|
|
|
}
|
|
|
|
|
2016-06-28 04:48:37 +02:00
|
|
|
- (NSArray<NSString *> *)textSecureIdentifiers {
|
2015-12-07 03:31:43 +01:00
|
|
|
__block NSMutableArray *identifiers = [NSMutableArray array];
|
|
|
|
|
|
|
|
[[TSStorageManager sharedManager]
|
|
|
|
.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
|
|
|
for (PhoneNumber *number in self.parsedPhoneNumbers) {
|
|
|
|
if ([SignalRecipient recipientWithTextSecureIdentifier:number.toE164 withTransaction:transaction]) {
|
|
|
|
[identifiers addObject:number.toE164];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}];
|
2016-12-04 01:33:21 +01:00
|
|
|
return [identifiers copy];
|
2015-12-07 03:31:43 +01:00
|
|
|
}
|
|
|
|
|
2016-11-29 16:02:46 +01:00
|
|
|
+ (NSComparator)comparatorSortingNamesByFirstThenLast:(BOOL)firstNameOrdering {
|
|
|
|
return ^NSComparisonResult(id obj1, id obj2) {
|
|
|
|
Contact *contact1 = (Contact *)obj1;
|
|
|
|
Contact *contact2 = (Contact *)obj2;
|
|
|
|
|
|
|
|
if (firstNameOrdering) {
|
|
|
|
return [contact1.comparableNameFirstLast caseInsensitiveCompare:contact2.comparableNameFirstLast];
|
|
|
|
} else {
|
|
|
|
return [contact1.comparableNameLastFirst caseInsensitiveCompare:contact2.comparableNameLastFirst];
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2017-05-04 17:07:58 +02:00
|
|
|
- (NSString *)nameForPhoneNumber:(NSString *)recipientId
|
2017-04-30 16:34:30 +02:00
|
|
|
{
|
|
|
|
OWSAssert(recipientId.length > 0);
|
|
|
|
OWSAssert([self.textSecureIdentifiers containsObject:recipientId]);
|
|
|
|
|
2017-05-04 17:07:58 +02:00
|
|
|
NSString *value = self.phoneNumberNameMap[recipientId];
|
2017-04-30 16:34:30 +02:00
|
|
|
OWSAssert(value);
|
|
|
|
if (!value) {
|
2017-05-04 17:07:58 +02:00
|
|
|
return NSLocalizedString(@"PHONE_NUMBER_TYPE_UNKNOWN",
|
|
|
|
@"Label used when we don't what kind of phone number it is (e.g. mobile/work/home).");
|
2017-04-30 16:34:30 +02:00
|
|
|
}
|
2017-05-04 17:07:58 +02:00
|
|
|
return value;
|
2017-04-30 16:34:30 +02:00
|
|
|
}
|
|
|
|
|
2017-05-11 18:59:03 +02:00
|
|
|
- (NSUInteger)hash
|
|
|
|
{
|
2017-05-12 15:21:33 +02:00
|
|
|
// base hash is some arbitrary number
|
|
|
|
NSUInteger hash = 1825038313;
|
2017-05-11 18:59:03 +02:00
|
|
|
|
2017-05-12 15:21:33 +02:00
|
|
|
hash = hash ^ self.fullName.hash;
|
|
|
|
|
|
|
|
// base thumbnailHash is some arbitrary number
|
|
|
|
NSUInteger thumbnailHash = 389201946;
|
|
|
|
if (self.cnContact.thumbnailImageData) {
|
|
|
|
NSData *thumbnailHashData =
|
|
|
|
[Cryptography computeSHA256Digest:self.cnContact.thumbnailImageData truncatedToBytes:sizeof(thumbnailHash)];
|
|
|
|
[thumbnailHashData getBytes:&thumbnailHash length:sizeof(thumbnailHash)];
|
|
|
|
}
|
|
|
|
|
|
|
|
hash = hash ^ thumbnailHash;
|
2017-05-11 18:59:03 +02:00
|
|
|
|
|
|
|
for (PhoneNumber *phoneNumber in self.parsedPhoneNumbers) {
|
|
|
|
hash = hash ^ phoneNumber.toE164.hash;
|
|
|
|
}
|
|
|
|
|
2017-05-11 23:56:18 +02:00
|
|
|
for (NSString *email in self.emails) {
|
|
|
|
hash = hash ^ email.hash;
|
|
|
|
}
|
|
|
|
|
2017-05-11 18:59:03 +02:00
|
|
|
return hash;
|
|
|
|
}
|
|
|
|
|
2015-12-07 03:31:43 +01:00
|
|
|
@end
|
2016-11-26 18:29:02 +01:00
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|