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

2140 lines
74 KiB
Mathematica
Raw Normal View History

/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "MXKAccount.h"
#import "MXKAccountManager.h"
#import "MXKRoomDataSourceManager.h"
#import "MXKEventFormatter.h"
#import "MXKTools.h"
#import "MXKContactManager.h"
#import "MXKConstants.h"
#import "NSBundle+MatrixKit.h"
#import <AFNetworking/AFNetworking.h>
#import <MatrixSDK/MXBackgroundModeHandler.h>
#import "MXKSwiftHeader.h"
#import "GeneratedInterface-Swift.h"
NSString *const kMXKAccountUserInfoDidChangeNotification = @"kMXKAccountUserInfoDidChangeNotification";
NSString *const kMXKAccountAPNSActivityDidChangeNotification = @"kMXKAccountAPNSActivityDidChangeNotification";
NSString *const kMXKAccountPushKitActivityDidChangeNotification = @"kMXKAccountPushKitActivityDidChangeNotification";
NSString *const kMXKAccountErrorDomain = @"kMXKAccountErrorDomain";
static MXKAccountOnCertificateChange _onCertificateChangeBlock;
/**
HTTP status codes for error cases on initial sync requests, for which errors will not be propagated to the client.
*/
static NSArray<NSNumber*> *initialSyncSilentErrorsHTTPStatusCodes;
@interface MXKAccount ()
{
// We will notify user only once on session failure
BOOL notifyOpenSessionFailure;
// The timer used to postpone server sync on failure
NSTimer* initialServerSyncTimer;
// Reachability observer
id reachabilityObserver;
// Session state observer
id sessionStateObserver;
// Handle user's settings change
id userUpdateListener;
// Used for logging application start up
NSDate *openSessionStartDate;
// Event notifications listener
id notificationCenterListener;
// Internal list of ignored rooms
NSMutableArray* ignoredRooms;
// If a server sync is in progress, the pause is delayed at the end of sync (except if resume is called).
BOOL isPauseRequested;
// Background sync management
MXOnBackgroundSyncDone backgroundSyncDone;
MXOnBackgroundSyncFail backgroundSyncFails;
NSTimer* backgroundSyncTimer;
// Observe UIApplicationSignificantTimeChangeNotification to refresh MXRoomSummaries on time formatting change.
id UIApplicationSignificantTimeChangeNotificationObserver;
// Observe NSCurrentLocaleDidChangeNotification to refresh MXRoomSummaries on time formatting change.
id NSCurrentLocaleDidChangeNotificationObserver;
}
@property (nonatomic, strong) id<MXBackgroundTask> backgroundTask;
@property (nonatomic, strong) id<MXBackgroundTask> backgroundSyncBgTask;
@end
@implementation MXKAccount
@synthesize mxSession, mxRestClient;
@synthesize userPresence;
@synthesize userTintColor;
@synthesize hideUserPresence;
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initialSyncSilentErrorsHTTPStatusCodes = @[
@(504),
@(522),
@(524),
@(599)
];
});
}
+ (void)registerOnCertificateChangeBlock:(MXKAccountOnCertificateChange)onCertificateChangeBlock
{
_onCertificateChangeBlock = onCertificateChangeBlock;
}
+ (UIColor*)presenceColor:(MXPresence)presence
{
switch (presence)
{
case MXPresenceOnline:
return [[MXKAppSettings standardAppSettings] presenceColorForOnlineUser];
case MXPresenceUnavailable:
return [[MXKAppSettings standardAppSettings] presenceColorForUnavailableUser];
case MXPresenceOffline:
return [[MXKAppSettings standardAppSettings] presenceColorForOfflineUser];
case MXPresenceUnknown:
default:
return nil;
}
}
- (instancetype)initWithCredentials:(MXCredentials*)credentials
{
if (self = [super init])
{
notifyOpenSessionFailure = YES;
// Report credentials and alloc REST client.
_mxCredentials = credentials;
[self prepareRESTClient];
userPresence = MXPresenceUnknown;
// Refresh device information
[self loadDeviceInformation:nil failure:nil];
[self registerAccountDataDidChangeIdentityServerNotification];
[self registerIdentityServiceDidChangeAccessTokenNotification];
}
return self;
}
- (void)dealloc
{
[self closeSession:NO];
mxSession = nil;
[mxRestClient close];
mxRestClient = nil;
}
#pragma mark - NSCoding
- (id)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self)
{
notifyOpenSessionFailure = YES;
[self prepareRESTClient];
[self registerAccountDataDidChangeIdentityServerNotification];
[self registerIdentityServiceDidChangeAccessTokenNotification];
userPresence = MXPresenceUnknown;
// Refresh device information
[self loadDeviceInformation:nil failure:nil];
}
return self;
}
#pragma mark - Properties
- (void)setIdentityServerURL:(NSString *)identityServerURL
{
if (identityServerURL.length)
{
_identityServerURL = identityServerURL;
self.mxCredentials.identityServer = identityServerURL;
// Update services used in MXSession
[mxSession setIdentityServer:self.mxCredentials.identityServer andAccessToken:self.mxCredentials.identityServerAccessToken];
}
else
{
_identityServerURL = nil;
[mxSession setIdentityServer:nil andAccessToken:nil];
}
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (void)setAntivirusServerURL:(NSString *)antivirusServerURL
{
_antivirusServerURL = antivirusServerURL;
// Update the current session if any
[mxSession setAntivirusServerURL:antivirusServerURL];
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (void)setPushGatewayURL:(NSString *)pushGatewayURL
{
_pushGatewayURL = pushGatewayURL.length ? pushGatewayURL : nil;
MXLogDebug(@"[MXKAccount][Push] setPushGatewayURL: %@", _pushGatewayURL);
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (NSString*)userDisplayName
{
if (mxSession)
{
return mxSession.myUser.displayname;
}
return nil;
}
- (NSString*)userAvatarUrl
{
if (mxSession)
{
return mxSession.myUser.avatarUrl;
}
return nil;
}
- (NSString*)fullDisplayName
{
if (self.userDisplayName.length)
{
return [NSString stringWithFormat:@"%@ (%@)", self.userDisplayName, self.mxCredentials.userId];
}
else
{
return self.mxCredentials.userId;
}
}
- (NSArray<NSString *> *)linkedEmails
{
NSMutableArray<NSString *> *linkedEmails = [NSMutableArray array];
for (MXThirdPartyIdentifier *threePID in self.threePIDs)
{
if ([threePID.medium isEqualToString:kMX3PIDMediumEmail])
{
[linkedEmails addObject:threePID.address];
}
}
return linkedEmails;
}
- (NSArray<NSString *> *)linkedPhoneNumbers
{
NSMutableArray<NSString *> *linkedPhoneNumbers = [NSMutableArray array];
for (MXThirdPartyIdentifier *threePID in self.threePIDs)
{
if ([threePID.medium isEqualToString:kMX3PIDMediumMSISDN])
{
[linkedPhoneNumbers addObject:threePID.address];
}
}
return linkedPhoneNumbers;
}
- (UIColor*)userTintColor
{
if (!userTintColor)
{
userTintColor = [MXKTools colorWithRGBValue:[self.mxCredentials.userId hash]];
}
return userTintColor;
}
- (BOOL)pushNotificationServiceIsActive
{
BOOL pushNotificationServiceIsActive = ([[MXKAccountManager sharedManager] isAPNSAvailable] && self.hasPusherForPushNotifications && mxSession);
MXLogDebug(@"[MXKAccount][Push] pushNotificationServiceIsActive: %@", @(pushNotificationServiceIsActive));
return pushNotificationServiceIsActive;
}
- (void)enablePushNotifications:(BOOL)enable
success:(void (^)(void))success
failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: %@", @(enable));
if (enable)
{
if ([[MXKAccountManager sharedManager] isAPNSAvailable])
{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Enable Push for %@ account", self.mxCredentials.userId);
// Create/restore the pusher
[self enableAPNSPusher:YES success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Enable Push: Success");
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Enable Push: Error: %@", error);
if (failure)
{
failure(error);
}
}];
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Error: Cannot enable Push");
NSError *error = [NSError errorWithDomain:kMXKAccountErrorDomain
code:0
userInfo:@{
NSLocalizedDescriptionKey:
[MatrixKitL10n accountErrorPushNotAllowed]
}];
if (failure)
{
failure (error);
}
}
}
else if (self.hasPusherForPushNotifications)
{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Disable APNS for %@ account", self.mxCredentials.userId);
// Delete the pusher, report the new value only on success.
[self enableAPNSPusher:NO
success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Disable Push: Success");
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] enablePushNotifications: Disable Push: Error: %@", error);
if (failure)
{
failure(error);
}
}];
}
}
- (BOOL)isPushKitNotificationActive
{
BOOL isPushKitNotificationActive = ([[MXKAccountManager sharedManager] isPushAvailable] && self.hasPusherForPushKitNotifications && mxSession);
MXLogDebug(@"[MXKAccount][Push] isPushKitNotificationActive: %@", @(isPushKitNotificationActive));
return isPushKitNotificationActive;
}
- (void)enablePushKitNotifications:(BOOL)enable
success:(void (^)(void))success
failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: %@", @(enable));
if (enable)
{
if ([[MXKAccountManager sharedManager] isPushAvailable])
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Enable Push for %@ account", self.mxCredentials.userId);
// Create/restore the pusher
[self enablePushKitPusher:YES success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Enable Push: Success");
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Enable Push: Error: %@", error);
if (failure)
{
failure(error);
}
}];
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Error: Cannot enable Push");
NSError *error = [NSError errorWithDomain:kMXKAccountErrorDomain
code:0
userInfo:@{
NSLocalizedDescriptionKey:
[MatrixKitL10n accountErrorPushNotAllowed]
}];
failure (error);
}
}
else if (self.hasPusherForPushKitNotifications)
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Disable Push for %@ account", self.mxCredentials.userId);
// Delete the pusher, report the new value only on success.
[self enablePushKitPusher:NO success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Disable Push: Success");
if (success)
{
success();
}
} failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: Disable Push: Error: %@", error);
if (failure)
{
failure(error);
}
}];
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitNotifications: PushKit is already disabled for %@", self.mxCredentials.userId);
if (success)
{
success();
}
}
}
- (void)setEnableInAppNotifications:(BOOL)enableInAppNotifications
{
MXLogDebug(@"[MXKAccount] setEnableInAppNotifications: %@", @(enableInAppNotifications));
_enableInAppNotifications = enableInAppNotifications;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (void)setDisabled:(BOOL)disabled
{
if (_disabled != disabled)
{
_disabled = disabled;
if (_disabled)
{
[self deletePusher];
[self enablePushKitNotifications:NO success:nil failure:nil];
// Close session (keep the storage).
[self closeSession:NO];
}
else if (!mxSession)
{
// Open a new matrix session
id<MXStore> store = [[[MXKAccountManager sharedManager].storeClass alloc] init];
[self openSessionWithStore:store];
}
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
}
- (void)setWarnedAboutEncryption:(BOOL)warnedAboutEncryption
{
_warnedAboutEncryption = warnedAboutEncryption;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
- (NSMutableDictionary<NSString *, id<NSCoding>> *)others
{
if(_others == nil)
{
_others = [NSMutableDictionary dictionary];
}
return _others;
}
#pragma mark - Matrix user's profile
- (void)setUserDisplayName:(NSString*)displayname success:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
if (mxSession && mxSession.myUser)
{
[mxSession.myUser setDisplayName:displayname
success:^{
if (success) {
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
failure:failure];
}
else if (failure)
{
failure ([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey: [MatrixKitL10n accountErrorMatrixSessionIsNotOpened]}]);
}
}
- (void)setUserAvatarUrl:(NSString*)avatarUrl success:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
if (mxSession && mxSession.myUser)
{
[mxSession.myUser setAvatarUrl:avatarUrl
success:^{
if (success) {
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
failure:failure];
}
else if (failure)
{
failure ([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey: [MatrixKitL10n accountErrorMatrixSessionIsNotOpened]}]);
}
}
- (void)changePassword:(NSString*)oldPassword with:(NSString*)newPassword success:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
if (mxSession)
{
[mxRestClient changePassword:oldPassword
with:newPassword
success:^{
if (success) {
success();
}
}
failure:failure];
}
else if (failure)
{
failure ([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey: [MatrixKitL10n accountErrorMatrixSessionIsNotOpened]}]);
}
}
- (void)load3PIDs:(void (^)(void))success failure:(void (^)(NSError *))failure
{
[mxRestClient threePIDs:^(NSArray<MXThirdPartyIdentifier *> *threePIDs2) {
self->_threePIDs = threePIDs2;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
if (success)
{
success();
}
} failure:^(NSError *error) {
if (failure)
{
failure(error);
}
}];
}
- (void)loadDeviceInformation:(void (^)(void))success failure:(void (^)(NSError *error))failure
{
if (self.mxCredentials.deviceId)
{
[mxRestClient deviceByDeviceId:self.mxCredentials.deviceId success:^(MXDevice *device) {
self->_device = device;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
if (success)
{
success();
}
} failure:^(NSError *error) {
if (failure)
{
failure(error);
}
}];
}
else
{
_device = nil;
if (success)
{
success();
}
}
}
- (void)setUserPresence:(MXPresence)presence andStatusMessage:(NSString *)statusMessage completion:(void (^)(void))completion
{
userPresence = presence;
if (mxSession && !hideUserPresence)
{
// Update user presence on server side
[mxSession.myUser setPresence:userPresence
andStatusMessage:statusMessage
success:^{
MXLogDebug(@"[MXKAccount] %@: set user presence (%lu) succeeded", self.mxCredentials.userId, (unsigned long)self->userPresence);
if (completion)
{
completion();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount] %@: set user presence (%lu) failed", self.mxCredentials.userId, (unsigned long)self->userPresence);
}];
}
else if (hideUserPresence)
{
MXLogDebug(@"[MXKAccount] %@: set user presence is disabled.", self.mxCredentials.userId);
}
}
#pragma mark -
/**
Create a matrix session based on the provided store.
When store data is ready, the live stream is automatically launched by synchronising the session with the server.
In case of failure during server sync, the method is reiterated until the data is up-to-date with the server.
This loop is stopped if you call [MXCAccount closeSession:], it is suspended if you call [MXCAccount pauseInBackgroundTask].
@param store the store to use for the session.
*/
-(void)openSessionWithStore:(id<MXStore>)store
{
// Sanity check
if (!self.mxCredentials || !mxRestClient)
{
MXLogDebug(@"[MXKAccount] Matrix session cannot be created without credentials");
return;
}
// Close potential session (keep associated store).
[self closeSession:NO];
openSessionStartDate = [NSDate date];
// Instantiate new session
mxSession = [[MXSession alloc] initWithMatrixRestClient:mxRestClient];
// Check whether an antivirus url is defined.
if (_antivirusServerURL)
{
// Enable the antivirus scanner in the current session.
[mxSession setAntivirusServerURL:_antivirusServerURL];
}
// Set default MXEvent -> NSString formatter
MXKEventFormatter *eventFormatter = [[MXKEventFormatter alloc] initWithMatrixSession:self.mxSession];
eventFormatter.isForSubtitle = YES;
// Apply the event types filter to display only the wanted event types.
eventFormatter.eventTypesFilterForMessages = [MXKAppSettings standardAppSettings].eventsFilterForMessages;
mxSession.roomSummaryUpdateDelegate = eventFormatter;
// Observe UIApplicationSignificantTimeChangeNotification to refresh to MXRoomSummaries if date/time are shown.
// UIApplicationSignificantTimeChangeNotification is posted if DST is updated, carrier time is updated
UIApplicationSignificantTimeChangeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationSignificantTimeChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
[self onDateTimeFormatUpdate];
}];
// Observe NSCurrentLocaleDidChangeNotification to refresh MXRoomSummaries if date/time are shown.
// NSCurrentLocaleDidChangeNotification is triggered when the time swicthes to AM/PM to 24h time format
NSCurrentLocaleDidChangeNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:NSCurrentLocaleDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
[self onDateTimeFormatUpdate];
}];
// Force a date refresh for all the last messages.
[self onDateTimeFormatUpdate];
// Register session state observer
sessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) {
// Check whether the concerned session is the associated one
if (notif.object == self->mxSession)
{
[self onMatrixSessionStateChange];
}
}];
MXWeakify(self);
[mxSession setStore:store success:^{
// Complete session registration by launching live stream
MXStrongifyAndReturnIfNil(self);
// Validate the availability of local contact sync for any changes to the
// authorization of contacts access that may have occurred since the last launch.
// The session is passed in as the contacts manager may not have had a session added yet.
[MXKContactManager.sharedManager validateSyncLocalContactsStateForSession:self.mxSession];
// Refresh pusher state
[self refreshAPNSPusher];
[self refreshPushKitPusher];
// Launch server sync
[self launchInitialServerSync];
} failure:^(NSError *error) {
// This cannot happen. Loading of MXFileStore cannot fail.
MXStrongifyAndReturnIfNil(self);
self->mxSession = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self->sessionStateObserver];
self->sessionStateObserver = nil;
}];
}
/**
Close the matrix session.
@param clearStore set YES to delete all store data.
*/
- (void)closeSession:(BOOL)clearStore
{
MXLogDebug(@"[MXKAccount] closeSession (%u)", clearStore);
if (NSCurrentLocaleDidChangeNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:NSCurrentLocaleDidChangeNotificationObserver];
NSCurrentLocaleDidChangeNotificationObserver = nil;
}
if (UIApplicationSignificantTimeChangeNotificationObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:UIApplicationSignificantTimeChangeNotificationObserver];
UIApplicationSignificantTimeChangeNotificationObserver = nil;
}
[self removeNotificationListener];
if (reachabilityObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver];
reachabilityObserver = nil;
}
if (sessionStateObserver)
{
[[NSNotificationCenter defaultCenter] removeObserver:sessionStateObserver];
sessionStateObserver = nil;
}
[initialServerSyncTimer invalidate];
initialServerSyncTimer = nil;
if (userUpdateListener)
{
[mxSession.myUser removeListener:userUpdateListener];
userUpdateListener = nil;
}
if (mxSession)
{
// Reset room data stored in memory
[MXKRoomDataSourceManager removeSharedManagerForMatrixSession:mxSession];
if (clearStore)
{
// Force a reload of device keys at the next session start.
// This will fix potential UISIs other peoples receive for our messages.
[mxSession.crypto resetDeviceKeys];
// Clean other stores
[mxSession.scanManager deleteAllAntivirusScans];
[mxSession.aggregations resetData];
}
else
{
// For recomputing of room summaries as they are a cache of computed data
[mxSession resetRoomsSummariesLastMessage];
}
// Close session
[mxSession close];
if (clearStore)
{
[mxSession.store deleteAllData];
}
mxSession = nil;
}
notifyOpenSessionFailure = YES;
}
- (void)logout:(void (^)(void))completion
{
if (!mxSession)
{
MXLogDebug(@"[MXKAccount] logout: Need to open the closed session to make a logout request");
id<MXStore> store = [[[MXKAccountManager sharedManager].storeClass alloc] init];
mxSession = [[MXSession alloc] initWithMatrixRestClient:mxRestClient];
MXWeakify(self);
[mxSession setStore:store success:^{
MXStrongifyAndReturnIfNil(self);
[self logout:completion];
} failure:^(NSError *error) {
completion();
}];
return;
}
[self deletePusher];
[self enablePushKitNotifications:NO success:nil failure:nil];
MXHTTPOperation *operation = [mxSession logout:^{
[self closeSession:YES];
if (completion)
{
completion();
}
} failure:^(NSError *error) {
// Close the session even if the logout request failed
[self closeSession:YES];
if (completion)
{
completion();
}
}];
// Do not retry on failure.
operation.maxNumberOfTries = 1;
}
// Logout locally, do not send server request
- (void)logoutLocally:(void (^)(void))completion
{
[self deletePusher];
[self enablePushKitNotifications:NO success:nil failure:nil];
[mxSession enableCrypto:NO success:^{
[self closeSession:YES];
if (completion)
{
completion();
}
} failure:^(NSError *error) {
// Close the session even if the logout request failed
[self closeSession:YES];
if (completion)
{
completion();
}
}];
}
- (void)logoutSendingServerRequest:(BOOL)sendLogoutServerRequest
completion:(void (^)(void))completion
{
if (sendLogoutServerRequest)
{
[self logout:completion];
}
else
{
[self logoutLocally:completion];
}
}
#pragma mark - Soft logout
- (void)softLogout
{
_isSoftLogout = YES;
[[MXKAccountManager sharedManager] saveAccounts];
// Stop SDK making requests to the homeserver
[mxSession close];
}
- (void)hydrateWithCredentials:(MXCredentials*)credentials
{
// Sanity check
if ([self.mxCredentials.userId isEqualToString:credentials.userId])
{
_mxCredentials = credentials;
_isSoftLogout = NO;
[[MXKAccountManager sharedManager] saveAccounts];
[self prepareRESTClient];
}
else
{
MXLogDebug(@"[MXKAccount] hydrateWithCredentials: Error: users ids mismatch: %@ vs %@", credentials.userId, self.mxCredentials.userId);
}
}
- (void)deletePusher
{
if (self.pushNotificationServiceIsActive)
{
[self enableAPNSPusher:NO success:nil failure:nil];
}
}
- (void)pauseInBackgroundTask
{
// Reset internal flag
isPauseRequested = NO;
if (mxSession && mxSession.isPauseable)
{
id<MXBackgroundModeHandler> handler = [MXSDKOptions sharedInstance].backgroundModeHandler;
if (handler)
{
if (!self.backgroundTask.isRunning)
{
self.backgroundTask = [handler startBackgroundTaskWithName:@"[MXKAccount] pauseInBackgroundTask" expirationHandler:nil];
}
}
// Pause SDK
[mxSession pause];
// Update user presence
__weak typeof(self) weakSelf = self;
[self setUserPresence:MXPresenceUnavailable andStatusMessage:nil completion:^{
if (weakSelf)
{
typeof(self) self = weakSelf;
if (self.backgroundTask.isRunning)
{
[self.backgroundTask stop];
self.backgroundTask = nil;
}
}
}];
}
else
{
// Cancel pending actions
[[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver];
reachabilityObserver = nil;
[initialServerSyncTimer invalidate];
initialServerSyncTimer = nil;
if (mxSession.state == MXSessionStateSyncInProgress || mxSession.state == MXSessionStateInitialised || mxSession.state == MXSessionStateStoreDataReady)
{
// Make sure the SDK finish its work before the app goes sleeping in background
id<MXBackgroundModeHandler> handler = [MXSDKOptions sharedInstance].backgroundModeHandler;
if (handler)
{
if (!self.backgroundTask.isRunning)
{
self.backgroundTask = [handler startBackgroundTaskWithName:@"[MXKAccount] pauseInBackgroundTask" expirationHandler:nil];
}
}
MXLogDebug(@"[MXKAccount] Pause is delayed at the end of sync (current state %tu)", mxSession.state);
isPauseRequested = YES;
}
}
}
- (void)resume
{
isPauseRequested = NO;
if (mxSession)
{
MXLogVerbose(@"[MXKAccount] resume with session state: %tu", mxSession.state);
[self cancelBackgroundSync];
if (mxSession.state == MXSessionStatePaused || mxSession.state == MXSessionStatePauseRequested)
{
// Resume SDK and update user presence
[mxSession resume:^{
[self setUserPresence:MXPresenceOnline andStatusMessage:nil completion:nil];
[self refreshAPNSPusher];
[self refreshPushKitPusher];
}];
}
else if (mxSession.state == MXSessionStateStoreDataReady || mxSession.state == MXSessionStateInitialSyncFailed)
{
// The session initialisation was uncompleted, we try to complete it here.
[self launchInitialServerSync];
[self refreshAPNSPusher];
[self refreshPushKitPusher];
}
else if (mxSession.state == MXSessionStateSyncInProgress)
{
[self refreshAPNSPusher];
[self refreshPushKitPusher];
}
// Cancel background task
if (self.backgroundTask.isRunning)
{
[self.backgroundTask stop];
self.backgroundTask = nil;
}
}
}
- (void)reload:(BOOL)clearCache
{
// close potential session
[self closeSession:clearCache];
if (!_disabled)
{
// Open a new matrix session
id<MXStore> store = [[[MXKAccountManager sharedManager].storeClass alloc] init];
[self openSessionWithStore:store];
}
}
#pragma mark - Push notifications
// Refresh the APNS pusher state for this account on this device.
- (void)refreshAPNSPusher
{
MXLogDebug(@"[MXKAccount][Push] refreshAPNSPusher");
// Check the conditions required to run the pusher
if (self.pushNotificationServiceIsActive)
{
MXLogDebug(@"[MXKAccount][Push] refreshAPNSPusher: Refresh APNS pusher for %@ account", self.mxCredentials.userId);
// Create/restore the pusher
[self enableAPNSPusher:YES
success:nil
failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] ;: Error: %@", error);
}];
}
else if (_hasPusherForPushNotifications)
{
if ([MXKAccountManager sharedManager].apnsDeviceToken)
{
if (mxSession)
{
// Turn off pusher if user denied remote notification.
MXLogDebug(@"[MXKAccount][Push] refreshAPNSPusher: Disable APNS pusher for %@ account (notifications are denied)", self.mxCredentials.userId);
[self enableAPNSPusher:NO success:nil failure:nil];
}
}
else
{
MXLogDebug(@"[MXKAccount][Push] refreshAPNSPusher: APNS pusher for %@ account is already disabled. Reset _hasPusherForPushNotifications", self.mxCredentials.userId);
_hasPusherForPushNotifications = NO;
[[MXKAccountManager sharedManager] saveAccounts];
}
}
}
// Enable/Disable the APNS pusher for this account on this device on the homeserver.
- (void)enableAPNSPusher:(BOOL)enabled success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: %@", @(enabled));
#ifdef DEBUG
NSString *appId = [[NSUserDefaults standardUserDefaults] objectForKey:@"pusherAppIdDev"];
#else
NSString *appId = [[NSUserDefaults standardUserDefaults] objectForKey:@"pusherAppIdProd"];
#endif
NSString *locKey = MXKAppSettings.standardAppSettings.notificationBodyLocalizationKey;
NSDictionary *pushData = @{
@"url": self.pushGatewayURL,
@"format": @"event_id_only",
@"default_payload": @{@"aps": @{@"mutable-content": @(1), @"alert": @{@"loc-key": locKey, @"loc-args": @[]}}}
};
[self enablePusher:enabled appId:appId token:[MXKAccountManager sharedManager].apnsDeviceToken pushData:pushData success:^{
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: Succeeded to update APNS pusher for %@ (%d)", self.mxCredentials.userId, enabled);
self->_hasPusherForPushNotifications = enabled;
[[MXKAccountManager sharedManager] saveAccounts];
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountAPNSActivityDidChangeNotification object:self.mxCredentials.userId];
} failure:^(NSError *error) {
// Ignore error if the client try to disable an unknown token
if (!enabled)
{
// Check whether the token was unknown
MXError *mxError = [[MXError alloc] initWithNSError:error];
if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringUnknown])
{
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: APNS was already disabled for %@!", self.mxCredentials.userId);
// Ignore the error
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountAPNSActivityDidChangeNotification object:self.mxCredentials.userId];
return;
}
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: Failed to disable APNS %@! (%@)", self.mxCredentials.userId, error);
}
else
{
MXLogDebug(@"[MXKAccount][Push] enableAPNSPusher: Failed to send APNS token for %@! (%@)", self.mxCredentials.userId, error);
}
if (failure)
{
failure(error);
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountAPNSActivityDidChangeNotification object:self.mxCredentials.userId];
}];
}
// Refresh the PushKit pusher state for this account on this device.
- (void)refreshPushKitPusher
{
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher");
// Check the conditions required to run the pusher
if (![MXKAppSettings standardAppSettings].allowPushKitPushers)
{
// Turn off pusher if PushKit pushers are not allowed
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: Disable PushKit pusher for %@ account (pushers are not allowed)", self.mxCredentials.userId);
[self enablePushKitPusher:NO success:nil failure:nil];
}
else if (self.isPushKitNotificationActive)
{
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: Refresh PushKit pusher for %@ account", self.mxCredentials.userId);
// Create/restore the pusher
[self enablePushKitPusher:YES
success:nil
failure:^(NSError *error) {
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: Error: %@", error);
}];
}
else if (self.hasPusherForPushKitNotifications)
{
if ([MXKAccountManager sharedManager].pushDeviceToken)
{
if (mxSession)
{
// Turn off pusher if user denied remote notification.
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: Disable PushKit pusher for %@ account (notifications are denied)", self.mxCredentials.userId);
[self enablePushKitPusher:NO success:nil failure:nil];
}
}
else
{
MXLogDebug(@"[MXKAccount][Push] refreshPushKitPusher: PushKit pusher for %@ account is already disabled. Reset _hasPusherForPushKitNotifications", self.mxCredentials.userId);
self->_hasPusherForPushKitNotifications = NO;
[[MXKAccountManager sharedManager] saveAccounts];
}
}
}
// Enable/Disable the pusher based on PushKit for this account on this device on the homeserver.
- (void)enablePushKitPusher:(BOOL)enabled success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: %@", @(enabled));
if (enabled && ![MXKAppSettings standardAppSettings].allowPushKitPushers)
{
// sanity check, if accidently try to enable the pusher
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Do not enable it because PushKit pushers not allowed");
if (failure)
{
failure([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]);
}
return;
}
NSString *appIdKey;
#ifdef DEBUG
appIdKey = @"pushKitAppIdDev";
#else
appIdKey = @"pushKitAppIdProd";
#endif
NSString *appId = [[NSUserDefaults standardUserDefaults] objectForKey:appIdKey];
NSMutableDictionary *pushData = [NSMutableDictionary dictionaryWithDictionary:@{@"url": self.pushGatewayURL}];
NSDictionary *options = [MXKAccountManager sharedManager].pushOptions;
if (options.count)
{
[pushData addEntriesFromDictionary:options];
}
NSData *token = [MXKAccountManager sharedManager].pushDeviceToken;
if (!token)
{
// sanity check, if no token there is no point of calling the endpoint
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Failed to update PushKit pusher to %@ for %@. (token is missing)", @(enabled), self.mxCredentials.userId);
if (failure)
{
failure([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]);
}
return;
}
[self enablePusher:enabled appId:appId token:token pushData:pushData success:^{
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Succeeded to update PushKit pusher for %@. Enabled: %@. Token: %@", self.mxCredentials.userId, @(enabled), [MXKTools logForPushToken:token]);
self->_hasPusherForPushKitNotifications = enabled;
[[MXKAccountManager sharedManager] saveAccounts];
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountPushKitActivityDidChangeNotification object:self.mxCredentials.userId];
} failure:^(NSError *error) {
// Ignore error if the client try to disable an unknown token
if (!enabled)
{
// Check whether the token was unknown
MXError *mxError = [[MXError alloc] initWithNSError:error];
if (mxError && [mxError.errcode isEqualToString:kMXErrCodeStringUnknown])
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Push was already disabled for %@!", self.mxCredentials.userId);
// Ignore the error
if (success)
{
success();
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountPushKitActivityDidChangeNotification object:self.mxCredentials.userId];
return;
}
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Failed to disable Push %@! (%@)", self.mxCredentials.userId, error);
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePushKitPusher: Failed to send Push token for %@! (%@)", self.mxCredentials.userId, error);
}
if (failure)
{
failure(error);
}
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountPushKitActivityDidChangeNotification object:self.mxCredentials.userId];
}];
}
- (void)enablePusher:(BOOL)enabled appId:(NSString*)appId token:(NSData*)token pushData:(NSDictionary*)pushData success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: %@", @(enabled));
// Refuse to try & turn push on if we're not logged in, it's nonsensical.
if (!self.mxCredentials)
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: Not setting push token because we're not logged in");
return;
}
// Check whether the Push Gateway URL has been configured.
if (!self.pushGatewayURL)
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: Not setting pusher because the Push Gateway URL is undefined");
return;
}
if (!appId)
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: Not setting pusher because pusher app id is undefined");
return;
}
NSString *appDisplayName = [NSString stringWithFormat:@"%@ (iOS)", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"]];
NSString *b64Token = [token base64EncodedStringWithOptions:0];
NSString *deviceLang = [NSLocale preferredLanguages][0];
NSString * profileTag = [[NSUserDefaults standardUserDefaults] valueForKey:@"pusherProfileTag"];
if (!profileTag)
{
profileTag = @"";
NSString *alphabet = @"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (int i = 0; i < 16; ++i)
{
unsigned char c = [alphabet characterAtIndex:arc4random() % alphabet.length];
profileTag = [profileTag stringByAppendingFormat:@"%c", c];
}
MXLogDebug(@"[MXKAccount][Push] enablePusher: Generated fresh profile tag: %@", profileTag);
[[NSUserDefaults standardUserDefaults] setValue:profileTag forKey:@"pusherProfileTag"];
}
else
{
MXLogDebug(@"[MXKAccount][Push] enablePusher: Using existing profile tag: %@", profileTag);
}
NSObject *kind = enabled ? @"http" : [NSNull null];
// Use the append flag to handle multiple accounts registration.
BOOL append = NO;
// Check whether a pusher is running for another account
NSArray *activeAccounts = [MXKAccountManager sharedManager].activeAccounts;
for (MXKAccount *account in activeAccounts)
{
if (![account.mxCredentials.userId isEqualToString:self.mxCredentials.userId] && account.pushNotificationServiceIsActive)
{
append = YES;
break;
}
}
MXLogDebug(@"[MXKAccount][Push] enablePusher: append flag: %d", append);
MXRestClient *restCli = self.mxRestClient;
[restCli setPusherWithPushkey:b64Token kind:kind appId:appId appDisplayName:appDisplayName deviceDisplayName:[[UIDevice currentDevice] name] profileTag:profileTag lang:deviceLang data:pushData append:append success:success failure:failure];
}
#pragma mark - InApp notifications
- (void)listenToNotifications:(MXOnNotification)onNotification
{
// Check conditions required to add notification listener
if (!mxSession || !onNotification)
{
return;
}
// Remove existing listener (if any)
[self removeNotificationListener];
// Register on notification center
notificationCenterListener = [self.mxSession.notificationCenter listenToNotifications:^(MXEvent *event, MXRoomState *roomState, MXPushRule *rule)
{
// Apply first the event filter defined in the related room data source
MXKRoomDataSourceManager *roomDataSourceManager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:self->mxSession];
[roomDataSourceManager roomDataSourceForRoom:event.roomId create:NO onComplete:^(MXKRoomDataSource *roomDataSource) {
if (roomDataSource)
{
if (!roomDataSource.eventFormatter.eventTypesFilterForMessages || [roomDataSource.eventFormatter.eventTypesFilterForMessages indexOfObject:event.type] != NSNotFound)
{
// Check conditions to report this notification
if (nil == self->ignoredRooms || [self->ignoredRooms indexOfObject:event.roomId] == NSNotFound)
{
onNotification(event, roomState, rule);
}
}
}
}];
}];
}
- (void)removeNotificationListener
{
if (notificationCenterListener)
{
[self.mxSession.notificationCenter removeListener:notificationCenterListener];
notificationCenterListener = nil;
}
ignoredRooms = nil;
}
- (void)updateNotificationListenerForRoomId:(NSString*)roomID ignore:(BOOL)isIgnored
{
if (isIgnored)
{
if (!ignoredRooms)
{
ignoredRooms = [[NSMutableArray alloc] init];
}
[ignoredRooms addObject:roomID];
}
else if (ignoredRooms)
{
[ignoredRooms removeObject:roomID];
}
}
#pragma mark - Internals
- (void)launchInitialServerSync
{
// Complete the session registration when store data is ready.
// Cancel potential reachability observer and pending action
[[NSNotificationCenter defaultCenter] removeObserver:reachabilityObserver];
reachabilityObserver = nil;
[initialServerSyncTimer invalidate];
initialServerSyncTimer = nil;
// Sanity check
if (!mxSession || (mxSession.state != MXSessionStateStoreDataReady && mxSession.state != MXSessionStateInitialSyncFailed))
{
MXLogDebug(@"[MXKAccount] Initial server sync is applicable only when store data is ready to complete session initialisation");
return;
}
// Use /sync filter corresponding to current settings and homeserver capabilities
MXWeakify(self);
[self buildSyncFilter:^(MXFilterJSONModel *syncFilter) {
MXStrongifyAndReturnIfNil(self);
// Make sure the filter is compatible with the previously used one
MXWeakify(self);
[self checkSyncFilterCompatibility:syncFilter completion:^(BOOL compatible) {
MXStrongifyAndReturnIfNil(self);
if (!compatible)
{
// Else clear the cache
MXLogDebug(@"[MXKAccount] New /sync filter not compatible with previous one. Clear cache");
[self reload:YES];
return;
}
// Launch mxSession
MXWeakify(self);
[self.mxSession startWithSyncFilter:syncFilter onServerSyncDone:^{
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[MXKAccount] %@: The session is ready. Matrix SDK session has been started in %0.fms.", self.mxCredentials.userId, [[NSDate date] timeIntervalSinceDate:self->openSessionStartDate] * 1000);
[self setUserPresence:MXPresenceOnline andStatusMessage:nil completion:nil];
} failure:^(NSError *error) {
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[MXKAccount] Initial Sync failed. Error: %@", error);
BOOL isClientTimeout = [error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorTimedOut;
NSHTTPURLResponse *httpResponse = [MXHTTPOperation urlResponseFromError:error];
BOOL isServerTimeout = httpResponse && [initialSyncSilentErrorsHTTPStatusCodes containsObject:@(httpResponse.statusCode)];
if (isClientTimeout || isServerTimeout)
{
// do not propagate this error to the client
// the request will be retried or postponed according to the reachability status
MXLogDebug(@"[MXKAccount] Initial sync failure did not propagated");
}
else if (self->notifyOpenSessionFailure && error)
{
// Notify MatrixKit user only once
self->notifyOpenSessionFailure = NO;
NSString *myUserId = self.mxSession.myUser.userId;
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKErrorNotification object:error userInfo:myUserId ? @{kMXKErrorUserIdKey: myUserId} : nil];
}
// Check if it is a network connectivity issue
AFNetworkReachabilityManager *networkReachabilityManager = [AFNetworkReachabilityManager sharedManager];
MXLogDebug(@"[MXKAccount] Network reachability: %d", networkReachabilityManager.isReachable);
if (networkReachabilityManager.isReachable)
{
// The problem is not the network
// Postpone a new attempt in 10 sec
self->initialServerSyncTimer = [NSTimer scheduledTimerWithTimeInterval:10 target:self selector:@selector(launchInitialServerSync) userInfo:self repeats:NO];
}
else
{
// The device is not connected to the internet, wait for the connection to be up again before retrying
// Add observer to launch a new attempt according to reachability.
self->reachabilityObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingReachabilityDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
NSNumber *statusItem = note.userInfo[AFNetworkingReachabilityNotificationStatusItem];
if (statusItem)
{
AFNetworkReachabilityStatus reachabilityStatus = statusItem.integerValue;
if (reachabilityStatus == AFNetworkReachabilityStatusReachableViaWiFi || reachabilityStatus == AFNetworkReachabilityStatusReachableViaWWAN)
{
// New attempt
[self launchInitialServerSync];
}
}
}];
}
}];
}];
}];
}
- (void)attemptDeviceDehydrationWithKeyData:(NSData *)keyData
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
[self attemptDeviceDehydrationWithKeyData:keyData retry:YES success:success failure:failure];
}
- (void)attemptDeviceDehydrationWithKeyData:(NSData *)keyData
retry:(BOOL)retry
success:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
if (keyData == nil)
{
MXLogWarning(@"[MXKAccount] attemptDeviceDehydrationWithRetry: no key provided for device dehydration");
if (failure)
{
failure(nil);
}
return;
}
MXLogDebug(@"[MXKAccount] attemptDeviceDehydrationWithRetry: starting device dehydration");
[[MXKAccountManager sharedManager].dehydrationService dehydrateDeviceWithMatrixRestClient:mxRestClient crypto:mxSession.crypto dehydrationKey:keyData success:^(NSString *deviceId) {
MXLogDebug(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device successfully dehydrated");
if (success)
{
success();
}
} failure:^(NSError *error) {
if (retry)
{
[self attemptDeviceDehydrationWithKeyData:keyData retry:NO success:success failure:failure];
MXLogError(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device dehydration failed due to error: %@. Retrying.", error);
}
else
{
MXLogError(@"[MXKAccount] attemptDeviceDehydrationWithRetry: device dehydration failed due to error: %@", error);
if (failure)
{
failure(error);
}
}
}];
}
- (void)onMatrixSessionStateChange
{
if (mxSession.state == MXSessionStateRunning)
{
// Check if pause has been requested
if (isPauseRequested)
{
MXLogDebug(@"[MXKAccount] Apply the pending pause.");
[self pauseInBackgroundTask];
return;
}
// Check whether the session was not already running
if (!userUpdateListener)
{
// Register listener to user's information change
userUpdateListener = [mxSession.myUser listenToUserUpdate:^(MXEvent *event) {
// Consider events related to user's presence
if (event.eventType == MXEventTypePresence)
{
self->userPresence = [MXTools presence:event.content[@"presence"]];
}
// Here displayname or other information have been updated, post update notification.
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}];
// User information are just up-to-date (`mxSession` is running), post update notification.
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
}
else if (mxSession.state == MXSessionStateStoreDataReady || mxSession.state == MXSessionStateSyncInProgress)
{
// Remove listener (if any), this action is required to handle correctly matrix sdk handler reload (see clear cache)
if (userUpdateListener)
{
[mxSession.myUser removeListener:userUpdateListener];
userUpdateListener = nil;
}
else
{
// Here the initial server sync is in progress. The session is not running yet, but some user's information are available (from local storage).
// We post update notification to let observer take into account this user's information even if they may not be up-to-date.
[[NSNotificationCenter defaultCenter] postNotificationName:kMXKAccountUserInfoDidChangeNotification object:self.mxCredentials.userId];
}
}
else if (mxSession.state == MXSessionStatePaused)
{
isPauseRequested = NO;
}
}
- (void)prepareRESTClient
{
if (!self.mxCredentials)
{
return;
}
MXWeakify(self);
mxRestClient = [[MXRestClient alloc] initWithCredentials:self.mxCredentials andOnUnrecognizedCertificateBlock:^BOOL(NSData *certificate) {
MXStrongifyAndReturnValueIfNil(self, NO);
if (_onCertificateChangeBlock)
{
if (_onCertificateChangeBlock (self, certificate))
{
// Update the certificate in credentials
self.mxCredentials.allowedCertificate = certificate;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
return YES;
}
self.mxCredentials.ignoredCertificate = certificate;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
return NO;
} andPersistentTokenDataHandler:^(void (^handler)(NSArray<MXCredentials *> *credentials, void (^completion)(BOOL didUpdateCredentials))) {
[MXKAccountManager.sharedManager readAndWriteCredentials:handler];
} andUnauthenticatedHandler:^(MXError *error, BOOL isSoftLogout, BOOL isRefreshTokenAuth, void (^completion)(void)) {
MXStrongifyAndReturnIfNil(self);
[self handleUnauthenticatedWithError:error isSoftLogout:isSoftLogout isRefreshTokenAuth:isRefreshTokenAuth andCompletion:completion];
}];
}
- (void)handleUnauthenticatedWithError:(MXError *)error isSoftLogout:(BOOL)isSoftLogout isRefreshTokenAuth:(BOOL)isRefreshTokenAuth andCompletion:(void (^)(void))completion
{
[Analytics.shared trackAuthUnauthenticatedErrorWithSoftLogout:isSoftLogout refreshTokenAuth:isRefreshTokenAuth errorCode:error.errcode errorReason:error.error];
MXLogDebug(@"[MXKAccountManager] handleUnauthenticated: trackAuthUnauthenticatedErrorWithSoftLogout sent");
if (isSoftLogout)
{
MXLogDebug(@"[MXKAccountManager] handleUnauthenticated: soft logout.");
[[MXKAccountManager sharedManager] softLogout:self];
completion();
}
else
{
MXLogDebug(@"[MXKAccountManager] handleUnauthenticated: hard logout.");
[[MXKAccountManager sharedManager] removeAccount:self sendLogoutRequest:NO completion:completion];
}
}
- (void)onDateTimeFormatUpdate
{
if ([mxSession.roomSummaryUpdateDelegate isKindOfClass:MXKEventFormatter.class])
{
MXKEventFormatter *eventFormatter = (MXKEventFormatter*)mxSession.roomSummaryUpdateDelegate;
// Update the date and time formatters
[eventFormatter initDateTimeFormatters];
dispatch_group_t dispatchGroup = dispatch_group_create();
for (MXRoom *room in mxSession.rooms)
{
MXRoomSummary *summary = room.summary;
if (summary)
{
dispatch_group_enter(dispatchGroup);
[summary.mxSession eventWithEventId:summary.lastMessage.eventId
inRoom:summary.roomId
success:^(MXEvent *event) {
if (event)
{
if (summary.lastMessage.others == nil)
{
summary.lastMessage.others = [NSMutableDictionary dictionary];
}
summary.lastMessage.others[@"lastEventDate"] = [eventFormatter dateStringFromEvent:event withTime:YES];
2022-01-07 15:30:30 +00:00
[self->mxSession.store.roomSummaryStore storeSummary:summary];
}
dispatch_group_leave(dispatchGroup);
} failure:^(NSError *error) {
MXLogError(@"[MXKAccount] onDateTimeFormatUpdate: event fetch failed: %@", error);
dispatch_group_leave(dispatchGroup);
}];
}
}
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{
// Commit store changes done
if ([self->mxSession.store respondsToSelector:@selector(commit)])
{
[self->mxSession.store commit];
}
// Broadcast the change which concerns all the room summaries.
[[NSNotificationCenter defaultCenter] postNotificationName:kMXRoomSummaryDidChangeNotification object:nil userInfo:nil];
});
}
}
#pragma mark - Crypto
- (void)resetDeviceId
{
self.mxCredentials.deviceId = nil;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
#pragma mark - backgroundSync management
- (void)cancelBackgroundSync
{
if (self.backgroundSyncBgTask.isRunning)
{
MXLogDebug(@"[MXKAccount] The background Sync is cancelled.");
if (mxSession)
{
if (mxSession.state == MXSessionStateBackgroundSyncInProgress)
{
[mxSession pause];
}
}
[self onBackgroundSyncDone:[NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]];
}
}
- (void)onBackgroundSyncDone:(NSError*)error
{
if (backgroundSyncTimer)
{
[backgroundSyncTimer invalidate];
backgroundSyncTimer = nil;
}
if (backgroundSyncFails && error)
{
backgroundSyncFails(error);
}
if (backgroundSyncDone && !error)
{
backgroundSyncDone();
}
backgroundSyncDone = nil;
backgroundSyncFails = nil;
// End background task
if (self.backgroundSyncBgTask.isRunning)
{
[self.backgroundSyncBgTask stop];
self.backgroundSyncBgTask = nil;
}
}
- (void)onBackgroundSyncTimerOut
{
[self cancelBackgroundSync];
}
- (void)backgroundSync:(unsigned int)timeout success:(void (^)(void))success failure:(void (^)(NSError *))failure
{
// Check whether a background mode handler has been set.
id<MXBackgroundModeHandler> handler = [MXSDKOptions sharedInstance].backgroundModeHandler;
if (handler)
{
// Only work when the application is suspended.
// Check conditions before launching background sync
if (mxSession && mxSession.state == MXSessionStatePaused)
{
MXLogDebug(@"[MXKAccount] starts a background Sync");
backgroundSyncDone = success;
backgroundSyncFails = failure;
MXWeakify(self);
self.backgroundSyncBgTask = [handler startBackgroundTaskWithName:@"[MXKAccount] backgroundSync:success:failure:" expirationHandler:^{
MXStrongifyAndReturnIfNil(self);
MXLogDebug(@"[MXKAccount] the background Sync fails because of the bg task timeout");
[self cancelBackgroundSync];
}];
// ensure that the backgroundSync will be really done in the expected time
// the request could be done but the treatment could be long so add a timer to cancel it
// if it takes too much time
backgroundSyncTimer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:(timeout - 1) / 1000]
interval:0
target:self
selector:@selector(onBackgroundSyncTimerOut)
userInfo:nil
repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:backgroundSyncTimer forMode:NSDefaultRunLoopMode];
[mxSession backgroundSync:timeout success:^{
MXLogDebug(@"[MXKAccount] the background Sync succeeds");
[self onBackgroundSyncDone:nil];
}
failure:^(NSError* error) {
MXLogDebug(@"[MXKAccount] the background Sync fails");
[self onBackgroundSyncDone:error];
}
];
}
else
{
MXLogDebug(@"[MXKAccount] cannot start background Sync (invalid state %tu)", mxSession.state);
failure([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]);
}
}
else
{
MXLogDebug(@"[MXKAccount] cannot start background Sync");
failure([NSError errorWithDomain:kMXKAccountErrorDomain code:0 userInfo:nil]);
}
}
#pragma mark - Sync filter
- (void)supportLazyLoadOfRoomMembers:(void (^)(BOOL supportLazyLoadOfRoomMembers))completion
{
void(^onUnsupportedLazyLoadOfRoomMembers)(NSError *) = ^(NSError *error) {
completion(NO);
};
// Check if the server supports LL sync filter
MXFilterJSONModel *filter = [self syncFilterWithLazyLoadOfRoomMembers:YES];
[mxSession.store filterIdForFilter:filter success:^(NSString * _Nullable filterId) {
if (filterId)
{
// The LL filter is already in the store. The HS supports LL
completion(YES);
}
else
{
// Check the Matrix versions supported by the HS
[self.mxSession supportedMatrixVersions:^(MXMatrixVersions *matrixVersions) {
if (matrixVersions.supportLazyLoadMembers)
{
// The HS supports LL
completion(YES);
}
else
{
onUnsupportedLazyLoadOfRoomMembers(nil);
}
} failure:onUnsupportedLazyLoadOfRoomMembers];
}
} failure:onUnsupportedLazyLoadOfRoomMembers];
}
/**
Build the sync filter according to application settings and HS capability.
@param completion the block providing the sync filter to use.
*/
- (void)buildSyncFilter:(void (^)(MXFilterJSONModel *syncFilter))completion
{
// Check settings
BOOL syncWithLazyLoadOfRoomMembersSetting = [MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers;
if (syncWithLazyLoadOfRoomMembersSetting)
{
// Check if the server supports LL sync filter before enabling it
[self supportLazyLoadOfRoomMembers:^(BOOL supportLazyLoadOfRoomMembers) {
if (supportLazyLoadOfRoomMembers)
{
completion([self syncFilterWithLazyLoadOfRoomMembers:YES]);
}
else
{
// No support from the HS
// Disable the setting. That will avoid to make a request at every startup
[MXKAppSettings standardAppSettings].syncWithLazyLoadOfRoomMembers = NO;
completion([self syncFilterWithLazyLoadOfRoomMembers:NO]);
}
}];
}
else
{
completion([self syncFilterWithLazyLoadOfRoomMembers:NO]);
}
}
/**
Compute the sync filter to use according to the device screen size.
@param syncWithLazyLoadOfRoomMembers enable LL support.
@return the sync filter to use.
*/
- (MXFilterJSONModel *)syncFilterWithLazyLoadOfRoomMembers:(BOOL)syncWithLazyLoadOfRoomMembers
{
MXFilterJSONModel *syncFilter;
NSUInteger limit = 10;
// Define a message limit for /sync requests that is high enough so that
// a full page of room messages can be displayed without an additional
// server request.
// This limit value depends on the device screen size. So, the rough rule is:
// - use 10 for small phones (5S/SE)
// - use 15 for phones (6/6S/7/8)
// - use 20 for phablets (.Plus/X/XR/XS/XSMax)
// - use 30 for iPads
UIUserInterfaceIdiom userInterfaceIdiom = [[UIDevice currentDevice] userInterfaceIdiom];
if (userInterfaceIdiom == UIUserInterfaceIdiomPhone)
{
CGFloat screenHeight = [[UIScreen mainScreen] nativeBounds].size.height;
if (screenHeight == 1334) // 6/6S/7/8 screen height
{
limit = 15;
}
else if (screenHeight > 1334)
{
limit = 20;
}
}
else if (userInterfaceIdiom == UIUserInterfaceIdiomPad)
{
limit = 30;
}
// Set that limit in the filter
if (syncWithLazyLoadOfRoomMembers)
{
syncFilter = [MXFilterJSONModel syncFilterForLazyLoadingWithMessageLimit:limit];
}
else
{
syncFilter = [MXFilterJSONModel syncFilterWithMessageLimit:limit];
}
// TODO: We could extend the filter to match other settings (self.showAllEventsInRoomHistory,
// self.eventsFilterForMessages, etc).
return syncFilter;
}
/**
Check the sync filter we want to use is compatible with the one previously used.
@param syncFilter the sync filter to use.
@param completion the block called to indicated the compatibility.
*/
- (void)checkSyncFilterCompatibility:(MXFilterJSONModel*)syncFilter completion:(void (^)(BOOL compatible))completion
{
// There is no compatibility issue if no /sync was done before
if (!mxSession.store.eventStreamToken)
{
completion(YES);
}
// Check the filter we want to use is compatible with the one previously used
else if (!syncFilter && !mxSession.syncFilterId)
{
// A nil filter implies a nil mxSession.syncFilterId. So, there is no filter change
completion(YES);
}
else if (!syncFilter || !mxSession.syncFilterId)
{
// Change from no filter with using a filter or vice-versa. So, there is a filter change
MXLogDebug(@"[MXKAccount] checkSyncFilterCompatibility: Incompatible filter. New or old is nil. mxSession.syncFilterId: %@ - syncFilter: %@",
mxSession.syncFilterId, syncFilter.JSONDictionary);
completion(NO);
}
else
{
// Check the filter is the one previously set
// It must be already in the store
MXWeakify(self);
[mxSession.store filterIdForFilter:syncFilter success:^(NSString * _Nullable filterId) {
MXStrongifyAndReturnIfNil(self);
// Note: We could be more tolerant here
// We could accept filter hot change if the change is limited to the `limit` filter value
// But we do not have this requirement yet
BOOL compatible = [filterId isEqualToString:self.mxSession.syncFilterId];
if (!compatible)
{
MXLogDebug(@"[MXKAccount] checkSyncFilterCompatibility: Incompatible filter ids. mxSession.syncFilterId: %@ - store.filterId: %@ - syncFilter: %@",
self.mxSession.syncFilterId, filterId, syncFilter.JSONDictionary);
}
completion(compatible);
} failure:^(NSError * _Nullable error) {
// Should never happen
completion(NO);
}];
}
}
#pragma mark - Identity server updates
- (void)registerAccountDataDidChangeIdentityServerNotification
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleAccountDataDidChangeIdentityServerNotification:) name:kMXSessionAccountDataDidChangeIdentityServerNotification object:nil];
}
- (void)handleAccountDataDidChangeIdentityServerNotification:(NSNotification*)notification
{
MXSession *mxSession = notification.object;
if (mxSession == self.mxSession)
{
if (![self.mxCredentials.identityServer isEqualToString:self.mxSession.accountDataIdentityServer])
{
_identityServerURL = self.mxSession.accountDataIdentityServer;
self.mxCredentials.identityServer = _identityServerURL;
self.mxCredentials.identityServerAccessToken = nil;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
}
}
#pragma mark - Identity server Access Token updates
- (void)identityService:(MXIdentityService *)identityService didUpdateAccessToken:(NSString *)accessToken
{
self.mxCredentials.identityServerAccessToken = accessToken;
}
- (void)registerIdentityServiceDidChangeAccessTokenNotification
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleIdentityServiceDidChangeAccessTokenNotification:) name:MXIdentityServiceDidChangeAccessTokenNotification object:nil];
}
- (void)handleIdentityServiceDidChangeAccessTokenNotification:(NSNotification*)notification
{
NSDictionary *userInfo = notification.userInfo;
NSString *userId = userInfo[MXIdentityServiceNotificationUserIdKey];
NSString *identityServer = userInfo[MXIdentityServiceNotificationIdentityServerKey];
NSString *accessToken = userInfo[MXIdentityServiceNotificationAccessTokenKey];
if (userId && identityServer && accessToken && [self.mxCredentials.identityServer isEqualToString:identityServer])
{
self.mxCredentials.identityServerAccessToken = accessToken;
// Archive updated field
[[MXKAccountManager sharedManager] saveAccounts];
}
}
@end