/* 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 *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 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 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 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 *)accounts { return [mxAccounts copy]; } - (NSArray *)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 *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 *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 * _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* mxAccountsData = [decoder decodeObjectForKey:@"mxAccounts"]; NSMutableArray* 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