/* Copyright 2024 New Vector Ltd. Copyright 2020 Vector Creations Ltd Copyright 2014 OpenMarket Ltd Copyright (c) 2021 BWI GmbH SPDX-License-Identifier: AGPL-3.0-only Please see LICENSE in the repository root for full details. */ #import "PushNotificationService.h" #import #import "GeneratedInterface-Swift.h" @interface PushNotificationService() /** Matrix session observer used to detect new opened sessions. */ @property (nonatomic, weak) id matrixSessionStateObserver; @property (nonatomic, nullable, copy) void (^registrationForRemoteNotificationsCompletion)(NSError *); @property (nonatomic, strong) PKPushRegistry *pushRegistry; @property (nonatomic, strong) PushNotificationStore *pushNotificationStore; /// Should PushNotificationService receive VoIP pushes @property (nonatomic, assign) BOOL shouldReceiveVoIPPushes; @end @implementation PushNotificationService - (instancetype)initWithPushNotificationStore:(PushNotificationStore *)pushNotificationStore { if (self = [super init]) { self.pushNotificationStore = pushNotificationStore; if (BWIBuildSettings.shared.allowVoIPUsage) { _pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; } self.shouldReceiveVoIPPushes = YES; } return self; } #pragma mark - Public Methods - (void)registerUserNotificationSettings { MXLogDebug(@"[PushNotificationService][Push] registerUserNotificationSettings: isPushRegistered: %@", @(_isPushRegistered)); if (!_isPushRegistered) { UNTextInputNotificationAction *quickReply = [UNTextInputNotificationAction actionWithIdentifier:@"inline-reply" title:[VectorL10n roomMessageShortPlaceholder] options:UNNotificationActionOptionAuthenticationRequired ]; UNNotificationCategory *quickReplyCategory = [UNNotificationCategory categoryWithIdentifier:@"QUICK_REPLY" actions:@[quickReply] intentIdentifiers:@[] options:UNNotificationCategoryOptionNone]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center setNotificationCategories:[[NSSet alloc] initWithArray:@[quickReplyCategory]]]; [center setDelegate:self]; UNAuthorizationOptions authorizationOptions = (UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge); [center requestAuthorizationWithOptions:authorizationOptions completionHandler:^(BOOL granted, NSError *error) { // code here is equivalent to self:application:didRegisterUserNotificationSettings: if (granted) { [self registerForRemoteNotificationsWithCompletion:nil]; } else { // Clear existing token [self clearPushNotificationToken]; } }]; } } - (void)registerForRemoteNotificationsWithCompletion:(nullable void (^)(NSError *))completion { self.registrationForRemoteNotificationsCompletion = completion; // BWI #7555 migration part 3 if([[BWIBuildSettings shared] BuMXMigrationInfoLevel] < 2) { dispatch_async(dispatch_get_main_queue(), ^{ // Even after we unregistered from Apples push service we must make sure that // the app will not register again. After reaching migration level 3 this app should neither show // remote notifications any more nor register for new notifications. [[UIApplication sharedApplication] registerForRemoteNotifications]; }); } // BWI #7555 END } - (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { MXLogDebug(@"[PushNotificationService][Push] didRegisterForRemoteNotificationsWithDeviceToken"); MXKAccountManager* accountManager = [MXKAccountManager sharedManager]; [accountManager setApnsDeviceToken:deviceToken]; // Resurrect old PushKit token to better kill it if (!accountManager.pushDeviceToken) { // If we don't have the pushDeviceToken, we may have migrated it into the shared user defaults. NSString *pushDeviceToken = [MXKAppSettings.standardAppSettings.sharedUserDefaults objectForKey:@"pushDeviceToken"]; if (pushDeviceToken) { MXLogDebug(@"[PushNotificationService][Push] didRegisterForRemoteNotificationsWithDeviceToken: Move PushKit token to user defaults"); // Set the token in standard user defaults, as MXKAccount will read it from there when removing the pusher. // This will allow to remove the PushKit pusher in the next step [[NSUserDefaults standardUserDefaults] setObject:pushDeviceToken forKey:@"pushDeviceToken"]; [MXKAppSettings.standardAppSettings.sharedUserDefaults removeObjectForKey:@"pushDeviceToken"]; [MXKAppSettings.standardAppSettings.sharedUserDefaults removeObjectForKey:@"pushOptions"]; } } // If we already have pushDeviceToken or recovered it in above step, remove its PushKit pusher if (accountManager.pushDeviceToken) { MXLogDebug(@"[PushNotificationService][Push] didRegisterForRemoteNotificationsWithDeviceToken: A PushKit pusher still exists. Remove it"); // Attempt to remove PushKit pushers explicitly [self clearPushNotificationToken]; } _isPushRegistered = YES; if (!_pushNotificationStore.pushKitToken) { [self configurePushKit]; } if (self.registrationForRemoteNotificationsCompletion) { self.registrationForRemoteNotificationsCompletion(nil); self.registrationForRemoteNotificationsCompletion = nil; } } - (void)didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { [self clearPushNotificationToken]; if (self.registrationForRemoteNotificationsCompletion) { self.registrationForRemoteNotificationsCompletion(error); self.registrationForRemoteNotificationsCompletion = nil; } } - (void)didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { MXLogDebug(@"[PushNotificationService][Push] didReceiveRemoteNotification: applicationState: %tu - payload: %@", [UIApplication sharedApplication].applicationState, userInfo); completionHandler(UIBackgroundFetchResultNewData); } - (void)deregisterRemoteNotifications { _isPushRegistered = NO; self.shouldReceiveVoIPPushes = NO; } - (void)applicationWillResignActive { [[UNUserNotificationCenter currentNotificationCenter] removeUnwantedNotifications]; [[UNUserNotificationCenter currentNotificationCenter] removeCallNotificationsFor:nil]; if (_pushNotificationStore.pushKitToken) { self.shouldReceiveVoIPPushes = YES; } } - (void)applicationDidEnterBackground { } - (void)applicationDidBecomeActive { [[UNUserNotificationCenter currentNotificationCenter] removeUnwantedNotifications]; [[UNUserNotificationCenter currentNotificationCenter] removeCallNotificationsFor:nil]; if (_pushNotificationStore.pushKitToken) { self.shouldReceiveVoIPPushes = NO; } } - (void)checkPushKitPushersInSession:(MXSession*)session { [session.matrixRestClient pushers:^(NSArray *pushers) { MXLogDebug(@"[PushNotificationService][Push] checkPushKitPushers: %@ has %@ pushers:", session.myUserId, @(pushers.count)); for (MXPusher *pusher in pushers) { MXLogDebug(@" - %@", pusher.appId); // We do not want anymore PushKit pushers the app used to use if ([pusher.appId isEqualToString:BuildSettings.pushKitAppIdProd] || [pusher.appId isEqualToString:BuildSettings.pushKitAppIdDev]) { [self removePusher:pusher inSession:session]; } } } failure:^(NSError *error) { MXLogDebug(@"[PushNotificationService][Push] checkPushKitPushers: Error: %@", error); }]; } #pragma mark - Private Methods - (void)setShouldReceiveVoIPPushes:(BOOL)shouldReceiveVoIPPushes { _shouldReceiveVoIPPushes = shouldReceiveVoIPPushes; MXLogDebug(@"[PushNotificationService] setShouldReceiveVoIPPushes: %u", _shouldReceiveVoIPPushes) if (_shouldReceiveVoIPPushes && _pushNotificationStore.pushKitToken) { MXSession *session = [AppDelegate theDelegate].mxSessions.firstObject; if (session.state >= MXSessionStateStoreDataReady) { [self configurePushKit]; } else { // add an observer for session state MXWeakify(self); NSNotificationCenter * __weak notificationCenter = [NSNotificationCenter defaultCenter]; self.matrixSessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { MXStrongifyAndReturnIfNil(self); MXSession *mxSession = (MXSession*)notif.object; if ([[AppDelegate theDelegate].mxSessions containsObject:mxSession] && mxSession.state >= MXSessionStateStoreDataReady && self->_shouldReceiveVoIPPushes) { [self configurePushKit]; [notificationCenter removeObserver:self.matrixSessionStateObserver]; } }]; } } else { [self deconfigurePushKit]; } } - (void)configurePushKit { MXLogDebug(@"[PushNotificationService] configurePushKit") NSData* token = [_pushRegistry pushTokenForType:PKPushTypeVoIP]; if (token) { // If the token is available, store it. This can happen if you sign out and back in. // i.e We are registered, but we have cleared it from the the store on logout and the // _pushRegistry lives through signin/signout as PushNotificationService is a singleton // on app delegate. _pushNotificationStore.pushKitToken = token; MXLogDebug(@"[PushNotificationService] configurePushKit: Restored pushKit token") } _pushRegistry.delegate = self; _pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; } - (void)deconfigurePushKit { MXLogDebug(@"[PushNotificationService] deconfigurePushKit") _pushRegistry.delegate = nil; } - (void)removePusher:(MXPusher*)pusher inSession:(MXSession*)session { MXLogDebug(@"[PushNotificationService][Push] removePusher: %@", pusher.appId); // Shortcut MatrixKit and its complex logic and call directly the API [session.matrixRestClient setPusherWithPushkey:pusher.pushkey kind:[NSNull null] // This is how we remove a pusher appId:pusher.appId appDisplayName:pusher.appDisplayName deviceDisplayName:pusher.deviceDisplayName profileTag:pusher.profileTag lang:pusher.lang data:pusher.data.JSONDictionary append:NO success:^{ MXLogDebug(@"[PushNotificationService][Push] removePusher: Success"); // Brute clean remaining MatrixKit data [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushDeviceToken"]; [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"pushOptions"]; } failure:^(NSError *error) { MXLogDebug(@"[PushNotificationService][Push] removePusher: Error: %@", error); }]; } - (void)launchBackgroundSync { // Launch a background sync for all existing matrix sessions NSArray *mxAccounts = [MXKAccountManager sharedManager].activeAccounts; for (MXKAccount *account in mxAccounts) { MXLogDebug(@"[PushNotificationService] launchBackgroundSync"); [account backgroundSync:20000 success:^{ [[UNUserNotificationCenter currentNotificationCenter] removeUnwantedNotifications]; [[UNUserNotificationCenter currentNotificationCenter] removeCallNotificationsFor:nil]; MXLogDebug(@"[PushNotificationService] launchBackgroundSync: the background sync succeeds"); } failure:^(NSError *error) { MXLogDebug(@"[PushNotificationService] launchBackgroundSync: the background sync failed. Error: %@ (%@).", error.domain, @(error.code)); }]; } } #pragma mark - UNUserNotificationCenterDelegate - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler { NSDictionary *userInfo = notification.request.content.userInfo; if (userInfo[Constants.userInfoKeyPresentNotificationOnForeground]) { if (!userInfo[Constants.userInfoKeyPresentNotificationInRoom] && [[AppDelegate theDelegate].visibleRoomId isEqualToString:userInfo[@"room_id"]]) { // do not show the notification when we're in the notified room completionHandler(UNNotificationPresentationOptionNone); } else { completionHandler(UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionList); } } else { completionHandler(UNNotificationPresentationOptionNone); } } // iOS 10+, see application:handleActionWithIdentifier:forLocalNotification:withResponseInfo:completionHandler: - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler { UNNotification *notification = response.notification; UNNotificationContent *content = notification.request.content; NSString *actionIdentifier = [response actionIdentifier]; NSString *roomId = content.userInfo[@"room_id"]; NSString *threadId = content.userInfo[@"thread_id"]; NSString *userId = content.userInfo[@"user_id"]; if ([actionIdentifier isEqualToString:@"inline-reply"]) { if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) { UNTextInputNotificationResponse *textInputNotificationResponse = (UNTextInputNotificationResponse *)response; NSString *responseText = [textInputNotificationResponse userText]; [self handleNotificationInlineReplyForRoomId:roomId threadId:threadId withResponseText:responseText success:^(NSString *eventId) { completionHandler(); } failure:^(NSError *error) { UNMutableNotificationContent *failureNotificationContent = [[UNMutableNotificationContent alloc] init]; failureNotificationContent.userInfo = content.userInfo; failureNotificationContent.body = [VectorL10n roomEventFailedToSend]; failureNotificationContent.threadIdentifier = roomId; NSString *uuid = [[NSUUID UUID] UUIDString]; UNNotificationRequest *failureNotificationRequest = [UNNotificationRequest requestWithIdentifier:uuid content:failureNotificationContent trigger:nil]; [center addNotificationRequest:failureNotificationRequest withCompletionHandler:nil]; MXLogDebug(@"[PushNotificationService][Push] didReceiveNotificationResponse: error sending text message: %@", error); completionHandler(); }]; } else { MXLogDebug(@"[PushNotificationService][Push] didReceiveNotificationResponse: error, expect a response of type UNTextInputNotificationResponse"); completionHandler(); } } else if ([actionIdentifier isEqualToString:UNNotificationDefaultActionIdentifier]) { [self notifyNavigateToRoomById:roomId threadId:threadId sender:userId]; completionHandler(); } else { MXLogDebug(@"[PushNotificationService][Push] didReceiveNotificationResponse: unhandled identifier %@", actionIdentifier); completionHandler(); } } #pragma mark - Other Methods - (void)handleNotificationInlineReplyForRoomId:(NSString*)roomId threadId:(NSString*)threadId withResponseText:(NSString*)responseText success:(void(^)(NSString *eventId))success failure:(void(^)(NSError *error))failure { if (!roomId.length) { failure(nil); return; } NSArray* mxAccounts = [MXKAccountManager sharedManager].activeAccounts; __block MXSession *mxSession; dispatch_group_t dispatchGroupSession = dispatch_group_create(); for (MXKAccount* account in mxAccounts) { void(^storeDataReadyBlock)(void) = ^{ MXRoom *room = [account.mxSession roomWithRoomId:roomId]; if (room) { mxSession = account.mxSession; } }; if (account.mxSession.state >= MXSessionStateStoreDataReady) { storeDataReadyBlock(); if (mxSession) { break; } } else { dispatch_group_enter(dispatchGroupSession); // wait for session state to be store data ready id sessionStateObserver = nil; sessionStateObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kMXSessionStateDidChangeNotification object:account.mxSession queue:nil usingBlock:^(NSNotification * _Nonnull note) { if (mxSession) { [[NSNotificationCenter defaultCenter] removeObserver:sessionStateObserver]; return; } if (account.mxSession.state >= MXSessionStateStoreDataReady) { [[NSNotificationCenter defaultCenter] removeObserver:sessionStateObserver]; storeDataReadyBlock(); dispatch_group_leave(dispatchGroupSession); } }]; } } dispatch_group_notify(dispatchGroupSession, dispatch_get_main_queue(), ^{ if (mxSession == nil) { MXLogDebug(@"[PushNotificationService][Push] didReceiveNotificationResponse: room with id %@ not found", roomId); failure(nil); } else { // initialize data source for a thread or a room __block MXKRoomDataSource *dataSource; dispatch_group_t dispatchGroupDataSource = dispatch_group_create(); if (RiotSettings.shared.enableThreads && threadId) { dispatch_group_enter(dispatchGroupDataSource); [ThreadDataSource loadRoomDataSourceWithRoomId:roomId initialEventId:nil threadId:threadId andMatrixSession:mxSession onComplete:^(MXKRoomDataSource *threadDataSource) { dataSource = threadDataSource; dispatch_group_leave(dispatchGroupDataSource); }]; } else { dispatch_group_enter(dispatchGroupDataSource); MXKRoomDataSourceManager *manager = [MXKRoomDataSourceManager sharedManagerForMatrixSession:mxSession]; [manager roomDataSourceForRoom:roomId create:YES onComplete:^(MXKRoomDataSource *roomDataSource) { dataSource = roomDataSource; dispatch_group_leave(dispatchGroupDataSource); }]; } dispatch_group_notify(dispatchGroupDataSource, dispatch_get_main_queue(), ^{ if (responseText != nil && responseText.length != 0) { NSString *logForThread = threadId ? [NSString stringWithFormat:@", thread: %@", threadId] : nil; MXLogDebug(@"[PushNotificationService][Push] didReceiveNotificationResponse: sending message to room: %@%@", roomId, logForThread); [dataSource sendTextMessage:responseText success:^(NSString* eventId) { success(eventId); } failure:^(NSError* error) { failure(error); }]; } else { failure(nil); } }); } }); } - (void)clearPushNotificationToken { MXLogDebug(@"[PushNotificationService][Push] clearPushNotificationToken: Clear existing token"); // Clear existing pushkit token registered on the HS MXKAccountManager* accountManager = [MXKAccountManager sharedManager]; [accountManager setPushDeviceToken:nil withPushOptions:nil]; } // Remove delivred notifications for a given room id except call notifications - (void)removeDeliveredNotificationsWithRoomId:(NSString*)roomId completion:(dispatch_block_t)completion { MXLogDebug(@"[PushNotificationService][Push] removeDeliveredNotificationsWithRoomId: Remove potential delivered notifications for room id: %@", roomId); NSMutableArray *notificationRequestIdentifiersToRemove = [NSMutableArray new]; UNUserNotificationCenter *notificationCenter = [UNUserNotificationCenter currentNotificationCenter]; [notificationCenter getDeliveredNotificationsWithCompletionHandler:^(NSArray * _Nonnull notifications) { for (UNNotification *notification in notifications) { NSString *threadIdentifier = notification.request.content.threadIdentifier; if ([threadIdentifier isEqualToString:roomId]) { [notificationRequestIdentifiersToRemove addObject:notification.request.identifier]; } } [notificationCenter removeDeliveredNotificationsWithIdentifiers:notificationRequestIdentifiersToRemove]; if (completion) { completion(); } }]; } #pragma mark - Delegate Notifiers - (void)notifyNavigateToRoomById:(NSString *)roomId threadId:(NSString *)threadId sender:(NSString *)userId { if ([_delegate respondsToSelector:@selector(pushNotificationService:shouldNavigateToRoomWithId:threadId:sender:)]) { [_delegate pushNotificationService:self shouldNavigateToRoomWithId:roomId threadId:threadId sender:userId]; } } #pragma mark - PKPushRegistryDelegate - (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type { MXLogDebug(@"[PushNotificationService] did update PushKit credentials"); _pushNotificationStore.pushKitToken = pushCredentials.token; if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { self.shouldReceiveVoIPPushes = NO; } } - (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion { MXLogDebug(@"[PushNotificationService] didReceiveIncomingPushWithPayload: %@", payload.dictionaryPayload); NSString *roomId = payload.dictionaryPayload[@"room_id"]; NSString *eventId = payload.dictionaryPayload[@"event_id"]; [[UNUserNotificationCenter currentNotificationCenter] removeUnwantedNotifications]; [[UNUserNotificationCenter currentNotificationCenter] removeCallNotificationsFor:roomId]; if (@available(iOS 13.0, *)) { // for iOS 13, we'll just report the incoming call in the same runloop. It means we cannot call an async API here. MXEvent *callInvite = [_pushNotificationStore callInviteForEventId:eventId]; // remove event [_pushNotificationStore removeCallInviteWithEventId:eventId]; MXSession *session = [AppDelegate theDelegate].mxSessions.firstObject; // when we have a VoIP push while the application is killed, session.callManager will not be ready yet. Configure it. [[AppDelegate theDelegate] configureCallManagerIfRequiredForSession:session]; MXLogDebug(@"[PushNotificationService] didReceiveIncomingPushWithPayload: iOS 13+, callInvite: %@", callInvite); if (callInvite) { // We're using this dispatch_group to continue event stream after cache fully processed. dispatch_group_t dispatchGroup = dispatch_group_create(); dispatch_group_enter(dispatchGroup); session.spaceService.graphUpdateEnabled = NO; // Not continuing in completion block here, because PushKit mandates reporting a new call in the same run loop. // 'handleBackgroundSyncCacheIfRequiredWithCompletion' is processing to-device events synchronously. [session handleBackgroundSyncCacheIfRequiredWithCompletion:^{ session.spaceService.graphUpdateEnabled = YES; dispatch_group_leave(dispatchGroup); }]; if (callInvite.eventType == MXEventTypeCallInvite) { // process the call invite synchronously [session.callManager handleCallEvent:callInvite]; MXCallInviteEventContent *content = [MXCallInviteEventContent modelFromJSON:callInvite.content]; MXCall *call = [session.callManager callWithCallId:content.callId]; if (call) { [session.callManager.callKitAdapter reportIncomingCall:call]; MXLogDebug(@"[PushNotificationService] didReceiveIncomingPushWithPayload: Reporting new call in room %@ for the event: %@", roomId, eventId); // Wait for the sync response in cache to be processed for data integrity. dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^{ // After reporting the call, we can continue async. Launch a background sync to handle call answers/declines on other devices of the user. [self launchBackgroundSync]; }); } else { MXLogDebug(@"[PushNotificationService] didReceiveIncomingPushWithPayload: Error on call object on room %@ for the event: %@", roomId, eventId); } } else if ([callInvite.type isEqualToString:kWidgetMatrixEventTypeString] || [callInvite.type isEqualToString:kWidgetModularEventTypeString]) { [[AppDelegate theDelegate].callPresenter processWidgetEvent:callInvite inSession:session]; } else { // It's a serious error. There is nothing to avoid iOS to kill us here. MXLogDebug(@"[PushNotificationService] didReceiveIncomingPushWithPayload: We have an unknown type of event for %@. There is something wrong.", eventId); } } else { // It's a serious error. There is nothing to avoid iOS to kill us here. MXLogDebug(@"[PushNotificationService] didReceiveIncomingPushWithPayload: iOS 13+, but we don't have the callInvite event for the eventId: %@.", eventId); } } else { if ([UIApplication sharedApplication].applicationState == UIApplicationStateActive) { // below iOS 13, we don't have to report a call immediately. // We can wait for a call invite from event stream and process. MXLogDebug(@"[PushNotificationService] didReceiveIncomingPushWithPayload: Below iOS 13 and active app. Do nothing."); completion(); return; } // below iOS 13, we can call an async API. After background sync, we'll hopefully fetch the call invite and report a new call to the CallKit. [self launchBackgroundSync]; } completion(); } @end