element-ios/Riot/Modules/MatrixKit/Models/Contact/MXKContactManager.m

1944 lines
70 KiB
Objective-C

/*
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C
Copyright 2017 Vector Creations Ltd
Copyright 2015 OpenMarket Ltd
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
*/
#import "MXKContactManager.h"
#import "MXKContact.h"
#import "MXKAppSettings.h"
#import "MXKTools.h"
#import "NSBundle+MatrixKit.h"
#import <MatrixSDK/MXAes.h>
#import <MatrixSDK/MXRestClient.h>
#import <MatrixSDK/MXKeyProvider.h>
#import "MXKSwiftHeader.h"
NSString *const kMXKContactManagerDidUpdateMatrixContactsNotification = @"kMXKContactManagerDidUpdateMatrixContactsNotification";
NSString *const kMXKContactManagerDidUpdateLocalContactsNotification = @"kMXKContactManagerDidUpdateLocalContactsNotification";
NSString *const kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification = @"kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification";
NSString *const kMXKContactManagerMatrixUserPresenceChangeNotification = @"kMXKContactManagerMatrixUserPresenceChangeNotification";
NSString *const kMXKContactManagerMatrixPresenceKey = @"kMXKContactManagerMatrixPresenceKey";
NSString *const kMXKContactManagerDidInternationalizeNotification = @"kMXKContactManagerDidInternationalizeNotification";
NSString *const MXKContactManagerDataType = @"org.matrix.kit.MXKContactManagerDataType";
@interface MXKContactManager()
{
/**
Array of `MXSession` instances.
*/
NSMutableArray *mxSessionArray;
id mxSessionStateObserver;
id mxSessionNewSyncedRoomObserver;
/**
Listeners registered on matrix presence and membership events (one by matrix session)
*/
NSMutableArray *mxEventListeners;
/**
Local contacts handling
*/
BOOL isLocalContactListRefreshing;
dispatch_queue_t processingQueue;
NSDate *lastSyncDate;
// Local contacts by contact Id
NSMutableDictionary* localContactByContactID;
NSMutableArray* localContactsWithMethods;
NSMutableArray* splitLocalContacts;
// Matrix id linked to 3PID.
NSMutableDictionary<NSString*, NSString*> *matrixIDBy3PID;
/**
Matrix contacts handling
*/
// Matrix contacts by contact Id
NSMutableDictionary* matrixContactByContactID;
// Matrix contacts by matrix id
NSMutableDictionary* matrixContactByMatrixID;
}
@end
@implementation MXKContactManager
@synthesize contactManagerMXRoomSource;
#pragma mark Singleton Methods
+ (instancetype)sharedManager
{
static MXKContactManager *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[MXKContactManager alloc] init];
});
return sharedInstance;
}
#pragma mark -
-(MXKContactManager *)init
{
if (self = [super init])
{
NSString *label = [NSString stringWithFormat:@"MatrixKit.%@.Contacts", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]];
[self deleteOldFiles];
processingQueue = dispatch_queue_create([label UTF8String], DISPATCH_QUEUE_SERIAL);
// save the last sync date
// to avoid resync the whole phonebook
lastSyncDate = nil;
self.contactManagerMXRoomSource = MXKContactManagerMXRoomSourceDirectChats;
// Observe related settings change
[[MXKAppSettings standardAppSettings] addObserver:self forKeyPath:@"syncLocalContacts" options:0 context:nil];
[[MXKAppSettings standardAppSettings] addObserver:self forKeyPath:@"phonebookCountryCode" options:0 context:nil];
[self registerAccountDataDidChangeIdentityServerNotification];
self.allowLocalContactsAccess = YES;
}
return self;
}
-(void)dealloc
{
matrixIDBy3PID = nil;
localContactByContactID = nil;
localContactsWithMethods = nil;
splitLocalContacts = nil;
matrixContactByContactID = nil;
matrixContactByMatrixID = nil;
lastSyncDate = nil;
while (mxSessionArray.count) {
[self removeMatrixSession:mxSessionArray.lastObject];
}
mxSessionArray = nil;
mxEventListeners = nil;
[[MXKAppSettings standardAppSettings] removeObserver:self forKeyPath:@"syncLocalContacts"];
[[MXKAppSettings standardAppSettings] removeObserver:self forKeyPath:@"phonebookCountryCode"];
processingQueue = nil;
}
#pragma mark -
- (void)addMatrixSession:(MXSession*)mxSession
{
if (!mxSessionArray)
{
mxSessionArray = [NSMutableArray array];
}
if (!mxEventListeners)
{
mxEventListeners = [NSMutableArray array];
}
if ([mxSessionArray indexOfObject:mxSession] == NSNotFound)
{
[mxSessionArray addObject:mxSession];
MXWeakify(self);
// Register a listener on matrix presence and membership events
id eventListener = [mxSession listenToEventsOfTypes:@[kMXEventTypeStringRoomMember, kMXEventTypeStringPresence]
onEvent:^(MXEvent *event, MXTimelineDirection direction, id customObject) {
MXStrongifyAndReturnIfNil(self);
// Consider only live event
if (direction == MXTimelineDirectionForwards)
{
// Consider first presence events
if (event.eventType == MXEventTypePresence)
{
// Check whether the concerned matrix user belongs to at least one contact.
BOOL isMatched = ([self->matrixContactByMatrixID objectForKey:event.sender] != nil);
if (!isMatched)
{
NSArray *matrixIDs = [self->matrixIDBy3PID allValues];
isMatched = ([matrixIDs indexOfObject:event.sender] != NSNotFound);
}
if (isMatched) {
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerMatrixUserPresenceChangeNotification object:event.sender userInfo:@{kMXKContactManagerMatrixPresenceKey:event.content[@"presence"]}];
}
}
// Else the event type is MXEventTypeRoomMember.
// Ignore here membership events if the session is not running yet,
// Indeed all the contacts are refreshed when session state becomes running.
else if (mxSession.state == MXSessionStateRunning)
{
// Update matrix contact list on membership change
[self updateMatrixContactWithID:event.sender];
}
}
}];
[mxEventListeners addObject:eventListener];
// Update matrix contact list in case of new synced one-to-one room
if (!mxSessionNewSyncedRoomObserver)
{
mxSessionNewSyncedRoomObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXRoomInitialSyncNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXStrongifyAndReturnIfNil(self);
// create contact for known room members
if (self.contactManagerMXRoomSource != MXKContactManagerMXRoomSourceNone)
{
MXRoom *room = notif.object;
[room state:^(MXRoomState *roomState) {
MXRoomMembers *roomMembers = roomState.members;
NSArray *members = roomMembers.members;
// Consider only 1:1 chat for MXKMemberContactCreationOneToOneRoom
// or adding all
if (((members.count == 2) && (self.contactManagerMXRoomSource == MXKContactManagerMXRoomSourceDirectChats)) || (self.contactManagerMXRoomSource == MXKContactManagerMXRoomSourceAll))
{
NSString* myUserId = room.mxSession.myUser.userId;
for (MXRoomMember* member in members)
{
if ([member.userId isEqualToString:myUserId])
{
[self updateMatrixContactWithID:member.userId];
}
}
}
}];
}
}];
}
// Update all matrix contacts as soon as matrix session is ready
if (!mxSessionStateObserver) {
mxSessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
MXStrongifyAndReturnIfNil(self);
MXSession *mxSession = notif.object;
if ([self->mxSessionArray indexOfObject:mxSession] != NSNotFound)
{
if ((mxSession.state == MXSessionStateStoreDataReady) || (mxSession.state == MXSessionStateRunning)) {
[self refreshMatrixContacts];
}
}
}];
}
// refreshMatrixContacts can take time. Delay its execution to not overload
// launch of apps that call [MXKContactManager addMatrixSession] at startup
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshMatrixContacts];
});
}
}
- (void)removeMatrixSession:(MXSession*)mxSession
{
NSUInteger index = [mxSessionArray indexOfObject:mxSession];
if (index != NSNotFound)
{
id eventListener = [mxEventListeners objectAtIndex:index];
[mxSession removeListener:eventListener];
[mxEventListeners removeObjectAtIndex:index];
[mxSessionArray removeObjectAtIndex:index];
if (!mxSessionArray.count) {
if (mxSessionStateObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:mxSessionStateObserver];
mxSessionStateObserver = nil;
}
if (mxSessionNewSyncedRoomObserver) {
[[NSNotificationCenter defaultCenter] removeObserver:mxSessionNewSyncedRoomObserver];
mxSessionNewSyncedRoomObserver = nil;
}
}
// Update matrix contacts list
[self refreshMatrixContacts];
}
}
- (NSArray*)mxSessions
{
return [NSArray arrayWithArray:mxSessionArray];
}
- (NSArray*)matrixContacts
{
NSParameterAssert([NSThread isMainThread]);
return [matrixContactByContactID allValues];
}
- (NSArray*)localContacts
{
NSParameterAssert([NSThread isMainThread]);
// Return nil if the loading step is in progress.
if (isLocalContactListRefreshing)
{
return nil;
}
return [localContactByContactID allValues];
}
- (NSArray*)localContactsWithMethods
{
NSParameterAssert([NSThread isMainThread]);
// Return nil if the loading step is in progress.
if (isLocalContactListRefreshing)
{
return nil;
}
// Check whether the array must be prepared
if (!localContactsWithMethods)
{
// List all the local contacts with emails and/or phones
NSArray *localContacts = self.localContacts;
localContactsWithMethods = [NSMutableArray arrayWithCapacity:localContacts.count];
for (MXKContact* contact in localContacts)
{
if (contact.emailAddresses)
{
[localContactsWithMethods addObject:contact];
}
else if (contact.phoneNumbers)
{
[localContactsWithMethods addObject:contact];
}
}
}
return localContactsWithMethods;
}
- (NSArray*)localContactsSplitByContactMethod
{
NSParameterAssert([NSThread isMainThread]);
// Return nil if the loading step is in progress.
if (isLocalContactListRefreshing)
{
return nil;
}
// Check whether the array must be prepared
if (!splitLocalContacts)
{
// List all the local contacts with contact methods
NSArray *contactsArray = self.localContactsWithMethods;
splitLocalContacts = [NSMutableArray arrayWithCapacity:contactsArray.count];
for (MXKContact* contact in contactsArray)
{
NSArray *emails = contact.emailAddresses;
NSArray *phones = contact.phoneNumbers;
if (emails.count + phones.count > 1)
{
for (MXKEmail *email in emails)
{
MXKContact *splitContact = [[MXKContact alloc] initContactWithDisplayName:contact.displayName emails:@[email] phoneNumbers:nil andThumbnail:contact.thumbnail];
[splitLocalContacts addObject:splitContact];
}
for (MXKPhoneNumber *phone in phones)
{
MXKContact *splitContact = [[MXKContact alloc] initContactWithDisplayName:contact.displayName emails:nil phoneNumbers:@[phone] andThumbnail:contact.thumbnail];
[splitLocalContacts addObject:splitContact];
}
}
else if (emails.count + phones.count)
{
[splitLocalContacts addObject:contact];
}
}
// Sort alphabetically the resulting list
[self sortAlphabeticallyContacts:splitLocalContacts];
}
return splitLocalContacts;
}
//- (void)localContactsSplitByContactMethod:(void (^)(NSArray<MXKContact*> *localContactsSplitByContactMethod))onComplete
//{
// NSParameterAssert([NSThread isMainThread]);
//
// // Return nil if the loading step is in progress.
// if (isLocalContactListRefreshing)
// {
// onComplete(nil);
// return;
// }
//
// // Check whether the array must be prepared
// if (!splitLocalContacts)
// {
// // List all the local contacts with contact methods
// NSArray *contactsArray = self.localContactsWithMethods;
//
// splitLocalContacts = [NSMutableArray arrayWithCapacity:contactsArray.count];
//
// for (MXKContact* contact in contactsArray)
// {
// NSArray *emails = contact.emailAddresses;
// NSArray *phones = contact.phoneNumbers;
//
// if (emails.count + phones.count > 1)
// {
// for (MXKEmail *email in emails)
// {
// MXKContact *splitContact = [[MXKContact alloc] initContactWithDisplayName:contact.displayName emails:@[email] phoneNumbers:nil andThumbnail:contact.thumbnail];
// [splitLocalContacts addObject:splitContact];
// }
//
// for (MXKPhoneNumber *phone in phones)
// {
// MXKContact *splitContact = [[MXKContact alloc] initContactWithDisplayName:contact.displayName emails:nil phoneNumbers:@[phone] andThumbnail:contact.thumbnail];
// [splitLocalContacts addObject:splitContact];
// }
// }
// else if (emails.count + phones.count)
// {
// [splitLocalContacts addObject:contact];
// }
// }
//
// // Sort alphabetically the resulting list
// [self sortAlphabeticallyContacts:splitLocalContacts];
// }
//
// onComplete(splitLocalContacts);
//}
- (NSArray*)directMatrixContacts
{
NSParameterAssert([NSThread isMainThread]);
NSMutableDictionary *directContacts = [NSMutableDictionary dictionary];
NSArray *mxSessions = self.mxSessions;
for (MXSession *mxSession in mxSessions)
{
// Check all existing users for whom a direct chat exists
NSArray *mxUserIds = mxSession.directRooms.allKeys;
for (NSString *mxUserId in mxUserIds)
{
MXKContact* contact = [matrixContactByMatrixID objectForKey:mxUserId];
// Sanity check - the contact must be already defined here
if (contact)
{
[directContacts setValue:contact forKey:mxUserId];
}
}
}
return directContacts.allValues;
}
// The current identity service used with the contact manager
- (MXIdentityService*)identityService
{
// For the moment, only use the one of the first session
MXSession *mxSession = [mxSessionArray firstObject];
return mxSession.identityService;
}
- (BOOL)isUsersDiscoveringEnabled
{
// Check whether the 3pid lookup is available
return (self.discoverUsersBoundTo3PIDsBlock || self.identityService);
}
#pragma mark -
- (void)validateSyncLocalContactsStateForSession:(MXSession *)mxSession
{
if (!self.allowLocalContactsAccess)
{
return;
}
// Get the status of the identity service terms.
BOOL areAllTermsAgreed = mxSession.identityService.areAllTermsAgreed;
if (MXKAppSettings.standardAppSettings.syncLocalContacts)
{
// Disable local contact sync when all terms are no longer accepted or if contacts access has been revoked.
if (!areAllTermsAgreed || [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] != CNAuthorizationStatusAuthorized)
{
MXLogDebug(@"[MXKContactManager] validateSyncLocalContactsState : Disabling contacts sync.");
MXKAppSettings.standardAppSettings.syncLocalContacts = false;
return;
}
}
else
{
// Check whether the user has been directed to the Settings app to enable contact access.
if (MXKAppSettings.standardAppSettings.syncLocalContactsPermissionOpenedSystemSettings)
{
// Reset the system settings app flag as they are back in the app.
MXKAppSettings.standardAppSettings.syncLocalContactsPermissionOpenedSystemSettings = false;
// And if all other conditions are met for contacts sync enable it.
if (areAllTermsAgreed && [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusAuthorized)
{
MXLogDebug(@"[MXKContactManager] validateSyncLocalContactsState : Enabling contacts sync after user visited Settings app.");
MXKAppSettings.standardAppSettings.syncLocalContacts = true;
}
}
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
- (void)refreshLocalContacts
{
MXLogDebug(@"[MXKContactManager] refreshLocalContacts : Started");
if (!self.allowLocalContactsAccess)
{
MXLogDebug(@"[MXKContactManager] refreshLocalContacts : Finished because local contacts access not allowed.");
return;
}
NSDate *startDate = [NSDate date];
if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] != CNAuthorizationStatusAuthorized)
{
if ([MXKAppSettings standardAppSettings].syncLocalContacts)
{
// The user authorised syncLocalContacts and allowed access to his contacts
// but he then removed contacts access from app permissions.
// So, reset syncLocalContacts value
[MXKAppSettings standardAppSettings].syncLocalContacts = NO;
}
// Local contacts list is empty if the access is denied.
self->localContactByContactID = nil;
self->localContactsWithMethods = nil;
self->splitLocalContacts = nil;
[self cacheLocalContacts];
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateLocalContactsNotification object:nil userInfo:nil];
MXLogDebug(@"[MXKContactManager] refreshLocalContacts : Complete");
MXLogDebug(@"[MXKContactManager] refreshLocalContacts : Local contacts access denied");
}
else
{
self->isLocalContactListRefreshing = YES;
// Reset the internal contact lists (These arrays will be prepared only if need).
self->localContactsWithMethods = self->splitLocalContacts = nil;
BOOL isColdStart = NO;
// Check whether the local contacts sync has been disabled.
if (self->matrixIDBy3PID && ![MXKAppSettings standardAppSettings].syncLocalContacts)
{
// The user changed his mind and disabled the local contact sync, remove the cached data.
self->matrixIDBy3PID = nil;
[self cacheMatrixIDsDict];
// Reload the local contacts from the system
self->localContactByContactID = nil;
[self cacheLocalContacts];
}
// Check whether this is a cold start.
if (!self->matrixIDBy3PID)
{
isColdStart = YES;
// Load the dictionary from the file system. It is cached to improve UX.
[self loadCachedMatrixIDsDict];
}
MXWeakify(self);
dispatch_async(self->processingQueue, ^{
MXStrongifyAndReturnIfNil(self);
// In case of cold start, retrieve the data from the file system
if (isColdStart)
{
[self loadCachedLocalContacts];
[self loadCachedContactBookInfo];
// no local contact -> assume that the last sync date is useless
if (self->localContactByContactID.count == 0)
{
self->lastSyncDate = nil;
}
}
BOOL didContactBookChange = NO;
NSMutableArray* deletedContactIDs = [NSMutableArray arrayWithArray:[self->localContactByContactID allKeys]];
// can list local 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 = [MXKContact contactID:contactRecord];
// the contact still exists
[deletedContactIDs removeObject:contactID];
if (self->lastSyncDate)
{
// ignore unchanged contacts since the previous sync
CFDateRef lastModifDate = ABRecordCopyValue(contactRecord, kABPersonModificationDateProperty);
if (lastModifDate)
{
if (kCFCompareGreaterThan != CFDateCompare(lastModifDate, (__bridge CFDateRef)self->lastSyncDate, nil))
{
CFRelease(lastModifDate);
continue;
}
CFRelease(lastModifDate);
}
}
didContactBookChange = YES;
MXKContact* contact = [[MXKContact alloc] initLocalContactWithABRecord:contactRecord];
if (countryCode)
{
contact.defaultCountryCode = countryCode;
}
// update the local contacts list
[self->localContactByContactID setValue:contact forKey:contactID];
}
CFRelease(people);
}
if (ab)
{
CFRelease(ab);
}
}
// some contacts have been deleted
for (NSString* contactID in deletedContactIDs)
{
didContactBookChange = YES;
[self->localContactByContactID removeObjectForKey:contactID];
}
// something has been modified in the local contact book
if (didContactBookChange)
{
[self cacheLocalContacts];
}
self->lastSyncDate = [NSDate date];
[self cacheContactBookInfo];
// Update loaded contacts with the known dict 3PID -> matrix ID
[self updateAllLocalContactsMatrixIDs];
dispatch_async(dispatch_get_main_queue(), ^{
// Contacts are loaded, post a notification
self->isLocalContactListRefreshing = NO;
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateLocalContactsNotification object:nil userInfo:nil];
// Check the conditions required before triggering a matrix users lookup.
if (isColdStart || didContactBookChange)
{
[self updateMatrixIDsForAllLocalContacts];
}
MXLogDebug(@"[MXKContactManager] refreshLocalContacts : Complete");
MXLogDebug(@"[MXKContactManager] refreshLocalContacts : Refresh %tu local contacts in %.0fms", self->localContactByContactID.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
});
});
}
}
#pragma clang diagnostic pop
- (void)updateMatrixIDsForLocalContact:(MXKContact *)contact
{
// Check if the user allowed to sync local contacts.
// + Check whether users discovering is available.
if ([MXKAppSettings standardAppSettings].syncLocalContacts && !contact.isMatrixContact && [self isUsersDiscoveringEnabled])
{
// Retrieve all 3PIDs of the contact
NSMutableArray* threepids = [[NSMutableArray alloc] init];
NSMutableArray* lookup3pidsArray = [[NSMutableArray alloc] init];
for (MXKEmail* email in contact.emailAddresses)
{
// Not yet added
if (email.emailAddress.length && [threepids indexOfObject:email.emailAddress] == NSNotFound)
{
[lookup3pidsArray addObject:@[kMX3PIDMediumEmail, email.emailAddress]];
[threepids addObject:email.emailAddress];
}
}
for (MXKPhoneNumber* phone in contact.phoneNumbers)
{
if (phone.msisdn)
{
[lookup3pidsArray addObject:@[kMX3PIDMediumMSISDN, phone.msisdn]];
[threepids addObject:phone.msisdn];
}
}
if (lookup3pidsArray.count > 0)
{
MXWeakify(self);
void (^success)(NSArray<NSArray<NSString *> *> *) = ^(NSArray<NSArray<NSString *> *> *discoveredUsers) {
MXStrongifyAndReturnIfNil(self);
// Look for updates
BOOL isUpdated = NO;
// Consider each discored user
for (NSArray *discoveredUser in discoveredUsers)
{
// Sanity check
if (discoveredUser.count == 3)
{
NSString *pid = discoveredUser[1];
NSString *matrixId = discoveredUser[2];
// Remove the 3pid from the requested list
[threepids removeObject:pid];
NSString *currentMatrixID = [self->matrixIDBy3PID objectForKey:pid];
if (![currentMatrixID isEqualToString:matrixId])
{
[self->matrixIDBy3PID setObject:matrixId forKey:pid];
isUpdated = YES;
}
}
}
// Remove existing information which is not valid anymore
for (NSString *pid in threepids)
{
if ([self->matrixIDBy3PID objectForKey:pid])
{
[self->matrixIDBy3PID removeObjectForKey:pid];
isUpdated = YES;
}
}
if (isUpdated)
{
[self cacheMatrixIDsDict];
// Update only this contact
[self updateLocalContactMatrixIDs:contact];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:contact.contactID userInfo:nil];
});
}
};
void (^failure)(NSError *) = ^(NSError *error) {
MXLogDebug(@"[MXKContactManager] updateMatrixIDsForLocalContact failed");
};
if (self.discoverUsersBoundTo3PIDsBlock)
{
self.discoverUsersBoundTo3PIDsBlock(lookup3pidsArray, success, failure);
}
else
{
// Consider the potential identity server url by default
[self.identityService lookup3pids:lookup3pidsArray
success:success
failure:failure];
}
}
}
}
- (void)updateMatrixIDsForAllLocalContacts
{
// If localContactByContactID is not loaded, the manager will consider there is no local contacts
// and will reset its cache
NSAssert(localContactByContactID, @"[MXKContactManager] updateMatrixIDsForAllLocalContacts: refreshLocalContacts must be called before");
// Check if the user allowed to sync local contacts.
// + Check if at least an identity server is available, and if the loading step is not in progress.
if (![MXKAppSettings standardAppSettings].syncLocalContacts || ![self isUsersDiscoveringEnabled] || isLocalContactListRefreshing)
{
return;
}
MXWeakify(self);
// Refresh the 3PIDs -> Matrix ID mapping
dispatch_async(processingQueue, ^{
MXStrongifyAndReturnIfNil(self);
NSArray* contactsSnapshot = [self->localContactByContactID allValues];
// Retrieve all 3PIDs
NSMutableArray* threepids = [[NSMutableArray alloc] init];
NSMutableArray* lookup3pidsArray = [[NSMutableArray alloc] init];
for (MXKContact* contact in contactsSnapshot)
{
for (MXKEmail* email in contact.emailAddresses)
{
// Not yet added
if (email.emailAddress.length && [threepids indexOfObject:email.emailAddress] == NSNotFound)
{
[lookup3pidsArray addObject:@[kMX3PIDMediumEmail, email.emailAddress]];
[threepids addObject:email.emailAddress];
}
}
for (MXKPhoneNumber* phone in contact.phoneNumbers)
{
if (phone.msisdn)
{
// Not yet added
if ([threepids indexOfObject:phone.msisdn] == NSNotFound)
{
[lookup3pidsArray addObject:@[kMX3PIDMediumMSISDN, phone.msisdn]];
[threepids addObject:phone.msisdn];
}
}
}
}
// Update 3PIDs mapping
if (lookup3pidsArray.count > 0)
{
MXWeakify(self);
void (^success)(NSArray<NSArray<NSString *> *> *) = ^(NSArray<NSArray<NSString *> *> *discoveredUsers) {
MXStrongifyAndReturnIfNil(self);
[threepids removeAllObjects];
NSMutableArray* userIds = [[NSMutableArray alloc] init];
// Consider each discored user
for (NSArray *discoveredUser in discoveredUsers)
{
// Sanity check
if (discoveredUser.count == 3)
{
id threepid = discoveredUser[1];
id userId = discoveredUser[2];
if ([threepid isKindOfClass:[NSString class]] && [userId isKindOfClass:[NSString class]])
{
[threepids addObject:threepid];
[userIds addObject:userId];
}
}
}
if (userIds.count)
{
self->matrixIDBy3PID = [[NSMutableDictionary alloc] initWithObjects:userIds forKeys:threepids];
}
else
{
self->matrixIDBy3PID = nil;
}
[self cacheMatrixIDsDict];
[self updateAllLocalContactsMatrixIDs];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:nil userInfo:nil];
});
};
void (^failure)(NSError *) = ^(NSError *error) {
MXLogDebug(@"[MXKContactManager] updateMatrixIDsForAllLocalContacts failed");
};
if (self.discoverUsersBoundTo3PIDsBlock)
{
self.discoverUsersBoundTo3PIDsBlock(lookup3pidsArray, success, failure);
}
else if (self.identityService)
{
[self.identityService lookup3pids:lookup3pidsArray
success:success
failure:failure];
}
else
{
// No IS, no detection of Matrix users in local contacts
self->matrixIDBy3PID = nil;
[self cacheMatrixIDsDict];
}
}
else
{
self->matrixIDBy3PID = nil;
[self cacheMatrixIDsDict];
}
});
}
- (void)resetMatrixIDs
{
dispatch_async(processingQueue, ^{
self->matrixIDBy3PID = nil;
[self cacheMatrixIDsDict];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateLocalContactMatrixIDsNotification object:nil userInfo:nil];
});
});
}
- (void)reset
{
matrixIDBy3PID = nil;
[self cacheMatrixIDsDict];
isLocalContactListRefreshing = NO;
localContactByContactID = nil;
localContactsWithMethods = nil;
splitLocalContacts = nil;
[self cacheLocalContacts];
matrixContactByContactID = nil;
matrixContactByMatrixID = nil;
[self cacheMatrixContacts];
lastSyncDate = nil;
[self cacheContactBookInfo];
while (mxSessionArray.count) {
[self removeMatrixSession:mxSessionArray.lastObject];
}
mxSessionArray = nil;
mxEventListeners = nil;
// warn of the contacts list update
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil userInfo:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateLocalContactsNotification object:nil userInfo:nil];
}
- (MXKContact*)contactWithContactID:(NSString*)contactID
{
if ([contactID hasPrefix:kMXKContactLocalContactPrefixId])
{
return [localContactByContactID objectForKey:contactID];
}
else
{
return [matrixContactByContactID objectForKey:contactID];
}
}
// refresh the international phonenumber of the contacts
- (void)internationalizePhoneNumbers:(NSString*)countryCode
{
MXWeakify(self);
dispatch_async(processingQueue, ^{
MXStrongifyAndReturnIfNil(self);
NSArray* contactsSnapshot = [self->localContactByContactID allValues];
for (MXKContact* contact in contactsSnapshot)
{
contact.defaultCountryCode = countryCode;
}
[self cacheLocalContacts];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidInternationalizeNotification object:nil userInfo:nil];
});
});
}
- (MXKSectionedContacts *)getSectionedContacts:(NSArray*)contactsList
{
if (!contactsList.count)
{
return nil;
}
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 (MXKContact *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 [[MXKSectionedContacts alloc] initWithContacts:shortSectionsArray andTitles:tmpSectionedContactsTitle andCount:contactsCount];
}
- (void)sortAlphabeticallyContacts:(NSMutableArray<MXKContact*> *)contactsArray
{
NSComparator comparator = ^NSComparisonResult(MXKContact *contactA, MXKContact *contactB) {
if (contactA.sortingDisplayName.length && contactB.sortingDisplayName.length)
{
return [contactA.sortingDisplayName compare:contactB.sortingDisplayName options:NSCaseInsensitiveSearch];
}
else if (contactA.sortingDisplayName.length)
{
return NSOrderedAscending;
}
else if (contactB.sortingDisplayName.length)
{
return NSOrderedDescending;
}
return [contactA.displayName compare:contactB.displayName options:NSCaseInsensitiveSearch];
};
// Sort the contacts list
[contactsArray sortUsingComparator:comparator];
}
- (void)sortContactsByLastActiveInformation:(NSMutableArray<MXKContact*> *)contactsArray
{
// Sort invitable contacts by last active, with "active now" first.
// ...and then alphabetically.
NSComparator comparator = ^NSComparisonResult(MXKContact *contactA, MXKContact *contactB) {
MXUser *userA = [self firstMatrixUserOfContact:contactA];
MXUser *userB = [self firstMatrixUserOfContact:contactB];
// Non-Matrix-enabled contacts are moved to the end.
if (userA && !userB)
{
return NSOrderedAscending;
}
if (!userA && userB)
{
return NSOrderedDescending;
}
// Display active contacts first.
if (userA.currentlyActive && userB.currentlyActive)
{
// Then order by name
if (contactA.sortingDisplayName.length && contactB.sortingDisplayName.length)
{
return [contactA.sortingDisplayName compare:contactB.sortingDisplayName options:NSCaseInsensitiveSearch];
}
else if (contactA.sortingDisplayName.length)
{
return NSOrderedAscending;
}
else if (contactB.sortingDisplayName.length)
{
return NSOrderedDescending;
}
return [contactA.displayName compare:contactB.displayName options:NSCaseInsensitiveSearch];
}
if (userA.currentlyActive && !userB.currentlyActive)
{
return NSOrderedAscending;
}
if (!userA.currentlyActive && userB.currentlyActive)
{
return NSOrderedDescending;
}
// Finally, compare the lastActiveAgo
NSUInteger lastActiveAgoA = userA.lastActiveAgo;
NSUInteger lastActiveAgoB = userB.lastActiveAgo;
if (lastActiveAgoA == lastActiveAgoB)
{
return NSOrderedSame;
}
else
{
return ((lastActiveAgoA > lastActiveAgoB) ? NSOrderedDescending : NSOrderedAscending);
}
};
// Sort the contacts list
[contactsArray sortUsingComparator:comparator];
}
+ (void)requestUserConfirmationForLocalContactsSyncInViewController:(UIViewController *)viewController completionHandler:(void (^)(BOOL))handler
{
NSString *appDisplayName = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"];
[MXKContactManager requestUserConfirmationForLocalContactsSyncWithTitle:[VectorL10n localContactsAccessDiscoveryWarningTitle]
message:[VectorL10n localContactsAccessDiscoveryWarning:appDisplayName]
manualPermissionChangeMessage:[VectorL10n localContactsAccessNotGranted:appDisplayName]
showPopUpInViewController:viewController
completionHandler:handler];
}
+ (void)requestUserConfirmationForLocalContactsSyncWithTitle:(NSString*)title
message:(NSString*)message
manualPermissionChangeMessage:(NSString*)manualPermissionChangeMessage
showPopUpInViewController:(UIViewController*)viewController
completionHandler:(void (^)(BOOL granted))handler
{
if ([[MXKAppSettings standardAppSettings] syncLocalContacts])
{
handler(YES);
}
else
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n ok]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
[MXKTools checkAccessForContacts:manualPermissionChangeMessage showPopUpInViewController:viewController completionHandler:^(BOOL granted) {
handler(granted);
}];
}]];
[alert addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel]
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * action) {
handler(NO);
}]];
[viewController presentViewController:alert animated:YES completion:nil];
}
}
#pragma mark - Internals
- (NSDictionary*)matrixContactsByMatrixIDFromMXSessions:(NSArray<MXSession*>*)mxSessions
{
// The existing dictionary of contacts will be replaced by this one
NSMutableDictionary *matrixContactByMatrixID = [[NSMutableDictionary alloc] init];
for (MXSession *mxSession in mxSessions)
{
// Check all existing users
NSArray *mxUsers = [mxSession.users copy];
for (MXUser *user in mxUsers)
{
// Check whether this user has already been added
if (!matrixContactByMatrixID[user.userId])
{
if ((self.contactManagerMXRoomSource == MXKContactManagerMXRoomSourceAll) || ((self.contactManagerMXRoomSource == MXKContactManagerMXRoomSourceDirectChats) && mxSession.directRooms[user.userId]))
{
// Check whether a contact is already defined for this id in previous dictionary
// (avoid delete and create the same ones, it could save thumbnail downloads).
MXKContact* contact = matrixContactByMatrixID[user.userId];
if (contact)
{
contact.displayName = (user.displayname.length > 0) ? user.displayname : user.userId;
// Check the avatar change
if ((user.avatarUrl || contact.matrixAvatarURL) && ([user.avatarUrl isEqualToString:contact.matrixAvatarURL] == NO))
{
[contact resetMatrixThumbnail];
}
}
else
{
contact = [[MXKContact alloc] initMatrixContactWithDisplayName:((user.displayname.length > 0) ? user.displayname : user.userId) andMatrixID:user.userId];
}
matrixContactByMatrixID[user.userId] = contact;
}
}
}
}
// Do not make an immutable copy to avoid performance penalty
return matrixContactByMatrixID;
}
- (void)refreshMatrixContacts
{
NSArray *mxSessions = self.mxSessions;
// Check whether at least one session is available
if (!mxSessions.count)
{
matrixContactByMatrixID = nil;
matrixContactByContactID = nil;
[self cacheMatrixContacts];
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil userInfo:nil];
}
else if (self.contactManagerMXRoomSource != MXKContactManagerMXRoomSourceNone)
{
MXWeakify(self);
BOOL shouldFetchLocalContacts = self->matrixContactByContactID == nil;
dispatch_async(processingQueue, ^{
MXStrongifyAndReturnIfNil(self);
NSArray *sessions = self.mxSessions;
NSMutableDictionary *matrixContactsByMatrixID = nil;
NSMutableDictionary *matrixContactsByContactID = nil;
if (shouldFetchLocalContacts)
{
NSDictionary *cachedMatrixContacts = [self fetchCachedMatrixContacts];
if (!matrixContactsByContactID)
{
matrixContactsByContactID = [NSMutableDictionary dictionary];
}
else
{
matrixContactsByContactID = [cachedMatrixContacts mutableCopy];
}
}
else
{
matrixContactsByContactID = [NSMutableDictionary dictionary];
}
NSDictionary *matrixContacts = [self matrixContactsByMatrixIDFromMXSessions:sessions];
if (!matrixContacts)
{
matrixContactsByMatrixID = [NSMutableDictionary dictionary];
for (MXKContact *contact in matrixContactsByContactID.allValues)
{
matrixContactsByMatrixID[contact.matrixIdentifiers.firstObject] = contact;
}
}
else
{
matrixContactsByMatrixID = [matrixContacts mutableCopy];
}
for (MXKContact *contact in matrixContactsByMatrixID.allValues)
{
matrixContactsByContactID[contact.contactID] = contact;
}
dispatch_async(dispatch_get_main_queue(), ^{
MXStrongifyAndReturnIfNil(self);
// Update the matrix contacts list
self->matrixContactByMatrixID = matrixContactsByMatrixID;
self->matrixContactByContactID = matrixContactsByContactID;
[self cacheMatrixContacts];
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateMatrixContactsNotification object:nil userInfo:nil];
});
});
}
}
- (void)updateMatrixContactWithID:(NSString*)matrixId
{
// Check if a one-to-one room exist for this matrix user in at least one matrix session.
NSArray *mxSessions = self.mxSessions;
for (MXSession *mxSession in mxSessions)
{
if ((self.contactManagerMXRoomSource == MXKContactManagerMXRoomSourceAll) || ((self.contactManagerMXRoomSource == MXKContactManagerMXRoomSourceDirectChats) && mxSession.directRooms[matrixId]))
{
// Retrieve the user object related to this contact
MXUser* user = [mxSession userWithUserId:matrixId];
// This user may not exist (if the oneToOne room is a pending invitation to him).
if (user)
{
// Update or create a contact for this user
MXKContact* contact = [matrixContactByMatrixID objectForKey:matrixId];
BOOL isUpdated = NO;
// already defined
if (contact)
{
// Check the display name change
NSString *userDisplayName = (user.displayname.length > 0) ? user.displayname : user.userId;
if (![contact.displayName isEqualToString:userDisplayName])
{
contact.displayName = userDisplayName;
[self cacheMatrixContacts];
isUpdated = YES;
}
// Check the avatar change
if ((user.avatarUrl || contact.matrixAvatarURL) && ([user.avatarUrl isEqualToString:contact.matrixAvatarURL] == NO))
{
[contact resetMatrixThumbnail];
isUpdated = YES;
}
}
else
{
contact = [[MXKContact alloc] initMatrixContactWithDisplayName:((user.displayname.length > 0) ? user.displayname : user.userId) andMatrixID:user.userId];
[matrixContactByMatrixID setValue:contact forKey:matrixId];
// update the matrix contacts list
[matrixContactByContactID setValue:contact forKey:contact.contactID];
[self cacheMatrixContacts];
isUpdated = YES;
}
if (isUpdated)
{
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateMatrixContactsNotification object:contact.contactID userInfo:nil];
}
// Done
return;
}
}
}
// Here no one-to-one room exist, remove the contact if any
MXKContact* contact = [matrixContactByMatrixID objectForKey:matrixId];
if (contact)
{
[matrixContactByContactID removeObjectForKey:contact.contactID];
[matrixContactByMatrixID removeObjectForKey:matrixId];
[self cacheMatrixContacts];
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKContactManagerDidUpdateMatrixContactsNotification object:contact.contactID userInfo:nil];
}
}
- (void)updateLocalContactMatrixIDs:(MXKContact*) contact
{
for (MXKPhoneNumber* phoneNumber in contact.phoneNumbers)
{
if (phoneNumber.msisdn)
{
NSString* matrixID = [matrixIDBy3PID objectForKey:phoneNumber.msisdn];
dispatch_async(dispatch_get_main_queue(), ^{
[phoneNumber setMatrixID:matrixID];
});
}
}
for (MXKEmail* email in contact.emailAddresses)
{
if (email.emailAddress.length > 0)
{
NSString *matrixID = [matrixIDBy3PID objectForKey:email.emailAddress];
dispatch_async(dispatch_get_main_queue(), ^{
[email setMatrixID:matrixID];
});
}
}
}
- (void)updateAllLocalContactsMatrixIDs
{
// Check if the user allowed to sync local contacts
if (![MXKAppSettings standardAppSettings].syncLocalContacts)
{
return;
}
NSArray* localContacts = [localContactByContactID allValues];
// update the contacts info
for (MXKContact* contact in localContacts)
{
[self updateLocalContactMatrixIDs:contact];
}
}
- (MXUser*)firstMatrixUserOfContact:(MXKContact*)contact;
{
MXUser *user = nil;
NSArray *identifiers = contact.matrixIdentifiers;
if (identifiers.count)
{
for (MXSession *session in mxSessionArray)
{
user = [session userWithUserId:identifiers.firstObject];
if (user)
{
break;
}
}
}
return user;
}
#pragma mark - Identity server updates
- (void)registerAccountDataDidChangeIdentityServerNotification
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAccountDataDidChangeIdentityServerNotification:) name:kMXSessionAccountDataDidChangeIdentityServerNotification object:nil];
}
- (void)handleAccountDataDidChangeIdentityServerNotification:(NSNotification*)notification
{
MXLogDebug(@"[MXKContactManager] handleAccountDataDidChangeIdentityServerNotification");
if (!self.allowLocalContactsAccess)
{
MXLogDebug(@"[MXKContactManager] handleAccountDataDidChangeIdentityServerNotification. Does nothing because local contacts access not allowed.");
return;
}
// Use the identity server of the up
MXSession *mxSession = notification.object;
if (mxSession != mxSessionArray.firstObject)
{
return;
}
if (self.identityService)
{
// Do a full lookup
// But check first if the data is loaded
if (!self->localContactByContactID )
{
// Load data. That will trigger updateMatrixIDsForAllLocalContacts if needed
[self refreshLocalContacts];
}
else
{
[self updateMatrixIDsForAllLocalContacts];
}
}
else
{
[self resetMatrixIDs];
}
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (!self.allowLocalContactsAccess)
{
MXLogDebug(@"[MXKContactManager] Ignoring KVO changes, because local contacts access not allowed.");
return;
}
if ([@"syncLocalContacts" isEqualToString:keyPath])
{
dispatch_async(dispatch_get_main_queue(), ^{
[self refreshLocalContacts];
});
}
else if ([@"phonebookCountryCode" isEqualToString:keyPath])
{
dispatch_async(dispatch_get_main_queue(), ^{
[self internationalizePhoneNumbers:[[MXKAppSettings standardAppSettings] phonebookCountryCode]];
// Refresh local contacts if we have some
if (MXKAppSettings.standardAppSettings.syncLocalContacts && self->localContactByContactID.count)
{
[self refreshLocalContacts];
}
});
}
}
#pragma mark - file caches
static NSString *MXKContactManagerDomain = @"org.matrix.MatrixKit.MXKContactManager";
static NSInteger MXContactManagerEncryptionDelegateNotReady = -1;
static NSString *matrixContactsFileOld = @"matrixContacts";
static NSString *matrixIDsDictFileOld = @"matrixIDsDict";
static NSString *localContactsFileOld = @"localContacts";
static NSString *contactsBookInfoFileOld = @"contacts";
static NSString *matrixContactsFile = @"matrixContactsV2";
static NSString *matrixIDsDictFile = @"matrixIDsDictV2";
static NSString *localContactsFile = @"localContactsV2";
static NSString *contactsBookInfoFile = @"contactsV2";
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
- (NSString*)dataFilePathForComponent:(NSString*)component
{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
return [documentsDirectory stringByAppendingPathComponent:component];
}
- (void)cacheMatrixContacts
{
NSString *dataFilePath = [self dataFilePathForComponent:matrixContactsFile];
if (matrixContactByContactID && (matrixContactByContactID.count > 0))
{
// Switch on processing queue because matrixContactByContactID dictionary may be huge.
NSDictionary *matrixContactByContactIDCpy = [matrixContactByContactID copy];
dispatch_async(processingQueue, ^{
NSMutableData *theData = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:theData];
[encoder encodeObject:matrixContactByContactIDCpy forKey:@"matrixContactByContactID"];
[encoder finishEncoding];
[self encryptAndSaveData:theData toFile:matrixContactsFile];
});
}
else
{
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtPath:dataFilePath error:nil];
}
}
- (NSDictionary*)fetchCachedMatrixContacts
{
NSDate *startDate = [NSDate date];
NSString *dataFilePath = [self dataFilePathForComponent:matrixContactsFile];
NSFileManager *fileManager = [[NSFileManager alloc] init];
__block NSDictionary *matrixContactByContactID = nil;
if ([fileManager fileExistsAtPath:dataFilePath])
{
@try
{
NSData* filecontent = [NSData dataWithContentsOfFile:dataFilePath options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:nil];
NSError *error = nil;
filecontent = [self decryptData:filecontent error:&error fileName:matrixContactsFile];
if (!error)
{
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:filecontent];
id object = [decoder decodeObjectForKey:@"matrixContactByContactID"];
if ([object isKindOfClass:[NSDictionary class]])
{
matrixContactByContactID = object;
}
[decoder finishDecoding];
}
else
{
MXLogDebug(@"[MXKContactManager] fetchCachedMatrixContacts: failed to decrypt %@: %@", matrixContactsFile, error);
}
}
@catch (NSException *exception)
{
}
}
MXLogDebug(@"[MXKContactManager] fetchCachedMatrixContacts : Loaded %tu contacts in %.0fms", matrixContactByContactID.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
return matrixContactByContactID;
}
- (void)cacheMatrixIDsDict
{
NSString *dataFilePath = [self dataFilePathForComponent:matrixIDsDictFile];
if (matrixIDBy3PID.count)
{
NSMutableData *theData = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:theData];
[encoder encodeObject:matrixIDBy3PID forKey:@"matrixIDsDict"];
[encoder finishEncoding];
[self encryptAndSaveData:theData toFile:matrixIDsDictFile];
}
else
{
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtPath:dataFilePath error:nil];
}
}
- (void)loadCachedMatrixIDsDict
{
NSString *dataFilePath = [self dataFilePathForComponent: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];
NSError *error = nil;
filecontent = [self decryptData:filecontent error:&error fileName:matrixIDsDictFile];
if (!error)
{
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:filecontent];
id object = [decoder decodeObjectForKey:@"matrixIDsDict"];
if ([object isKindOfClass:[NSDictionary class]])
{
matrixIDBy3PID = [object mutableCopy];
}
[decoder finishDecoding];
}
else
{
MXLogDebug(@"[MXKContactManager] loadCachedMatrixIDsDict: failed to decrypt %@: %@", matrixIDsDictFile, error);
}
}
@catch (NSException *exception)
{
}
}
if (!matrixIDBy3PID)
{
matrixIDBy3PID = [[NSMutableDictionary alloc] init];
}
}
- (void)cacheLocalContacts
{
NSString *dataFilePath = [self dataFilePathForComponent:localContactsFile];
if (localContactByContactID && (localContactByContactID.count > 0))
{
NSMutableData *theData = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:theData];
[encoder encodeObject:localContactByContactID forKey:@"localContactByContactID"];
[encoder finishEncoding];
[self encryptAndSaveData:theData toFile:localContactsFile];
}
else
{
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtPath:dataFilePath error:nil];
}
}
- (void)loadCachedLocalContacts
{
NSString *dataFilePath = [self dataFilePathForComponent: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];
NSError *error = nil;
filecontent = [self decryptData:filecontent error:&error fileName:localContactsFile];
if (!error)
{
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:filecontent];
id object = [decoder decodeObjectForKey:@"localContactByContactID"];
if ([object isKindOfClass:[NSDictionary class]])
{
localContactByContactID = [object mutableCopy];
}
[decoder finishDecoding];
}
else
{
MXLogDebug(@"[MXKContactManager] loadCachedLocalContacts: failed to decrypt %@: %@", localContactsFile, error);
}
}
@catch (NSException *exception)
{
lastSyncDate = nil;
}
}
if (!localContactByContactID)
{
localContactByContactID = [[NSMutableDictionary alloc] init];
}
}
- (void)cacheContactBookInfo
{
NSString *dataFilePath = [self dataFilePathForComponent:contactsBookInfoFile];
if (lastSyncDate)
{
NSMutableData *theData = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:theData];
[encoder encodeObject:lastSyncDate forKey:@"lastSyncDate"];
[encoder finishEncoding];
[self encryptAndSaveData:theData toFile:contactsBookInfoFile];
}
else
{
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager removeItemAtPath:dataFilePath error:nil];
}
}
- (void)loadCachedContactBookInfo
{
NSString *dataFilePath = [self dataFilePathForComponent: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];
NSError *error = nil;
filecontent = [self decryptData:filecontent error:&error fileName:contactsBookInfoFile];
if (!error)
{
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:filecontent];
lastSyncDate = [decoder decodeObjectForKey:@"lastSyncDate"];
[decoder finishDecoding];
}
else
{
lastSyncDate = nil;
MXLogDebug(@"[MXKContactManager] loadCachedContactBookInfo: failed to decrypt %@: %@", contactsBookInfoFile, error);
}
}
@catch (NSException *exception)
{
lastSyncDate = nil;
}
}
}
#pragma clang diagnostic pop
- (BOOL)encryptAndSaveData:(NSData*)data toFile:(NSString*)fileName
{
NSError *error = nil;
NSData *cipher = [self encryptData:data error:&error fileName:fileName];
if (error == nil)
{
[cipher writeToFile:[self dataFilePathForComponent:fileName] atomically:YES];
[[NSFileManager defaultManager] excludeItemFromBackupAt:[NSURL fileURLWithPath:fileName] error:&error];
if (error) {
MXLogDebug(@"[MXKContactManager] Cannot exclude item from backup %@", error.localizedDescription);
}
}
else
{
MXLogDebug(@"[MXKContactManager] encryptAndSaveData: failed to encrypt %@", fileName);
}
return error == nil;
}
- (NSData*)encryptData:(NSData*)data error:(NSError**)error fileName:(NSString*)fileName
{
@try
{
MXKeyData *keyData = (MXKeyData *) [[MXKeyProvider sharedInstance] requestKeyForDataOfType:MXKContactManagerDataType isMandatory:NO expectedKeyType:kAes];
if (keyData && [keyData isKindOfClass:[MXAesKeyData class]])
{
MXAesKeyData *aesKey = (MXAesKeyData *) keyData;
NSData *cipher = [MXAes encrypt:data aesKey:aesKey.key iv:aesKey.iv error:error];
MXLogDebug(@"[MXKContactManager] encryptData: encrypted %lu Bytes for %@", cipher.length, fileName);
return cipher;
}
}
@catch (NSException *exception)
{
*error = [NSError errorWithDomain:MXKContactManagerDomain code:MXContactManagerEncryptionDelegateNotReady userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"encryptData failed: %@", exception.reason]}];
}
MXLogDebug(@"[MXKContactManager] encryptData: no key method provided for encryption of %@", fileName);
return data;
}
- (NSData*)decryptData:(NSData*)data error:(NSError**)error fileName:(NSString*)fileName
{
@try
{
MXKeyData *keyData = [[MXKeyProvider sharedInstance] requestKeyForDataOfType:MXKContactManagerDataType isMandatory:NO expectedKeyType:kAes];
if (keyData && [keyData isKindOfClass:[MXAesKeyData class]])
{
MXAesKeyData *aesKey = (MXAesKeyData *) keyData;
NSData *decrypt = [MXAes decrypt:data aesKey:aesKey.key iv:aesKey.iv error:error];
MXLogDebug(@"[MXKContactManager] decryptData: decrypted %lu Bytes for %@", decrypt.length, fileName);
return decrypt;
}
}
@catch (NSException *exception)
{
*error = [NSError errorWithDomain:MXKContactManagerDomain code:MXContactManagerEncryptionDelegateNotReady userInfo:@{NSLocalizedDescriptionKey: [NSString stringWithFormat:@"decryptData failed: %@", exception.reason]}];
}
MXLogDebug(@"[MXKContactManager] decryptData: no key method provided for decryption of %@", fileName);
return data;
}
- (void)deleteOldFiles {
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSArray<NSString*> *oldFileNames = @[matrixContactsFileOld, matrixIDsDictFileOld, localContactsFileOld, contactsBookInfoFileOld];
NSError *error = nil;
for (NSString *fileName in oldFileNames) {
NSString *filePath = [self dataFilePathForComponent:fileName];
if ([fileManager fileExistsAtPath:filePath])
{
error = nil;
if (![fileManager removeItemAtPath:filePath error:&error])
{
MXLogDebug(@"[MXKContactManager] deleteOldFiles: failed to remove %@", fileName);
}
}
}
}
@end