diff --git a/native/ios/NotificationService/NotificationService.mm b/native/ios/NotificationService/NotificationService.mm --- a/native/ios/NotificationService/NotificationService.mm +++ b/native/ios/NotificationService/NotificationService.mm @@ -1,7 +1,9 @@ #import "NotificationService.h" #import "Logger.h" #import "NotificationsCryptoModule.h" +#import "PlatformSpecificTools.h" #import "TemporaryMessageStorage.h" +#import NSString *const backgroundNotificationTypeKey = @"backgroundNotifType"; NSString *const messageInfosKey = @"messageInfos"; @@ -14,11 +16,31 @@ CFStringRef newMessageInfosDarwinNotification = CFSTR("app.comm.darwin_new_message_infos"); +// Implementation below was inspired by the +// following discussion with Apple staff member: +// https://developer.apple.com/forums/thread/105088 +size_t getMemoryUsage() { + task_vm_info_data_t vmInfo; + mach_msg_type_number_t count = TASK_VM_INFO_COUNT; + + kern_return_t result = + task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&vmInfo, &count); + + if (result != KERN_SUCCESS) { + return -1; + } + + size_t memory_usage = static_cast(vmInfo.phys_footprint); + // We divide to get the result in MB's + return memory_usage / (1024 * 1024); +} + @interface NotificationService () @property(nonatomic, strong) void (^contentHandler) (UNNotificationContent *contentToDeliver); @property(nonatomic, strong) UNMutableNotificationContent *bestAttemptContent; +@property BOOL contentHandlerCalled; @end @@ -30,16 +52,30 @@ contentHandler { self.contentHandler = contentHandler; self.bestAttemptContent = [request.content mutableCopy]; + self.contentHandlerCalled = NO; + dispatch_source_t memorySource = [self registerForMemoryEvents]; + + // Step 1: notification decryption. if ([self shouldBeDecrypted:self.bestAttemptContent.userInfo]) { - @try { - [self decryptBestAttemptContent]; - } @catch (NSException *e) { - comm::Logger::log( - "NSE: Received exception: " + std::string([e.name UTF8String]) + - " with reason: " + std::string([e.reason UTF8String]) + - " during notification decryption"); - self.contentHandler([[UNNotificationContent alloc] init]); + std::string decryptErrorMessage; + try { + @try { + [self decryptBestAttemptContent]; + } @catch (NSException *e) { + decryptErrorMessage = "NSE: Received Obj-C exception: " + + std::string([e.name UTF8String]) + + " during notification decryption."; + } + } catch (const std::exception &e) { + decryptErrorMessage = + "NSE: Received C++ exception: " + std::string(e.what()) + + " during notification decryption."; + } + + if (decryptErrorMessage.size()) { + [self callContentHandlerOnErrorMessage: + [NSString stringWithUTF8String:decryptErrorMessage.c_str()]]; return; } } else if ([self shouldAlertUnencryptedNotification:self.bestAttemptContent @@ -49,28 +85,73 @@ comm::Logger::log("NSE: Received erroneously unencrypted notitication."); } - [self persistMessagePayload:self.bestAttemptContent.userInfo]; + // Step 2: notification persistence in a temporary storage + std::string persistErrorMessage; + try { + @try { + [self persistMessagePayload:self.bestAttemptContent.userInfo]; + } @catch (NSException *e) { + persistErrorMessage = + "NSE: Received Obj-C exception: " + std::string([e.name UTF8String]) + + " during notification persistence."; + } + } catch (const std::exception &e) { + persistErrorMessage = + "NSE: Received C++ exception: " + std::string(e.what()) + + " during notification persistence."; + } + + if (persistErrorMessage.size()) { + [self callContentHandlerOnErrorMessage: + [NSString stringWithUTF8String:persistErrorMessage.c_str()]]; + return; + } + + // Step 3: (optional) rescind read notifications + // Message payload persistence is a higher priority task, so it has // to happen prior to potential notification center clearing. if ([self isRescind:self.bestAttemptContent.userInfo]) { - [self removeNotificationWithIdentifier:self.bestAttemptContent - .userInfo[@"notificationId"]]; - self.contentHandler([[UNNotificationContent alloc] init]); + std::string rescindErrorMessage; + try { + @try { + [self + removeNotificationWithIdentifier:self.bestAttemptContent + .userInfo[@"notificationId"]]; + } @catch (NSException *e) { + rescindErrorMessage = "NSE: Received Obj-C exception: " + + std::string([e.name UTF8String]) + " during notification rescind."; + } + } catch (const std::exception &e) { + rescindErrorMessage = + "NSE: Received C++ exception: " + std::string(e.what()) + + " during notification rescind."; + } + + if (rescindErrorMessage.size()) { + [self callContentHandlerOnErrorMessage: + [NSString stringWithUTF8String:rescindErrorMessage.c_str()]]; + return; + } + [self threadSafeContentHandlerCall:[[UNNotificationContent alloc] init]]; return; } + // Step 4: (optional) update badge count for badge only notif if ([self isBadgeOnly:self.bestAttemptContent.userInfo]) { UNMutableNotificationContent *badgeOnlyContent = [[UNMutableNotificationContent alloc] init]; badgeOnlyContent.badge = self.bestAttemptContent.badge; - self.contentHandler(badgeOnlyContent); + [self threadSafeContentHandlerCall:badgeOnlyContent]; return; } + // Step 5: notify main app that there is data + // to transfer to SQLite and redux. [self sendNewMessageInfosNotification]; // TODO modify self.bestAttemptContent here - - self.contentHandler(self.bestAttemptContent); + [self unregisterForMemoryEventsFrom:memorySource]; + [self threadSafeContentHandlerCall:self.bestAttemptContent]; } - (void)serviceExtensionTimeWillExpire { @@ -82,8 +163,9 @@ // remove relevant notification from notification center in // in time given to NSE to process notification. // It is an extremely unlikely to happen. - comm::Logger::log("NSE: Exceeded time limit to rescind a notification."); - self.contentHandler([[UNNotificationContent alloc] init]); + NSString *errorMessage = + @"NSE: Exceeded time limit to rescind a notification."; + [self callContentHandlerOnErrorMessage:errorMessage]; return; } if ([self shouldBeDecrypted:self.bestAttemptContent.userInfo] && @@ -91,8 +173,9 @@ // If we get to this place it means we were unable to // decrypt encrypted notification content in time // given to NSE to process notification. - comm::Logger::log("NSE: Exceeded time limit to decrypt a notification."); - self.contentHandler([[UNNotificationContent alloc] init]); + NSString *errorMessage = + @"NSE: Exceeded time limit to decrypt a notification."; + [self callContentHandlerOnErrorMessage:errorMessage]; return; } self.contentHandler(self.bestAttemptContent); @@ -260,4 +343,60 @@ self.bestAttemptContent.userInfo = mutableUserInfo; } +- (void)callContentHandlerOnErrorMessage:(NSString *)errorMessage { + comm::Logger::log(std::string([errorMessage UTF8String])); + + if (!comm::PlatformSpecificTools::isStaffRelease()) { + [self threadSafeContentHandlerCall:[[UNNotificationContent alloc] init]]; + return; + } + + UNMutableNotificationContent *content = + [[UNMutableNotificationContent alloc] init]; + content.body = errorMessage; + [self threadSafeContentHandlerCall:content]; +} + +- (void)threadSafeContentHandlerCall:(UNNotificationContent *)content { + @synchronized(self) { + if (self.contentHandlerCalled) { + return; + } + self.contentHandlerCalled = YES; + self.contentHandler(content); + } +} + +// Monitor memory usage +- (dispatch_source_t)registerForMemoryEvents { + dispatch_source_t memorySource = dispatch_source_create( + DISPATCH_SOURCE_TYPE_MEMORYPRESSURE, + 0L, + DISPATCH_MEMORYPRESSURE_CRITICAL, + dispatch_get_main_queue()); + + __weak __typeof(self) weakSelf = self; + dispatch_block_t eventHandler = ^{ + __typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + NSString *criticalMemoryEventMessage = + [NSString stringWithFormat: + @"NSE: Received CRITICAL memory event. Memory usage: %ld", + getMemoryUsage()]; + + [strongSelf callContentHandlerOnErrorMessage:criticalMemoryEventMessage]; + }; + + dispatch_source_set_event_handler(memorySource, eventHandler); + dispatch_activate(memorySource); + return memorySource; +} + +- (void)unregisterForMemoryEventsFrom:(dispatch_source_t)source { + dispatch_source_cancel(source); +} + @end