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

403 lines
15 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"
#import "Signal-Swift.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>
#import <SignalServiceKit/SignalAccount.h>
2014-05-06 19:41:08 +02:00
@import Contacts;
2014-05-06 19:41:08 +02:00
2017-05-01 18:51:59 +02:00
NSString *const OWSContactsManagerSignalAccountsDidChangeNotification =
@"OWSContactsManagerSignalAccountsDidChangeNotification";
@interface OWSContactsManager () <SystemContactsFetcherDelegate>
@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;
@property (nonatomic, readonly) SystemContactsFetcher *systemContactsFetcher;
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 = @[];
_systemContactsFetcher = [SystemContactsFetcher new];
_systemContactsFetcher.delegate = self;
OWSSingletonAssert();
2014-05-06 19:41:08 +02:00
return self;
}
#pragma mark - System Contact Fetching
2014-05-06 19:41:08 +02:00
// Request contacts access if you haven't asked recently.
- (void)requestSystemContactsOnce
{
[self.systemContactsFetcher requestOnce];
2014-05-06 19:41:08 +02:00
}
- (void)fetchSystemContactsIfAlreadyAuthorized
{
[self.systemContactsFetcher fetchIfAlreadyAuthorized];
}
- (BOOL)isSystemContactsAuthorized
{
return self.systemContactsFetcher.isAuthorized;
}
#pragma mark SystemContactsFetcherDelegate
2014-05-06 19:41:08 +02:00
- (void)systemContactsFetcher:(SystemContactsFetcher *)systemsContactsFetcher
updatedContacts:(NSArray<Contact *> *)contacts
{
[self updateWithContacts:contacts];
2014-05-06 19:41:08 +02:00
}
#pragma mark - Intersection
- (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)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
});
}
#pragma mark - View Helpers
// TODO move into Contact class.
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.
NSString *phoneNumberLabel = [contact nameForPhoneNumber:recipientId];
2017-05-01 18:51:59 +02:00
// 2. Find all phone numbers for this contact of the same type.
NSMutableArray *phoneNumbersWithTheSameName = [NSMutableArray new];
2017-05-01 18:51:59 +02:00
for (NSString *textSecureIdentifier in contact.textSecureIdentifiers) {
if ([phoneNumberLabel isEqualToString:[contact nameForPhoneNumber:textSecureIdentifier]]) {
[phoneNumbersWithTheSameName addObject:textSecureIdentifier];
2017-05-01 18:51:59 +02:00
}
}
OWSAssert([phoneNumbersWithTheSameName containsObject:recipientId]);
if (phoneNumbersWithTheSameName.count > 1) {
2017-05-01 18:51:59 +02:00
NSUInteger index =
[[phoneNumbersWithTheSameName sortedArrayUsingSelector:@selector(compare:)] indexOfObject:recipientId];
2017-05-01 18:51:59 +02:00
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
}
- (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2 {
return [phoneNumber1.toE164 isEqualToString:phoneNumber2.toE164];
2014-05-06 19:41:08 +02:00
}
#pragma mark - Whisper User Management
- (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;
}
// TODO move into Contact class.
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;
}
}
// TODO move into Contact class.
- (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 ]
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;
}
#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