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

522 lines
19 KiB
Objective-C

#import "ContactsManager.h"
#import "FutureSource.h"
#import "FutureUtil.h"
#import <AddressBook/AddressBook.h>
#import "Constraints.h"
#import "Environment.h"
#import "NotificationManifest.h"
#import "PhoneNumberDirectoryFilter.h"
#import "PhoneNumberDirectoryFilterManager.h"
#import "PreferencesUtil.h"
#import "Util.h"
//@TODO: Split Up into Seperate components.
#define ADDRESSBOOK_QUEUE dispatch_get_main_queue()
static NSString *const FAVOURITES_DEFAULT_KEY = @"FAVOURITES_DEFAULT_KEY";
static NSString *const KNOWN_USERS_DEFAULT_KEY = @"KNOWN_USERS_DEFAULT_KEY";
typedef BOOL (^ContactSearchBlock)(id, NSUInteger, BOOL*);
@interface ContactsManager () {
NSMutableArray *_favouriteContactIds;
NSMutableArray *_knownWhisperUserIds;
BOOL newUserNotificationsEnabled;
id addressBookReference;
}
@end
@implementation ContactsManager
- (id)init {
self = [super init];
if (self) {
newUserNotificationsEnabled = [self knownUserStoreInitialized];
_favouriteContactIds = [self loadFavouriteIds];
_knownWhisperUserIds = [self loadKnownWhisperUsers];
life = [CancelTokenSource cancelTokenSource];
observableContactsController = [ObservableValueController observableValueControllerWithInitialValue:nil];
observableWhisperUsersController = [ObservableValueController observableValueControllerWithInitialValue:nil];
[self registerNotificationHandlers];
}
return self;
}
-(void) doAfterEnvironmentInitSetup {
[self setupAddressBook];
[observableContactsController watchLatestValueOnArbitraryThread:^(NSArray *latestContacts) {
@synchronized(self) {
[self setupLatestContacts:latestContacts];
}
} untilCancelled:[life getToken]];
[observableWhisperUsersController watchLatestValueOnArbitraryThread:^(NSArray *latestUsers) {
@synchronized(self) {
[self setupLatestWhisperUsers:latestUsers];
}
} untilCancelled:[life getToken]];
}
-(void)dealloc {
[life cancel];
}
#pragma mark - Notification Handlers
-(void) registerNotificationHandlers{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(updatedDirectoryHandler:) name:NOTIFICATION_DIRECTORY_UPDATE object:nil];
}
-(void) updatedDirectoryHandler:(NSNotification*) notification {
[self checkForNewWhisperUsers];
}
-(void) enableNewUserNotifications{
newUserNotificationsEnabled = YES;
}
#pragma mark - Address Book callbacks
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context);
void onAddressBookChanged(ABAddressBookRef notifyAddressBook, CFDictionaryRef info, void *context) {
ContactsManager* contactsManager = (__bridge ContactsManager*)context;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[contactsManager pullLatestAddressBook];
});
}
#pragma mark - Setup
-(void) setupAddressBook {
dispatch_async(ADDRESSBOOK_QUEUE, ^{
[[ContactsManager asyncGetAddressBook] thenDo:^(id addressBook) {
addressBookReference = addressBook;
ABAddressBookRef cfAddressBook = (__bridge ABAddressBookRef)addressBook;
ABAddressBookRegisterExternalChangeCallback(cfAddressBook, onAddressBookChanged, (__bridge void*)self);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{
[self pullLatestAddressBook];
});
}];
});
}
-(void) pullLatestAddressBook{
CFErrorRef creationError = nil;
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
checkOperationDescribe(nil == creationError, [((__bridge NSError *)creationError) localizedDescription]) ;
ABAddressBookRequestAccessWithCompletion(addressBookRef, nil);
[observableContactsController updateValue:[self getContactsFromAddressBook:addressBookRef]];
}
- (void)setupLatestContacts:(NSArray *)contacts {
if (contacts) {
latestContactsById = [ContactsManager keyContactsById:contacts];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self checkForNewWhisperUsers];
});
}
}
- (void)setupLatestWhisperUsers:(NSArray *)users {
if (users) {
latestWhisperUsersById = [ContactsManager keyContactsById:users];
if (!observableFavouritesController) {
NSArray *favourites = [self contactsForContactIds:_favouriteContactIds];
observableFavouritesController = [ObservableValueController observableValueControllerWithInitialValue:favourites];
}
}
}
#pragma mark - Observables
-(ObservableValue *) getObservableContacts {
return observableContactsController;
}
-(ObservableValue *) getObservableWhisperUsers {
return observableWhisperUsersController;
}
-(ObservableValue *) getObservableFavourites {
return observableFavouritesController;
}
#pragma mark - Address Book utils
+(Future*) asyncGetAddressBook {
CFErrorRef creationError = nil;
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, &creationError);
assert((addressBookRef == nil) == (creationError != nil));
if (creationError != nil) {
return [Future failed:(__bridge_transfer id)creationError];
}
FutureSource *futureAddressBookSource = [FutureSource new];
id addressBook = (__bridge_transfer id)addressBookRef;
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef requestAccessError) {
if (granted) {
dispatch_async(ADDRESSBOOK_QUEUE,^{
[futureAddressBookSource trySetResult:addressBook];
});
} else {
[futureAddressBookSource trySetFailure:(__bridge id)requestAccessError];
}
});
return futureAddressBookSource;
}
-(NSArray*) getContactsFromAddressBook:(ABAddressBookRef)addressBook {
ABRecordRef source = ABAddressBookCopyDefaultSource(addressBook);
NSArray *allPeople = (__bridge_transfer NSArray *)
(ABAddressBookCopyArrayOfAllPeopleInSourceWithSortOrdering(addressBook,
source,
kABPersonSortByFirstName));
return [allPeople map:^id(id item) {
return [self contactForRecord:(__bridge ABRecordRef)item];
}];
}
-(NSArray*)latestContactsWithSearchString:(NSString *)searchString {
return [[latestContactsById allValues] filter:^int(Contact *contact) {
return [searchString length] == 0 || [ContactsManager name:[contact fullName] matchesQuery:searchString];
}];
}
#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);
NSArray *phoneNumbers = [self phoneNumbersForRecord:record];
if (!firstName && !lastName) {
NSString *companyName = (__bridge_transfer NSString*)ABRecordCopyValue(record, kABPersonOrganizationProperty);
if (companyName) {
firstName = companyName;
} else if ([phoneNumbers count]) {
firstName = [phoneNumbers firstObject];
}
}
NSString *notes = (__bridge_transfer NSString*)ABRecordCopyValue(record, kABPersonNoteProperty);
NSArray *emails = [ContactsManager emailsForRecord:record];
NSData *image = (__bridge_transfer NSData*)ABPersonCopyImageDataWithFormat(record, kABPersonImageFormatThumbnail);
UIImage *img = [UIImage imageWithData:image];
ContactSearchBlock searchBlock = ^BOOL(NSNumber *obj, NSUInteger idx, BOOL *stop) {
return [obj intValue] == recordID;
};
NSUInteger favouriteIndex = [_favouriteContactIds indexOfObjectPassingTest:searchBlock];
return [Contact contactWithFirstName:firstName
andLastName:lastName
andUserTextPhoneNumbers:phoneNumbers
andEmails:emails
andImage:img
andContactID:recordID
andIsFavourite:favouriteIndex != NSNotFound
andNotes:notes];
}
-(Contact*)latestContactForPhoneNumber:(PhoneNumber *)phoneNumber {
NSArray *allContacts = [latestContactsById allValues];
ContactSearchBlock searchBlock = ^BOOL(Contact *contact, NSUInteger idx, BOOL *stop) {
for (PhoneNumber *number in contact.parsedPhoneNumbers) {
if ([self phoneNumber:number matchesNumber:phoneNumber]) {
*stop = YES;
return YES;
}
}
return NO;
};
NSUInteger contactIndex = [allContacts indexOfObjectPassingTest:searchBlock];
if (contactIndex != NSNotFound) {
return allContacts[contactIndex];
} else {
return nil;
}
}
- (BOOL)phoneNumber:(PhoneNumber *)phoneNumber1 matchesNumber:(PhoneNumber *)phoneNumber2 {
return [[phoneNumber1 toE164] isEqualToString:[phoneNumber2 toE164]];
}
- (NSArray *)phoneNumbersForRecord:(ABRecordRef)record {
ABMultiValueRef numberRefs = ABRecordCopyValue(record, kABPersonPhoneProperty);
@try {
NSArray *phoneNumbers = (__bridge_transfer NSArray*)ABMultiValueCopyArrayOfAllValues(numberRefs);
if (phoneNumbers == nil) phoneNumbers = @[];
NSMutableArray *numbers = [NSMutableArray array];
for (CFIndex i = 0; i < (NSInteger)[phoneNumbers count]; i++) {
NSString *phoneNumber = phoneNumbers[(NSUInteger)i];
[numbers addObject:phoneNumber];
}
return numbers;
} @finally {
if (numberRefs) {
CFRelease(numberRefs);
}
}
}
+(NSArray *)emailsForRecord:(ABRecordRef)record {
ABMultiValueRef emailRefs = ABRecordCopyValue(record, kABPersonEmailProperty);
@try {
NSArray *emails = (__bridge_transfer NSArray*)ABMultiValueCopyArrayOfAllValues(emailRefs);
if (emails == nil) emails = @[];
return emails;
} @finally {
if (emailRefs) {
CFRelease(emailRefs);
}
}
}
+(NSDictionary *)groupContactsByFirstLetter:(NSArray *)contacts matchingSearchString:(NSString *)optionalSearchString {
require(contacts != nil);
NSArray *matchingContacts = [contacts filter:^int(Contact *contact) {
return [optionalSearchString length] == 0 || [self name:[contact fullName] matchesQuery:optionalSearchString];
}];
return [matchingContacts groupBy:^id(Contact *contact) {
NSString *nameToUse = @"";
BOOL firstNameOrdering = ABPersonGetSortOrdering() == kABPersonCompositeNameFormatFirstNameFirst?YES:NO;
if (firstNameOrdering && [contact firstName] != nil && [[contact firstName] length] > 0) {
nameToUse = [contact firstName];
} else if (!firstNameOrdering && [contact lastName] != nil && [[contact lastName] length] > 0){
nameToUse = [contact lastName];
} else if ([contact lastName] == nil) {
if ([[contact fullName] length] > 0) {
nameToUse = [contact fullName];
} else {
return nameToUse;
}
} else {
nameToUse = [contact lastName];
}
return [[[nameToUse substringToIndex:1] uppercaseString] decomposedStringWithCompatibilityMapping];
}];
}
+(NSDictionary *)keyContactsById:(NSArray *)contacts {
return [contacts keyedBy:^id(Contact* contact) {
return [NSNumber numberWithInt:(int)contact.recordID];
}];
}
-(Contact *)latestContactWithRecordId:(ABRecordID)recordId {
@synchronized(self) {
return [latestContactsById objectForKey:[NSNumber numberWithInt:recordId]];
}
}
-(NSArray*) recordsForContacts:(NSArray*) contacts{
return [contacts map:^id(Contact *contact) {
return [NSNumber numberWithInt:[contact recordID]];
}];
}
+(BOOL)name:(NSString *)nameString matchesQuery:(NSString *)queryString {
NSCharacterSet *whitespaceSet = [NSCharacterSet whitespaceCharacterSet];
NSArray *queryStrings = [queryString componentsSeparatedByCharactersInSet:whitespaceSet];
NSArray *nameStrings = [nameString componentsSeparatedByCharactersInSet:whitespaceSet];
return [queryStrings all:^int(NSString* query) {
if ([query length] == 0) return YES;
return [nameStrings any:^int(NSString* nameWord) {
NSStringCompareOptions searchOpts = NSCaseInsensitiveSearch | NSAnchoredSearch;
return [nameWord rangeOfString:query options:searchOpts].location != NSNotFound;
}];
}];
}
+(BOOL)phoneNumber:(PhoneNumber *)phoneNumber matchesQuery:(NSString *)queryString {
NSString *phoneNumberString = [phoneNumber localizedDescriptionForUser];
NSString *searchString = [[phoneNumberString componentsSeparatedByCharactersInSet:
[[NSCharacterSet decimalDigitCharacterSet] invertedSet]]
componentsJoinedByString:@""];
if ([queryString length] == 0) return YES;
NSStringCompareOptions searchOpts = NSCaseInsensitiveSearch | NSAnchoredSearch;
return [searchString rangeOfString:queryString options:searchOpts].location != NSNotFound;
}
-(NSArray*) contactsForContactIds:(NSArray *)contactIds {
NSMutableArray *contacts = [NSMutableArray array];
for (NSNumber *favouriteId in contactIds) {
Contact *contact = [self latestContactWithRecordId:[favouriteId intValue]];
if (contact) {
[contacts addObject:contact];
}
}
return [contacts copy];
}
#pragma mark - Favourites
-(NSMutableArray *)loadFavouriteIds {
NSArray *favourites = [[NSUserDefaults standardUserDefaults] objectForKey:FAVOURITES_DEFAULT_KEY];
return favourites == nil ? [NSMutableArray array] : [favourites mutableCopy];
}
-(void)saveFavouriteIds {
[[NSUserDefaults standardUserDefaults] setObject:[_favouriteContactIds copy]
forKey:FAVOURITES_DEFAULT_KEY];
[[NSUserDefaults standardUserDefaults] synchronize];
[observableFavouritesController updateValue:[self contactsForContactIds:_favouriteContactIds]];
}
-(void)toggleFavourite:(Contact *)contact {
require(contact != nil);
contact.isFavourite = !contact.isFavourite;
if (contact.isFavourite) {
[_favouriteContactIds addObject:[NSNumber numberWithInt:contact.recordID]];
} else {
ContactSearchBlock removeBlock = ^BOOL(NSNumber *favouriteNumber, NSUInteger idx, BOOL *stop) {
return [favouriteNumber integerValue] == contact.recordID;
};
NSUInteger indexToRemove = [_favouriteContactIds indexOfObjectPassingTest:removeBlock];
if (indexToRemove != NSNotFound) {
[_favouriteContactIds removeObjectAtIndex:indexToRemove];
}
}
[self saveFavouriteIds];
}
+(NSArray *)favouritesForAllContacts:(NSArray *)contacts {
return [contacts filter:^int(Contact* contact) {
return [contact isFavourite];
}];
}
#pragma mark - Whisper User Management
-(NSUInteger) checkForNewWhisperUsers {
NSArray *currentUsers = [self getWhisperUsersFromContactsArray:[latestContactsById allValues]];
NSArray *newUsers = [self getNewItemsFrom:currentUsers comparedTo:[latestWhisperUsersById allValues]];
if([newUsers count] > 0){
[observableWhisperUsersController updateValue:currentUsers];
}
NSArray *unacknowledgedUserIds = [self getUnacknowledgedUsersFrom:currentUsers];
if([unacknowledgedUserIds count] > 0){
NSArray *unacknowledgedUsers = [self contactsForContactIds: unacknowledgedUserIds];
if(!newUserNotificationsEnabled){
[self addContactsToKnownWhisperUsers:unacknowledgedUsers];
}else{
NSDictionary *payload = [NSDictionary dictionaryWithObject:unacknowledgedUsers forKey:NOTIFICATION_DATAKEY_NEW_USERS];
[[NSNotificationCenter defaultCenter] postNotificationName:NOTIFICATION_NEW_USERS_AVAILABLE object:self userInfo:payload];
}
}
return [newUsers count];
}
-(NSArray*) getUnacknowledgedUsersFrom:(NSArray*) users {
NSArray *userIds = [self recordsForContacts:users];
return [self getNewItemsFrom:userIds comparedTo:_knownWhisperUserIds ];
}
-(NSUInteger) getNumberOfUnacknowledgedCurrentUsers{
NSArray *currentUsers = [self getWhisperUsersFromContactsArray:[latestContactsById allValues]];
return [[self getUnacknowledgedUsersFrom:currentUsers] count];
}
-(NSArray*) getWhisperUsersFromContactsArray:(NSArray*) contacts {
return [contacts filter:^int(Contact* contact) {
return [self isContactRegisteredWithWhisper:contact];
}];
}
-(NSArray*) getNewItemsFrom:(NSArray*) newArray comparedTo:(NSArray*) oldArray {
NSMutableSet *newSet = [NSMutableSet setWithArray:newArray];
NSSet *oldSet = [NSSet setWithArray:oldArray];
[newSet minusSet:oldSet];
return [newSet allObjects];
}
- (BOOL)isContactRegisteredWithWhisper:(Contact*) contact {
for(PhoneNumber *phoneNumber in contact.parsedPhoneNumbers){
if ( [self isPhoneNumberRegisteredWithWhisper:phoneNumber]) {
return YES;
}
}
return NO;
}
- (BOOL)isPhoneNumberRegisteredWithWhisper:(PhoneNumber *)phoneNumber {
PhoneNumberDirectoryFilter* directory = [[[Environment getCurrent] phoneDirectoryManager] getCurrentFilter];
return phoneNumber != nil && [directory containsPhoneNumber:phoneNumber];
}
-(void) addContactsToKnownWhisperUsers:(NSArray*) contacts {
for( Contact *contact in contacts){
[_knownWhisperUserIds addObject:@([contact recordID])];
}
NSMutableSet *users = [NSMutableSet setWithArray:[latestWhisperUsersById allValues]];
[users addObjectsFromArray:contacts];
[observableWhisperUsersController updateValue:[users allObjects]];
[self saveKnownWhisperUsers];
}
-(BOOL) knownUserStoreInitialized{
NSUserDefaults *d = [[NSUserDefaults standardUserDefaults] objectForKey:KNOWN_USERS_DEFAULT_KEY];
return (Nil != d);
}
-(NSMutableArray*) loadKnownWhisperUsers{
NSArray *knownUsers = [[NSUserDefaults standardUserDefaults] objectForKey:KNOWN_USERS_DEFAULT_KEY];
return knownUsers == nil ? [NSMutableArray array] : [knownUsers mutableCopy];
}
-(void) saveKnownWhisperUsers{
_knownWhisperUserIds = [NSMutableArray arrayWithArray:[latestWhisperUsersById allKeys]];
[[NSUserDefaults standardUserDefaults] setObject:[_knownWhisperUserIds copy] forKey:KNOWN_USERS_DEFAULT_KEY];
[[NSUserDefaults standardUserDefaults] synchronize];
}
-(void) clearKnownWhisUsers{
[[NSUserDefaults standardUserDefaults] setObject:[NSArray array] forKey:KNOWN_USERS_DEFAULT_KEY];
[[NSUserDefaults standardUserDefaults] synchronize];
}
@end