// // UnityNotificationManager.m // iOS.notifications // #if TARGET_OS_IOS #import "UnityNotificationManager.h" #if UNITY_USES_LOCATION #import #endif @implementation UnityNotificationManager { NSLock* _lock; UNAuthorizationStatus _remoteNotificationsRegistered; NSString* _deviceToken; NSPointerArray* _pendingRemoteAuthRequests; } + (instancetype)sharedInstance { static UnityNotificationManager *sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[UnityNotificationManager alloc] init]; }); [sharedInstance updateNotificationSettings]; [sharedInstance updateScheduledNotificationList]; return sharedInstance; } - (id)init { _lock = [[NSLock alloc] init]; _remoteNotificationsRegistered = UNAuthorizationStatusNotDetermined; _deviceToken = nil; _pendingRemoteAuthRequests = nil; return self; } - (void)finishAuthorization:(struct iOSNotificationAuthorizationData*)authData forRequest:(void*)request { if (self.onAuthorizationCompletionCallback != NULL && request) self.onAuthorizationCompletionCallback(request, *authData); } - (void)finishRemoteNotificationRegistration:(UNAuthorizationStatus)status notification:(NSNotification*)notification { struct iOSNotificationAuthorizationData authData; authData.granted = status == UNAuthorizationStatusAuthorized; authData.error = NULL; authData.deviceToken = NULL; NSString* deviceToken = nil; if (authData.granted) { deviceToken = [UnityNotificationManager deviceTokenFromNotification: notification]; authData.deviceToken = [deviceToken UTF8String]; } [_lock lock]; _remoteNotificationsRegistered = status; _deviceToken = deviceToken; NSPointerArray* pointers = _pendingRemoteAuthRequests; _pendingRemoteAuthRequests = nil; [_lock unlock]; while (pointers.count > 0) { unsigned long idx = pointers.count - 1; void* request = [pointers pointerAtIndex: idx]; [pointers removePointerAtIndex: idx]; [self finishAuthorization: &authData forRequest: request]; } } - (void)requestAuthorization:(NSInteger)authorizationOptions withRegisterRemote:(BOOL)registerRemote forRequest:(void*)request { if (!SYSTEM_VERSION_10_OR_ABOVE) return; UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; BOOL supportsPushNotification = [[[NSBundle mainBundle] objectForInfoDictionaryKey: @"UnityAddRemoteNotificationCapability"] boolValue]; registerRemote = registerRemote && supportsPushNotification; [center requestAuthorizationWithOptions: authorizationOptions completionHandler:^(BOOL granted, NSError * _Nullable error) { BOOL authorizationRequestFinished = YES; struct iOSNotificationAuthorizationData authData; authData.granted = granted; authData.error = [[error localizedDescription]cStringUsingEncoding: NSUTF8StringEncoding]; authData.deviceToken = ""; if (granted) { [_lock lock]; if (registerRemote && _remoteNotificationsRegistered == UNAuthorizationStatusNotDetermined) { authorizationRequestFinished = NO; if (request) { if (_pendingRemoteAuthRequests == nil) _pendingRemoteAuthRequests = [NSPointerArray pointerArrayWithOptions: NSPointerFunctionsOpaqueMemory]; [_pendingRemoteAuthRequests addPointer: request]; } dispatch_async(dispatch_get_main_queue(), ^{ [[UIApplication sharedApplication] registerForRemoteNotifications]; }); } else authData.deviceToken = [_deviceToken UTF8String]; [_lock unlock]; } else NSLog(@"Requesting notification authorization failed with: %@", error); if (authorizationRequestFinished) [self finishAuthorization: &authData forRequest: request]; [self updateNotificationSettings]; }]; } + (NSString*)deviceTokenFromNotification:(NSNotification*)notification { NSData* deviceTokenData; if ([notification.userInfo isKindOfClass: [NSData class]]) deviceTokenData = (NSData*)notification.userInfo; else return nil; NSUInteger len = deviceTokenData.length; if (len == 0) return nil; const unsigned char *buffer = deviceTokenData.bytes; NSMutableString *str = [NSMutableString stringWithCapacity: (len * 2)]; for (int i = 0; i < len; ++i) [str appendFormat: @"%02x", buffer[i]]; return str; } // Called when a notification is delivered to a foreground app. - (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler { iOSNotificationData notificationData; BOOL haveNotificationData = NO; if (self.onNotificationReceivedCallback != NULL) { notificationData = UNNotificationRequestToiOSNotificationData(notification.request); haveNotificationData = YES; self.onNotificationReceivedCallback(notificationData); } BOOL showInForeground; NSInteger presentationOptions; if ([notification.request.trigger isKindOfClass: [UNPushNotificationTrigger class]]) { if (self.onRemoteNotificationReceivedCallback != NULL) { if (!haveNotificationData) { notificationData = UNNotificationRequestToiOSNotificationData(notification.request); haveNotificationData = YES; } showInForeground = NO; self.onRemoteNotificationReceivedCallback(notificationData); } else { showInForeground = YES; presentationOptions = self.remoteNotificationForegroundPresentationOptions; } } else { presentationOptions = [[notification.request.content.userInfo objectForKey: @"showInForegroundPresentationOptions"] intValue]; showInForeground = [[notification.request.content.userInfo objectForKey: @"showInForeground"] boolValue]; } if (haveNotificationData) freeiOSNotificationData(¬ificationData); if (showInForeground) completionHandler(presentationOptions); else completionHandler(UNNotificationPresentationOptionNone); [[UnityNotificationManager sharedInstance] updateDeliveredNotificationList]; } // Called to let your app know which action was selected by the user for a given notification. - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(nonnull void(^)(void))completionHandler { self.lastReceivedNotification = response.notification; self.lastRespondedNotificationAction = response.actionIdentifier; if ([response isKindOfClass: UNTextInputNotificationResponse.class]) { UNTextInputNotificationResponse* resp = (UNTextInputNotificationResponse*)response; self.lastRespondedNotificationUserText = resp.userText; } else self.lastRespondedNotificationUserText = nil; completionHandler(); [[UnityNotificationManager sharedInstance] updateDeliveredNotificationList]; } - (void)updateScheduledNotificationList { UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; [center getPendingNotificationRequestsWithCompletionHandler:^(NSArray * _Nonnull requests) { self.cachedPendingNotificationRequests = requests; }]; } - (void)updateDeliveredNotificationList { UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; [center getDeliveredNotificationsWithCompletionHandler:^(NSArray * _Nonnull notifications) { self.cachedDeliveredNotifications = notifications; }]; } - (void)updateNotificationSettings { UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { self.cachedNotificationSettings = settings; }]; } bool validateAuthorizationStatus(UnityNotificationManager* manager) { UNAuthorizationStatus authorizationStatus = manager.cachedNotificationSettings.authorizationStatus; if (authorizationStatus == UNAuthorizationStatusAuthorized) return true; if (@available(iOS 12.0, *)) { if (authorizationStatus == UNAuthorizationStatusProvisional) return true; } NSLog(@"Attempting to schedule a local notification without authorization, please call RequestAuthorization first."); return false; } - (void)scheduleLocalNotification:(iOSNotificationData*)data { if (!validateAuthorizationStatus(self)) return; assert(self.onNotificationReceivedCallback != NULL); NSDictionary* userInfo = (__bridge_transfer NSDictionary*)data->userInfo; data->userInfo = NULL; // Convert from iOSNotificationData to UNMutableNotificationContent. UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; // iOS 10 does not show notifications with an empty body or title fields. // Since this works fine on iOS 11+ we'll add assign a string with a space to maintain consistent behaviour. NSString *dataTitle, *dataBody; if (@available(iOS 11.0, *)) { dataTitle = data->title ? [NSString stringWithUTF8String: data->title] : [NSString string]; dataBody = data->body ? [NSString stringWithUTF8String: data->body] : [NSString string]; } else { dataTitle = data->title && data->title[0] ? [NSString stringWithUTF8String: data->title] : @" "; dataBody = data->body && data->body[0] ? [NSString stringWithUTF8String: data->body] : @" "; } content.title = [NSString localizedUserNotificationStringForKey: dataTitle arguments: nil]; content.body = [NSString localizedUserNotificationStringForKey: dataBody arguments: nil]; content.userInfo = userInfo; if (data->badge >= 0) content.badge = [NSNumber numberWithInt: data->badge]; if (data->subtitle != NULL) content.subtitle = [NSString localizedUserNotificationStringForKey: [NSString stringWithUTF8String: data->subtitle] arguments: nil]; if (data->categoryIdentifier != NULL) content.categoryIdentifier = [NSString stringWithUTF8String: data->categoryIdentifier]; if (data->threadIdentifier != NULL) content.threadIdentifier = [NSString stringWithUTF8String: data->threadIdentifier]; UNNotificationSound* sound = [self soundForNotification: data]; if (sound != nil) content.sound = sound; content.attachments = (__bridge_transfer NSArray*)data->attachments; data->attachments = NULL; NSString* identifier = [NSString stringWithUTF8String: data->identifier]; // Generate UNNotificationTrigger from iOSNotificationData. UNNotificationTrigger* trigger; if (data->triggerType == TIME_TRIGGER) { trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval: data->trigger.timeInterval.interval repeats: data->trigger.timeInterval.repeats]; } else if (data->triggerType == CALENDAR_TRIGGER) { NSDateComponents* date = [[NSDateComponents alloc] init]; if (data->trigger.calendar.year >= 0) date.year = data->trigger.calendar.year; if (data->trigger.calendar.month >= 0) date.month = data->trigger.calendar.month; if (data->trigger.calendar.day >= 0) date.day = data->trigger.calendar.day; if (data->trigger.calendar.hour >= 0) date.hour = data->trigger.calendar.hour; if (data->trigger.calendar.minute >= 0) date.minute = data->trigger.calendar.minute; if (data->trigger.calendar.second >= 0) date.second = data->trigger.calendar.second; // From C# we get UTC time date.calendar = [NSCalendar calendarWithIdentifier: NSCalendarIdentifierGregorian]; date.timeZone = [NSTimeZone timeZoneWithAbbreviation: @"UTC"]; trigger = [UNCalendarNotificationTrigger triggerWithDateMatchingComponents: date repeats: data->trigger.calendar.repeats]; } else if (data->triggerType == LOCATION_TRIGGER) { #if UNITY_USES_LOCATION CLLocationCoordinate2D center = CLLocationCoordinate2DMake(data->trigger.location.latitude, data->trigger.location.longitude); CLCircularRegion* region = [[CLCircularRegion alloc] initWithCenter: center radius: data->trigger.location.radius identifier: identifier]; region.notifyOnEntry = data->trigger.location.notifyOnEntry; region.notifyOnExit = data->trigger.location.notifyOnExit; trigger = [UNLocationNotificationTrigger triggerWithRegion: region repeats: data->trigger.location.repeats]; #else return; #endif } else { return; } UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier: identifier content: content trigger: trigger]; // Schedule the notification. UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; [center addNotificationRequest: request withCompletionHandler:^(NSError * _Nullable error) { if (error != NULL) NSLog(@"%@", [error localizedDescription]); [self updateScheduledNotificationList]; }]; } - (UNNotificationSound*)soundForNotification:(const iOSNotificationData*)data { NSString* soundName = nil; if (data->soundName != NULL) soundName = [NSString stringWithUTF8String: data->soundName]; switch (data->soundType) { case kSoundTypeNone: return nil; case kSoundTypeCritical: if (@available(iOS 12.0, *)) { if (soundName != nil) { if (data->soundVolume < 0) return [UNNotificationSound criticalSoundNamed: soundName]; return [UNNotificationSound criticalSoundNamed: soundName withAudioVolume: data->soundVolume]; } if (data->soundVolume >= 0) return [UNNotificationSound defaultCriticalSoundWithAudioVolume: data->soundVolume]; return UNNotificationSound.defaultCriticalSound; } else goto default_fallback; case kSoundTypeRingtone: if (@available(iOS 15.2, *)) { if (soundName != nil) return [UNNotificationSound ringtoneSoundNamed: soundName]; return UNNotificationSound.defaultRingtoneSound; } // continue to default case kSoundTypeDefault: default: default_fallback: if (soundName != nil) return [UNNotificationSound soundNamed: soundName]; return UNNotificationSound.defaultSound; } } @end #endif