element-ios/Riot/Modules/MatrixKit/Models/Account/MXKAccountManager.m

803 lines
29 KiB
Objective-C

/*
Copyright 2024 New Vector Ltd.
Copyright 2019 The Matrix.org Foundation C.I.C
Copyright 2018 New Vector Ltd
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 "MXKAccountManager.h"
#import "MXKAppSettings.h"
#import "MXKTools.h"
#import "MXKAccountData.h"
#import "MXRefreshTokenData.h"
static NSString *const kMXKAccountsKeyOld = @"accounts";
static NSString *const kMXKAccountsKey = @"accountsV2";
NSString *const kMXKAccountManagerDidAddAccountNotification = @"kMXKAccountManagerDidAddAccountNotification";
NSString *const kMXKAccountManagerDidRemoveAccountNotification = @"kMXKAccountManagerDidRemoveAccountNotification";
NSString *const kMXKAccountManagerDidSoftlogoutAccountNotification = @"kMXKAccountManagerDidSoftlogoutAccountNotification";
NSString *const MXKAccountManagerDataType = @"org.matrix.kit.MXKAccountManagerDataType";
@interface MXKAccountManager()
{
/**
The list of all accounts (enabled and disabled). Each value is a `MXKAccount` instance.
*/
NSMutableArray<MXKAccount *> *mxAccounts;
}
@end
@implementation MXKAccountManager
+ (MXKAccountManager *)sharedManager
{
return [MXKAccountManager sharedManagerWithReload:NO];
}
+ (MXKAccountManager *)sharedManagerWithReload:(BOOL)reload
{
static MXKAccountManager *sharedAccountManager = nil;
static dispatch_once_t onceToken;
__block BOOL didLoad = false;
dispatch_once(&onceToken, ^{
didLoad = true;
sharedAccountManager = [[super allocWithZone:NULL] init];
});
if (reload && !didLoad) {
[sharedAccountManager loadAccounts];
}
return sharedAccountManager;
}
- (instancetype)init
{
self = [super init];
if (self)
{
_storeClass = [MXFileStore class];
_savingAccountsEnabled = YES;
// Migrate old account file to new format
[self migrateAccounts];
// Load existing accounts from local storage
[self loadAccounts];
}
return self;
}
- (void)dealloc
{
mxAccounts = nil;
}
#pragma mark -
- (void)prepareSessionForActiveAccounts
{
for (MXKAccount *account in mxAccounts)
{
// Check whether the account is enabled. Open a new matrix session if none.
if (!account.isDisabled && !account.isSoftLogout && !account.mxSession)
{
MXLogDebug(@"[MXKAccountManager] openSession for %@ account", account.mxCredentials.userId);
id<MXStore> store = [[_storeClass alloc] init];
[account openSessionWithStore:store];
}
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (void)saveAccounts
{
MXLogDebug(@"[MXKAccountManager] saveAccounts...");
if (!self.isSavingAccountsEnabled)
{
MXLogDebug(@"[MXKAccountManager] saveAccounts: saving disabled.");
return;
}
NSDate *startDate = [NSDate date];
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[encoder encodeObject:mxAccounts forKey:@"mxAccounts"];
[encoder finishEncoding];
[data setData:[self encryptData:data]];
BOOL result = [data writeToFile:[self accountFile] atomically:YES];
MXLogDebug(@"[MXKAccountManager] saveAccounts. Done (result: %@) in %.0fms", @(result), [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
}
#pragma clang diagnostic pop
- (void)addAccount:(MXKAccount *)account andOpenSession:(BOOL)openSession
{
MXLogDebug(@"[MXKAccountManager] login (%@)", account.mxCredentials.userId);
[mxAccounts addObject:account];
[self saveAccounts];
// Check conditions to open a matrix session
if (openSession && !account.disabled)
{
// Open a new matrix session by default
MXLogDebug(@"[MXKAccountManager] openSession for %@ account (device %@)", account.mxCredentials.userId, account.mxCredentials.deviceId);
id<MXStore> store = [[_storeClass alloc] init];
[account openSessionWithStore:store];
}
// Post notification
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountManagerDidAddAccountNotification object:account userInfo:nil];
}
- (void)removeAccount:(MXKAccount*)theAccount completion:(void (^)(void))completion
{
[self removeAccount:theAccount sendLogoutRequest:YES completion:completion];
}
- (void)removeAccount:(MXKAccount*)theAccount
sendLogoutRequest:(BOOL)sendLogoutRequest
completion:(void (^)(void))completion
{
MXLogDebug(@"[MXKAccountManager] logout (%@), send logout request to homeserver: %d", theAccount.mxCredentials.userId, sendLogoutRequest);
// Close session and clear associated store.
[theAccount logoutSendingServerRequest:sendLogoutRequest completion:^{
// Retrieve the corresponding account in the internal array
MXKAccount* removedAccount = nil;
for (MXKAccount *account in self->mxAccounts)
{
if ([account.mxCredentials.userId isEqualToString:theAccount.mxCredentials.userId])
{
removedAccount = account;
break;
}
}
if (removedAccount)
{
[self->mxAccounts removeObject:removedAccount];
[self saveAccounts];
// Post notification
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountManagerDidRemoveAccountNotification object:removedAccount userInfo:nil];
}
if (completion)
{
completion();
}
}];
}
- (void)logoutWithCompletion:(void (^)(void))completion
{
// Logout one by one the existing accounts
if (mxAccounts.count)
{
[self removeAccount:mxAccounts.lastObject completion:^{
// loop: logout the next existing account (if any)
[self logoutWithCompletion:completion];
}];
return;
}
NSUserDefaults *sharedUserDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults;
// Remove APNS device token
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"apnsDeviceToken"];
// Remove Push device token
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushDeviceToken"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
// Be sure that no account survive in local storage
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kMXKAccountsKey];
[sharedUserDefaults removeObjectForKey:kMXKAccountsKey];
[[NSFileManager defaultManager] removeItemAtPath:[self accountFile] error:nil];
if (completion)
{
completion();
}
}
- (void)softLogout:(MXKAccount*)account
{
[account softLogout];
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountManagerDidSoftlogoutAccountNotification
object:account
userInfo:nil];
}
- (void)hydrateAccount:(MXKAccount*)account withCredentials:(MXCredentials*)credentials
{
MXLogDebug(@"[MXKAccountManager] hydrateAccount: %@", account.mxCredentials.userId);
if ([account.mxCredentials.userId isEqualToString:credentials.userId])
{
// Restart the account
[account hydrateWithCredentials:credentials];
MXLogDebug(@"[MXKAccountManager] hydrateAccount: Open session");
id<MXStore> store = [[_storeClass alloc] init];
[account openSessionWithStore:store];
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountManagerDidAddAccountNotification
object:account
userInfo:nil];
}
else
{
MXLogDebug(@"[MXKAccountManager] hydrateAccount: Credentials given for another account: %@", credentials.userId);
// Logout the old account and create a new one with the new credentials
[self removeAccount:account sendLogoutRequest:YES completion:nil];
MXKAccount *newAccount = [[MXKAccount alloc] initWithCredentials:credentials];
[self addAccount:newAccount andOpenSession:YES];
}
}
- (MXKAccount *)accountForUserId:(NSString *)userId
{
for (MXKAccount *account in mxAccounts)
{
if ([account.mxCredentials.userId isEqualToString:userId])
{
return account;
}
}
return nil;
}
- (MXKAccount *)accountKnowingRoomWithRoomIdOrAlias:(NSString *)roomIdOrAlias
{
MXKAccount *theAccount = nil;
NSArray *activeAccounts = self.activeAccounts;
for (MXKAccount *account in activeAccounts)
{
if ([roomIdOrAlias hasPrefix:@"#"])
{
if ([account.mxSession roomWithAlias:roomIdOrAlias])
{
theAccount = account;
break;
}
}
else
{
if ([account.mxSession roomWithRoomId:roomIdOrAlias])
{
theAccount = account;
break;
}
}
}
return theAccount;
}
- (MXKAccount *)accountKnowingUserWithUserId:(NSString *)userId
{
MXKAccount *theAccount = nil;
NSArray *activeAccounts = self.activeAccounts;
for (MXKAccount *account in activeAccounts)
{
if ([account.mxSession userWithUserId:userId])
{
theAccount = account;
break;
}
}
return theAccount;
}
#pragma mark -
- (void)setStoreClass:(Class)storeClass
{
// Sanity check
NSAssert([storeClass conformsToProtocol:@protocol(MXStore)], @"MXKAccountManager only manages store class that conforms to MXStore protocol");
_storeClass = storeClass;
}
- (NSArray<MXKAccount *> *)accounts
{
return [mxAccounts copy];
}
- (NSArray<MXKAccount *> *)activeAccounts
{
NSMutableArray *activeAccounts = [NSMutableArray arrayWithCapacity:mxAccounts.count];
for (MXKAccount *account in mxAccounts)
{
if (!account.disabled && !account.isSoftLogout)
{
[activeAccounts addObject:account];
}
}
return activeAccounts;
}
- (NSData *)apnsDeviceToken
{
NSData *token = [[NSUserDefaults standardUserDefaults] objectForKey:@"apnsDeviceToken"];
if (!token.length)
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"apnsDeviceToken"];
token = nil;
}
MXLogDebug(@"[MXKAccountManager][Push] apnsDeviceToken: %@", [MXKTools logForPushToken:token]);
return token;
}
- (void)setApnsDeviceToken:(NSData *)apnsDeviceToken
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: %@", [MXKTools logForPushToken:apnsDeviceToken]);
NSData *oldToken = self.apnsDeviceToken;
if (!apnsDeviceToken.length)
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: reset APNS device token");
if (oldToken)
{
// turn off the Apns flag for all accounts if any
for (MXKAccount *account in mxAccounts)
{
[account enablePushNotifications:NO success:nil failure:nil];
}
}
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"apnsDeviceToken"];
}
else
{
NSArray *activeAccounts = self.activeAccounts;
if (!oldToken)
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: set APNS device token");
[[NSUserDefaults standardUserDefaults] setObject:apnsDeviceToken forKey:@"apnsDeviceToken"];
// turn on the Apns flag for all accounts, when the Apns registration succeeds for the first time
for (MXKAccount *account in activeAccounts)
{
[account enablePushNotifications:YES success:nil failure:nil];
}
}
else if (![oldToken isEqualToData:apnsDeviceToken])
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: update APNS device token");
NSMutableArray<MXKAccount*> *accountsWithAPNSPusher = [NSMutableArray new];
// Delete the pushers related to the old token
for (MXKAccount *account in activeAccounts)
{
if (account.hasPusherForPushNotifications)
{
[accountsWithAPNSPusher addObject:account];
}
[account enablePushNotifications:NO success:nil failure:nil];
}
// Update the token
[[NSUserDefaults standardUserDefaults] setObject:apnsDeviceToken forKey:@"apnsDeviceToken"];
// Refresh pushers with the new token.
for (MXKAccount *account in activeAccounts)
{
if ([accountsWithAPNSPusher containsObject:account])
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: Resync APNS for %@ account", account.mxCredentials.userId);
[account enablePushNotifications:YES success:nil failure:nil];
}
else
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: hasPusherForPushNotifications = NO for %@ account. Do not enable Push", account.mxCredentials.userId);
}
}
}
else
{
MXLogDebug(@"[MXKAccountManager][Push] setApnsDeviceToken: Same token. Nothing to do.");
}
}
}
- (BOOL)isAPNSAvailable
{
// [UIApplication isRegisteredForRemoteNotifications] tells whether your app can receive
// remote notifications or not. Receiving remote notifications does not guarantee it will
// display them to the user as they may have notifications set to deliver quietly.
BOOL isRemoteNotificationsAllowed = NO;
UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
if (sharedApplication)
{
isRemoteNotificationsAllowed = [sharedApplication isRegisteredForRemoteNotifications];
MXLogDebug(@"[MXKAccountManager][Push] isAPNSAvailable: The user %@ remote notification", (isRemoteNotificationsAllowed ? @"allowed" : @"denied"));
}
BOOL isAPNSAvailable = (isRemoteNotificationsAllowed && self.apnsDeviceToken);
MXLogDebug(@"[MXKAccountManager][Push] isAPNSAvailable: %@", @(isAPNSAvailable));
return isAPNSAvailable;
}
- (NSData *)pushDeviceToken
{
NSData *token = [[NSUserDefaults standardUserDefaults] objectForKey:@"pushDeviceToken"];
if (!token.length)
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushDeviceToken"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
token = nil;
}
MXLogDebug(@"[MXKAccountManager][Push] pushDeviceToken: %@", [MXKTools logForPushToken:token]);
return token;
}
- (NSDictionary *)pushOptions
{
NSDictionary *pushOptions = [[NSUserDefaults standardUserDefaults] objectForKey:@"pushOptions"];
MXLogDebug(@"[MXKAccountManager][Push] pushOptions: %@", pushOptions);
return pushOptions;
}
- (void)setPushDeviceToken:(NSData *)pushDeviceToken withPushOptions:(NSDictionary *)pushOptions
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: %@ withPushOptions: %@", [MXKTools logForPushToken:pushDeviceToken], pushOptions);
NSData *oldToken = self.pushDeviceToken;
if (!pushDeviceToken.length)
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Reset Push device token");
if (oldToken)
{
// turn off the Push flag for all accounts if any
for (MXKAccount *account in mxAccounts)
{
[account enablePushKitNotifications:NO success:^{
// make sure pusher really removed before losing token.
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushDeviceToken"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
} failure:nil];
}
}
}
else
{
NSArray *activeAccounts = self.activeAccounts;
if (!oldToken)
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Set Push device token");
[[NSUserDefaults standardUserDefaults] setObject:pushDeviceToken forKey:@"pushDeviceToken"];
if (pushOptions)
{
[[NSUserDefaults standardUserDefaults] setObject:pushOptions forKey:@"pushOptions"];
}
else
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
}
// turn on the Push flag for all accounts
for (MXKAccount *account in activeAccounts)
{
[account enablePushKitNotifications:YES success:nil failure:nil];
}
}
else if (![oldToken isEqualToData:pushDeviceToken])
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Update Push device token");
NSMutableArray<MXKAccount*> *accountsWithPushKitPusher = [NSMutableArray new];
// Delete the pushers related to the old token
for (MXKAccount *account in activeAccounts)
{
if (account.hasPusherForPushKitNotifications)
{
[accountsWithPushKitPusher addObject:account];
}
[account enablePushKitNotifications:NO success:nil failure:nil];
}
// Update the token
[[NSUserDefaults standardUserDefaults] setObject:pushDeviceToken forKey:@"pushDeviceToken"];
if (pushOptions)
{
[[NSUserDefaults standardUserDefaults] setObject:pushOptions forKey:@"pushOptions"];
}
else
{
[[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"];
}
// Refresh pushers with the new token.
for (MXKAccount *account in activeAccounts)
{
if ([accountsWithPushKitPusher containsObject:account])
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Resync Push for %@ account", account.mxCredentials.userId);
[account enablePushKitNotifications:YES success:nil failure:nil];
}
else
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: hasPusherForPushKitNotifications = NO for %@ account. Do not enable Push", account.mxCredentials.userId);
}
}
}
else
{
MXLogDebug(@"[MXKAccountManager][Push] setPushDeviceToken: Same token. Nothing to do.");
}
}
}
- (BOOL)isPushAvailable
{
// [UIApplication isRegisteredForRemoteNotifications] tells whether your app can receive
// remote notifications or not. Receiving remote notifications does not guarantee it will
// display them to the user as they may have notifications set to deliver quietly.
BOOL isRemoteNotificationsAllowed = NO;
UIApplication *sharedApplication = [UIApplication performSelector:@selector(sharedApplication)];
if (sharedApplication)
{
isRemoteNotificationsAllowed = [sharedApplication isRegisteredForRemoteNotifications];
MXLogDebug(@"[MXKAccountManager][Push] isPushAvailable: The user %@ remote notification", (isRemoteNotificationsAllowed ? @"allowed" : @"denied"));
}
BOOL isPushAvailable = (isRemoteNotificationsAllowed && self.pushDeviceToken);
MXLogDebug(@"[MXKAccountManager][Push] isPushAvailable: %@", @(isPushAvailable));
return isPushAvailable;
}
#pragma mark -
// Return the path of the file containing stored MXAccounts array
- (NSString*)accountFile
{
NSString *matrixKitCacheFolder = [MXKAppSettings cacheFolder];
return [matrixKitCacheFolder stringByAppendingPathComponent:kMXKAccountsKey];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (void)loadAccounts
{
MXLogDebug(@"[MXKAccountManager] loadAccounts");
NSString *accountFile = [self accountFile];
if ([[NSFileManager defaultManager] fileExistsAtPath:accountFile])
{
NSDate *startDate = [NSDate date];
NSError *error = nil;
NSData* filecontent = [NSData dataWithContentsOfFile:accountFile options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:&error];
if (!error)
{
// Decrypt data if encryption method is provided
NSData *unciphered = [self decryptData:filecontent];
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:unciphered];
mxAccounts = [decoder decodeObjectForKey:@"mxAccounts"];
if (!mxAccounts && [[MXKeyProvider sharedInstance] isEncryptionAvailableForDataOfType:MXKAccountManagerDataType])
{
// This happens if the V2 file has not been encrypted -> read file content then save encrypted accounts
MXLogDebug(@"[MXKAccountManager] loadAccounts. Failed to read decrypted data: reading file data without encryption.");
decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:filecontent];
mxAccounts = [decoder decodeObjectForKey:@"mxAccounts"];
if (mxAccounts)
{
MXLogDebug(@"[MXKAccountManager] loadAccounts. saving encrypted accounts");
[self saveAccounts];
}
}
}
MXLogDebug(@"[MXKAccountManager] loadAccounts. %tu accounts loaded in %.0fms", mxAccounts.count, [[NSDate date] timeIntervalSinceDate:startDate] * 1000);
}
else
{
// Migration of accountData from sharedUserDefaults to a file
NSUserDefaults *sharedDefaults = [MXKAppSettings standardAppSettings].sharedUserDefaults;
NSData *accountData = [sharedDefaults objectForKey:kMXKAccountsKey];
if (!accountData)
{
// Migration of accountData from [NSUserDefaults standardUserDefaults], the first location storage
accountData = [[NSUserDefaults standardUserDefaults] objectForKey:kMXKAccountsKey];
}
if (accountData)
{
mxAccounts = [NSMutableArray arrayWithArray:[NSKeyedUnarchiver unarchiveObjectWithData:accountData]];
[self saveAccounts];
MXLogDebug(@"[MXKAccountManager] loadAccounts: performed data migration");
// Now that data has been migrated, erase old location of accountData
[[NSUserDefaults standardUserDefaults] removeObjectForKey:kMXKAccountsKey];
[sharedDefaults removeObjectForKey:kMXKAccountsKey];
}
}
if (!mxAccounts)
{
MXLogDebug(@"[MXKAccountManager] loadAccounts. No accounts");
mxAccounts = [NSMutableArray array];
}
}
#pragma clang diagnostic pop
- (NSData*)encryptData:(NSData*)data
{
// Exceptions are not caught as the key is always needed if the KeyProviderDelegate
// is provided.
MXKeyData *keyData = [[MXKeyProvider sharedInstance] requestKeyForDataOfType:MXKAccountManagerDataType isMandatory:YES expectedKeyType:kAes];
if (keyData && [keyData isKindOfClass:[MXAesKeyData class]])
{
MXAesKeyData *aesKey = (MXAesKeyData *) keyData;
NSData *cipher = [MXAes encrypt:data aesKey:aesKey.key iv:aesKey.iv error:nil];
return cipher;
}
MXLogDebug(@"[MXKAccountManager] encryptData: no key method provided for encryption.");
return data;
}
- (NSData*)decryptData:(NSData*)data
{
// Exceptions are not cached as the key is always needed if the KeyProviderDelegate
// is provided.
MXKeyData *keyData = [[MXKeyProvider sharedInstance] requestKeyForDataOfType:MXKAccountManagerDataType isMandatory:YES expectedKeyType:kAes];
if (keyData && [keyData isKindOfClass:[MXAesKeyData class]])
{
MXAesKeyData *aesKey = (MXAesKeyData *) keyData;
NSData *decrypt = [MXAes decrypt:data aesKey:aesKey.key iv:aesKey.iv error:nil];
return decrypt;
}
MXLogDebug(@"[MXKAccountManager] decryptData: no key method provided for decryption.");
return data;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (void)migrateAccounts
{
NSString *pathOld = [[MXKAppSettings cacheFolder] stringByAppendingPathComponent:kMXKAccountsKeyOld];
NSString *pathNew = [[MXKAppSettings cacheFolder] stringByAppendingPathComponent:kMXKAccountsKey];
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:pathOld])
{
if (![fileManager fileExistsAtPath:pathNew])
{
MXLogDebug(@"[MXKAccountManager] migrateAccounts: reading account");
mxAccounts = [NSKeyedUnarchiver unarchiveObjectWithFile:pathOld];
MXLogDebug(@"[MXKAccountManager] migrateAccounts: writing to accountV2");
[self saveAccounts];
}
MXLogDebug(@"[MXKAccountManager] migrateAccounts: removing account");
[fileManager removeItemAtPath:pathOld error:nil];
}
}
#pragma clang diagnostic pop
- (void)readAndWriteCredentials:(void (^)(NSArray<MXCredentials*> * _Nullable readData, void (^completion)(BOOL didUpdateCredentials)))readAnWriteHandler
{
NSError *error;
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] init];
__block BOOL coordinatorSuccess = NO;
MXLogDebug(@"[MXKAccountManager] readAndWriteCredentials: purposeIdentifier = %@", fileCoordinator.purposeIdentifier);
NSDate *coordinateStartTime = [NSDate date];
[fileCoordinator coordinateReadingItemAtURL:[self accountFileUrl]
options:0
writingItemAtURL:[self accountFileUrl]
options:NSFileCoordinatorWritingForMerging
error:&error
byAccessor:^(NSURL * _Nonnull newReadingURL, NSURL * _Nonnull newWritingURL) {
NSDate *accessorStartTime = [NSDate date];
NSTimeInterval acquireInterval = [accessorStartTime timeIntervalSinceDate:coordinateStartTime];
MXLogDebug(@"[MXKAccountManager] readAndWriteCredentials: acquireInterval = %f", acquireInterval);
NSError *error = nil;
NSData* data = [NSData dataWithContentsOfURL:newReadingURL options:(NSDataReadingMappedAlways | NSDataReadingUncached) error:&error];
// Decrypt data if encryption method is provided
NSData *unciphered = [self decryptData:data];
NSKeyedUnarchiver *decoder = [[NSKeyedUnarchiver alloc] initForReadingFromData:unciphered error:&error];
decoder.requiresSecureCoding = false;
[decoder setClass:[MXKAccountData class] forClassName:@"MXKAccount"];
NSMutableArray<MXKAccountData*>* mxAccountsData = [decoder decodeObjectForKey:@"mxAccounts"];
NSMutableArray<MXCredentials*>* mxAccountCredentials = [NSMutableArray arrayWithCapacity:mxAccounts.count];
for(MXKAccountData *account in mxAccountsData){
[mxAccountCredentials addObject:account.mxCredentials];
}
dispatch_group_t dispatchGroup = dispatch_group_create();
dispatch_group_enter(dispatchGroup);
__block BOOL didUpdate = NO;
readAnWriteHandler(mxAccountCredentials, ^(BOOL didUpdateCredentials) {
didUpdate = didUpdateCredentials;
dispatch_group_leave(dispatchGroup);
});
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
if (didUpdate) {
MXLogDebug(@"[MXKAccountManager] readAndWriteCredentials: did update saving credential data");
NSKeyedArchiver *encoder = [[NSKeyedArchiver alloc] initRequiringSecureCoding: NO];
[encoder setClassName:@"MXKAccount" forClass:[MXKAccountData class]];
[encoder encodeObject:mxAccountsData forKey:@"mxAccounts"];
NSData *writeData = [self encryptData:[encoder encodedData]];
coordinatorSuccess = [writeData writeToURL:newWritingURL atomically:YES];
} else {
MXLogDebug(@"[MXKAccountManager] readAndWriteCredentials: did not update not saving credential data");
coordinatorSuccess = YES;
}
NSDate *accessorEndTime = [NSDate date];
NSTimeInterval lockedTime = [accessorEndTime timeIntervalSinceDate:accessorStartTime];
MXLogDebug(@"[MXKAccountManager] readAndWriteCredentials: lockedTime = %f", lockedTime);
}];
MXLogDebug(@"[MXKAccountManager] readAndWriteCredentials:exit %d", coordinatorSuccess);
}
- (NSURL *)accountFileUrl
{
return [NSURL fileURLWithPath: [self accountFile]];
}
@end