element-ios/matrixConsole/API/ContactManager.m
2015-04-17 14:24:08 +02:00

781 lines
30 KiB
Objective-C

/*
Copyright 2014 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "ContactManager.h"
#import "MXCContact.h"
#import "MXCPhoneNumber.h"
#import "MXCEmail.h"
#import "MXKAppSettings.h"
// warn when there is a contacts list refresh
NSString *const kContactManagerContactsListRefreshNotification = @"kContactManagerContactsListRefreshNotification";
// the phonenumber has been internationalized
NSString *const kContactsDidInternationalizeNotification = @"kContactsDidInternationalizeNotification";
// get the 3PIDS in one requests
//#define CONTACTS_3PIDS_SYNC 1
// else checks the matrix IDs for each displayed contact
@interface ContactManager() {
NSDate *lastSyncDate;
NSMutableDictionary* deviceContactByContactID;
//
NSMutableArray* pending3PIDs;
NSMutableArray* checked3PIDs;
NSMutableDictionary* matrixContactByMatrixUserID;
id matrixSessionStateObserver;
}
@end
@implementation ContactManager
@synthesize contacts;
#pragma mark Singleton Methods
static ContactManager* sharedContactManager = nil;
+ (id)sharedManager {
@synchronized(self) {
if(sharedContactManager == nil)
sharedContactManager = [[self alloc] init];
}
return sharedContactManager;
}
#pragma mark -
-(ContactManager *)init {
if (self = [super init]) {
NSString *label = [NSString stringWithFormat:@"ConsoleMatrix.%@.Contacts", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]];
processingQueue = dispatch_queue_create([label UTF8String], NULL);
// save the last sync date
// to avoid resync the whole phonebook
lastSyncDate = nil;
// Observe related settings change
[[MXKAppSettings standardAppSettings] addObserver:self forKeyPath:@"syncLocalContacts" options:0 context:nil];
}
return self;
}
-(void)dealloc {
if (matrixSessionStateObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:matrixSessionStateObserver];
matrixSessionStateObserver = nil;
}
[[MXKAppSettings standardAppSettings] removeObserver:self forKeyPath:@"syncLocalContacts"];
}
#pragma mark -
- (void)setMxSession:(MXSession *)session {
// Remove potential session observer
[[NSNotificationCenter defaultCenter] removeObserver:matrixSessionStateObserver];
if (session) {
// Register session state observer
matrixSessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
// Check whether the concerned session is the associated one
if (notif.object == _mxSession) {
[self didMatrixSessionStateChange];
}
}];
}
_mxSession = session;
// Force update
[self didMatrixSessionStateChange];
}
- (void)didMatrixSessionStateChange {
if (_mxSession && _mxSession.state == MXSessionStateRunning) {
[self manage3PIDS];
}
}
#pragma mark -
// delete contacts info
- (void)reset {
contacts = nil;
lastSyncDate = nil;
deviceContactByContactID = nil;
matrixContactByMatrixUserID = nil;
if (matrixSessionStateObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:matrixSessionStateObserver];
matrixSessionStateObserver = nil;
}
_mxSession = nil;
[self saveMatrixIDsDict];
[self saveDeviceContacts];
[self saveContactBookInfo];
// warn of the contacts list update
[[NSNotificationCenter defaultCenter] postNotificationName:kContactManagerContactsListRefreshNotification object:nil userInfo:nil];
}
// refresh the international phonenumber of the contacts
- (void)internationalizePhoneNumbers:(NSString*)countryCode {
dispatch_async(processingQueue, ^{
NSArray* contactsSnapshot = [deviceContactByContactID allValues];
for(MXCContact* contact in contactsSnapshot) {
[contact internationalizePhonenumbers:countryCode];
}
[self saveDeviceContacts];
[[NSNotificationCenter defaultCenter] postNotificationName:kContactsDidInternationalizeNotification object:nil userInfo:nil];
});
}
- (void)fullRefresh {
// check if the user allowed to sync local contacts
if (![[MXKAppSettings standardAppSettings] syncLocalContacts]) {
contacts = nil;
// if the user did not allow to sync local contacts
// ignore this sync
// at least, display the known contacts
[[NSNotificationCenter defaultCenter] postNotificationName:kContactManagerContactsListRefreshNotification object:nil userInfo:nil];
return;
}
// check if the application is allowed to list the contacts
ABAuthorizationStatus cbStatus = ABAddressBookGetAuthorizationStatus();
// did not yet request the access
if (cbStatus == kABAuthorizationStatusNotDetermined) {
// request address book access
ABAddressBookRef ab = ABAddressBookCreateWithOptions(nil, nil);
if (ab) {
ABAddressBookRequestAccessWithCompletion(ab, ^(bool granted, CFErrorRef error) {
dispatch_async(dispatch_get_main_queue(), ^{
[self fullRefresh];
});
});
CFRelease(ab);
}
return;
}
pending3PIDs = [[NSMutableArray alloc] init];
checked3PIDs = [[NSMutableArray alloc] init];
// cold start
// launch the dict from the file system
// It is cached to improve UX.
if (!matrixIDBy3PID) {
[self loadMatrixIDsDict];
}
dispatch_async(processingQueue, ^{
// in case of cold start
// get the info from the file system
if (!lastSyncDate) {
// load cached contacts
[self loadDeviceContacts];
[self loadContactBookInfo];
// no local contact -> assume that the last sync date is useless
if (deviceContactByContactID.count == 0) {
lastSyncDate = nil;
}
}
BOOL contactBookUpdate = NO;
NSMutableArray* deletedContactIDs = [[deviceContactByContactID allKeys] mutableCopy];
// can list tocal contacts
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
NSString* countryCode = [[MXKAppSettings standardAppSettings] phonebookCountryCode];
ABAddressBookRef ab = ABAddressBookCreateWithOptions(nil, nil);
ABRecordRef contactRecord;
CFIndex index;
CFMutableArrayRef people = (CFMutableArrayRef)ABAddressBookCopyArrayOfAllPeople(ab);
if (nil != people) {
CFIndex peopleCount = CFArrayGetCount(people);
for (index = 0; index < peopleCount; index++) {
contactRecord = (ABRecordRef)CFArrayGetValueAtIndex(people, index);
NSString* contactID = [MXCContact contactID:contactRecord];
// the contact still exists
[deletedContactIDs removeObject:contactID];
if (lastSyncDate) {
// ignore unchanged contacts since the previous sync
CFDateRef lastModifDate = ABRecordCopyValue(contactRecord, kABPersonModificationDateProperty);
if (kCFCompareGreaterThan != CFDateCompare (lastModifDate, (__bridge CFDateRef)lastSyncDate, nil))
{
CFRelease(lastModifDate);
continue;
}
CFRelease(lastModifDate);
}
contactBookUpdate = YES;
MXCContact* contact = [[MXCContact alloc] initWithABRecord:contactRecord];
if (countryCode) {
[contact internationalizePhonenumbers:countryCode];
}
// update the contact
[deviceContactByContactID setValue:contact forKey:contactID];;
}
CFRelease(people);
}
if (ab) {
CFRelease(ab);
}
}
// some contacts have been deleted
for (NSString* contactID in deletedContactIDs) {
contactBookUpdate = YES;
[deviceContactByContactID removeObjectForKey:contactID];
}
// something has been modified in the device contact book
if (contactBookUpdate) {
[self saveDeviceContacts];
}
lastSyncDate = [NSDate date];
[self saveContactBookInfo];
NSMutableArray* deviceContacts = [[deviceContactByContactID allValues] mutableCopy];
if (_mxSession && _mxSession.state == MXSessionStateRunning) {
[self manage3PIDS];
} else {
// display what you could have read
dispatch_async(dispatch_get_main_queue(), ^{
contacts = deviceContacts;
// at least, display the known contacts
[[NSNotificationCenter defaultCenter] postNotificationName:kContactManagerContactsListRefreshNotification object:nil userInfo:nil];
});
}
});
}
// the local contacts are listed
// update their 3PIDs and their update
- (void) manage3PIDS {
dispatch_async(processingQueue, ^{
NSMutableArray* tmpContacts = nil;
// update with the known dict 3PID -> matrix ID
[self updateMatrixIDDeviceContacts];
tmpContacts = [[deviceContactByContactID allValues] mutableCopy];
dispatch_async(dispatch_get_main_queue(), ^{
// stored self.contacts in the right thread
contacts = tmpContacts;
#if CONTACTS_3PIDS_SYNC
// refresh the 3PIDS -> matrix IDs
[self refreshMatrixIDs];
#else
// nop
// wait that refreshContactMatrixIDs is called
#endif
// at least, display the known contacts
[[NSNotificationCenter defaultCenter] postNotificationName:kContactManagerContactsListRefreshNotification object:nil userInfo:nil];
});
});
}
- (void) updateContactMatrixIDs:(MXCContact*) contact {
// the phonenumbers wil be managed later
/*for(ConsolePhoneNumber* pn in contact.phoneNumbers) {
if (pn.textNumber.length > 0) {
// not yet added
if ([pids indexOfObject:pn.textNumber] == NSNotFound) {
[pids addObject:pn.textNumber];
[medias addObject:@"msisdn"];
}
}
}*/
for(MXCEmail* email in contact.emailAddresses) {
if (email.emailAddress.length > 0) {
id matrixID = [matrixIDBy3PID valueForKey:email.emailAddress];
if ([matrixID isKindOfClass:[NSString class]]) {
dispatch_async(dispatch_get_main_queue(), ^{
[email setMatrixID:matrixID];
});
}
}
}
}
- (void) updateMatrixIDDeviceContacts {
NSArray* deviceContacts = [deviceContactByContactID allValues];
// update the contacts info
for(MXCContact* contact in deviceContacts) {
[self updateContactMatrixIDs:contact];
}
}
#ifdef CONTACTS_3PIDS_SYNC
// refresh the 3PIDs -> Matrix ID list
// update the contact is required
- (void)refreshMatrixIDs {
// build the request parameters
NSMutableArray* pids = [[NSMutableArray alloc] init];
NSMutableArray* medias = [[NSMutableArray alloc] init];
for(MXCContact* contact in deviceContactsList) {
// the phonenumbers are not managed
/*for(ConsolePhoneNumber* pn in contact.phoneNumbers) {
if (pn.textNumber.length > 0) {
// not yet added
if ([pids indexOfObject:pn.textNumber] == NSNotFound) {
[pids addObject:pn.textNumber];
[medias addObject:@"msisdn"];
}
}
}*/
for(MXCEmail* email in contact.emailAddresses) {
if (email.emailAddress.length > 0) {
// not yet added
if ([pids indexOfObject:email.emailAddress] == NSNotFound) {
[pids addObject:email.emailAddress];
[medias addObject:@"email"];
}
}
}
}
// get some pids
if (pids.count > 0) {
// Select the right restClient
MXRestClient *restClient = _mxRestClient;
if (_mxSession) {
restClient = _mxSession.matrixRestClient;
}
if (restClient) {
[restClient lookup3pids:pids
forMedia:medias
success:^(NSArray *userIds) {
// sanity check
if (userIds.count == pids.count) {
matrixIDBy3PID = [[NSMutableDictionary alloc] initWithObjects:userIds forKeys:pids];
[self saveMatrixIDsDict];
[self updateMatrixIDDeviceContactsList];
// add the MX users
NSMutableArray* tmpContacts = [deviceContactsList mutableCopy];
[self mergeMXUsers:tmpContacts];
dispatch_async(dispatch_get_main_queue(), ^{
contacts = tmpContacts;
[[NSNotificationCenter defaultCenter] postNotificationName:kContactManagerContactsListRefreshNotification object:nil userInfo:nil];
});
}
}
failure:^(NSError *error) {
// try later
dispatch_after(dispatch_walltime(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self refreshMatrixIDs];
});
}
];
}
}
}
#endif
// refresh matrix IDs
- (void)refreshContactMatrixIDs:(MXCContact*)contact {
#ifndef CONTACTS_3PIDS_SYNC
if (!contact.isMatrixContact) {
// check pending requests
NSMutableArray* pids = [[NSMutableArray alloc] init];
NSMutableArray* medias = [[NSMutableArray alloc] init];
for(MXCEmail* email in contact.emailAddresses) {
if (([pending3PIDs indexOfObject:email.emailAddress] == NSNotFound) && ([checked3PIDs indexOfObject:email.emailAddress] == NSNotFound)) {
[pids addObject:email.emailAddress];
[medias addObject:@"email"];
}
}
if (pids.count > 0) {
[pending3PIDs addObjectsFromArray:pids];
// Select the right restClient
MXRestClient *restClient = _mxRestClient;
if (_mxSession) {
restClient = _mxSession.matrixRestClient;
}
if (restClient) {
[restClient lookup3pids:pids
forMedia:medias
success:^(NSArray *userIds) {
// sanity check
if (userIds.count == pids.count) {
// update statuses table
[checked3PIDs addObjectsFromArray:pids];
for(NSString* pid in pids) {
[pending3PIDs removeObject:pid];
}
BOOL isUpdated = NO;
NSMutableArray* matrixContactsToRemove = [[NSMutableArray alloc] init];
// apply updates
if (pids.count > 0) {
for(int index = 0; index < pids.count; index++) {
NSString* matrixID = [userIds objectAtIndex:index];
NSString* pid = [pids objectAtIndex:index];
// the dict is created on demand
if (!matrixIDBy3PID) {
[self loadMatrixIDsDict];
}
id currentMatrixID = [matrixIDBy3PID valueForKey:pid];
// do not keep useless info
if ([matrixID isKindOfClass:[NSString class]]) {
// do not update if not required
if (![currentMatrixID isKindOfClass:[NSString class]] || ![(NSString*)currentMatrixID isEqualToString:matrixID]) {
[matrixIDBy3PID setValue:matrixID forKey:pid];
isUpdated = YES;
}
} else {
if (currentMatrixID) {
[matrixIDBy3PID removeObjectForKey:pid];
isUpdated = YES;
}
}
// is there a matrix contact with the same
if ([matrixContactByMatrixUserID objectForKey:matrixID]) {
[matrixContactsToRemove addObject:[matrixContactByMatrixUserID objectForKey:matrixID]];
}
}
if (isUpdated) {
[self saveMatrixIDsDict];
}
}
// some matrix contacts will be replaced by this contact
if (matrixContactsToRemove.count > 0) {
[self updateContactMatrixIDs:contact];
for(MXCContact* contactToRemove in matrixContactsToRemove) {
[self.contacts removeObject:contactToRemove];
}
// warn there is a global refresh
[[NSNotificationCenter defaultCenter] postNotificationName:kContactManagerContactsListRefreshNotification object:nil userInfo:nil];
} else {
// update only this contact
[self updateContactMatrixIDs:contact];
}
}
}
failure:^(NSError *error) {
// try later
dispatch_after(dispatch_walltime(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self refreshContactMatrixIDs:contact];
});
}];
}
else {
dispatch_after(dispatch_walltime(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[self refreshContactMatrixIDs:contact];
});
}
}
}
#endif
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if ([@"syncLocalContacts" isEqualToString:keyPath]) {
dispatch_async(dispatch_get_main_queue(), ^{
[self fullRefresh];
});
}
}
- (SectionedContacts *)getSectionedContacts:(NSArray*)contactsList {
UILocalizedIndexedCollation *collation = [UILocalizedIndexedCollation currentCollation];
int indexOffset = 0;
NSInteger index, sectionTitlesCount = [[collation sectionTitles] count];
NSMutableArray *tmpSectionsArray = [[NSMutableArray alloc] initWithCapacity:(sectionTitlesCount)];
sectionTitlesCount += indexOffset;
for (index = 0; index < sectionTitlesCount; index++) {
NSMutableArray *array = [[NSMutableArray alloc] init];
[tmpSectionsArray addObject:array];
}
int contactsCount = 0;
for (MXCContact *aContact in contactsList)
{
NSInteger section = [collation sectionForObject:aContact collationStringSelector:@selector(displayName)] + indexOffset;
[[tmpSectionsArray objectAtIndex:section] addObject:aContact];
++contactsCount;
}
NSMutableArray *tmpSectionedContactsTitle = [[NSMutableArray alloc] initWithCapacity:sectionTitlesCount];
NSMutableArray *shortSectionsArray = [[NSMutableArray alloc] initWithCapacity:sectionTitlesCount];
for (index = indexOffset; index < sectionTitlesCount; index++) {
NSMutableArray *usersArrayForSection = [tmpSectionsArray objectAtIndex:index];
if ([usersArrayForSection count] != 0) {
NSArray* sortedUsersArrayForSection = [collation sortedArrayFromArray:usersArrayForSection collationStringSelector:@selector(displayName)];
[shortSectionsArray addObject:sortedUsersArrayForSection];
[tmpSectionedContactsTitle addObject:[[[UILocalizedIndexedCollation currentCollation] sectionTitles] objectAtIndex:(index - indexOffset)]];
}
}
return [[SectionedContacts alloc] initWithContacts:shortSectionsArray andTitles:tmpSectionedContactsTitle andCount:contactsCount];
}
#pragma mark - file caches
static NSString *matrixIDsDictFile = @"matrixIDsDict";
static NSString *localContactsFile = @"localContacts";
static NSString *contactsBookInfoFile = @"contacts";
- (void)saveMatrixIDsDict
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataFilePath = [documentsDirectory stringByAppendingPathComponent:matrixIDsDictFile];
if (matrixIDBy3PID)
{
NSMutableData *theData = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:theData];
[encoder encodeObject:matrixIDBy3PID forKey:@"matrixIDsDict"];
[encoder finishEncoding];
[theData writeToFile:dataFilePath atomically:YES];
}
else
{
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtPath:dataFilePath error:nil];
}
}
- (void)loadMatrixIDsDict
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataFilePath = [documentsDirectory stringByAppendingPathComponent:matrixIDsDictFile];
NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([fileManager fileExistsAtPath:dataFilePath])
{
// the file content could be corrupted
@try {
NSData* filecontent = [NSData dataWithContentsOfFile:dataFilePath options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:nil];
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:filecontent];
id object = [decoder decodeObjectForKey:@"matrixIDsDict"];
if ([object isKindOfClass:[NSDictionary class]]) {
matrixIDBy3PID = [object mutableCopy];
}
[decoder finishDecoding];
} @catch (NSException *exception) {
}
}
if (!matrixIDBy3PID) {
matrixIDBy3PID = [[NSMutableDictionary alloc] init];
}
}
- (void) saveDeviceContacts {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataFilePath = [documentsDirectory stringByAppendingPathComponent:localContactsFile];
if (deviceContactByContactID && (deviceContactByContactID.count > 0))
{
NSMutableData *theData = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:theData];
[encoder encodeObject:deviceContactByContactID forKey:@"deviceContactByContactID"];
[encoder finishEncoding];
[theData writeToFile:dataFilePath atomically:YES];
}
else
{
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtPath:dataFilePath error:nil];
}
}
- (void) loadDeviceContacts {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataFilePath = [documentsDirectory stringByAppendingPathComponent:localContactsFile];
NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([fileManager fileExistsAtPath:dataFilePath])
{
// the file content could be corrupted
@try {
NSData* filecontent = [NSData dataWithContentsOfFile:dataFilePath options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:nil];
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:filecontent];
id object = [decoder decodeObjectForKey:@"deviceContactByContactID"];
if ([object isKindOfClass:[NSDictionary class]]) {
deviceContactByContactID = [object mutableCopy];
}
[decoder finishDecoding];
} @catch (NSException *exception) {
lastSyncDate = nil;
}
}
if (!deviceContactByContactID) {
deviceContactByContactID = [[NSMutableDictionary alloc] init];
}
}
- (void) saveContactBookInfo {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataFilePath = [documentsDirectory stringByAppendingPathComponent:contactsBookInfoFile];
if (lastSyncDate)
{
NSMutableData *theData = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:theData];
[encoder encodeObject:lastSyncDate forKey:@"lastSyncDate"];
[encoder finishEncoding];
[theData writeToFile:dataFilePath atomically:YES];
}
else
{
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtPath:dataFilePath error:nil];
}
}
- (void) loadContactBookInfo {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *dataFilePath = [documentsDirectory stringByAppendingPathComponent:contactsBookInfoFile];
NSFileManager *fileManager = [[NSFileManager alloc] init];
if ([fileManager fileExistsAtPath:dataFilePath])
{
// the file content could be corrupted
@try {
NSData* filecontent = [NSData dataWithContentsOfFile:dataFilePath options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:nil];
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:filecontent];
lastSyncDate = [decoder decodeObjectForKey:@"lastSyncDate"];
[decoder finishDecoding];
} @catch (NSException *exception) {
lastSyncDate = nil;
}
}
}
@end