Exploration of the Principle of Phantom Trigger in JSBox

Preface

The ghost trigger is Zhong Ying. JSBox In the case of the app process being killed, the notification can also be fixed in the notification bar, even if the user clicks to clear, it can immediately pop up again, never disappear, unless the user closes the notification authority of App or uninstalls App, it can disappear. This function is really interesting, and Zhong Yingda mentioned in the introductory video that JSBox is unique at present. It shows that the implementation is very clever. It is hard to think of if you study it by yourself. It is worth learning. And when you understand the principle of its implementation, you will find that there are many other things you can do. When the product manager is not satisfied with the click-through rate of App push one day, he can sacrifice the killer to her. (Haha, joking, wireless push is not recommended by Apple, because it may be used by some bad Apps, and then push indefinitely to make users disgusted.) The following is for discussion only. JSBox is a very powerful App. There are many things worth learning. It is strongly recommended that you buy and use it.

Short effect video

Complete introductory video

https://weibo.com/tv/v/G79vjv...:1f37179499e39dbc8a7472897b9e056c
Starting in 2 minutes and 6 seconds

Exploration process

Because there is no jailbreak cell phone that can be used to smash the shell, and PP assistant does not have JSBox's package. At first, it searches for ghost trigger, implements infinite notification, and finds no answer. Developers on stack overflow are more interested in infinite notification and have more questions and answers, but nobody gives the answer, basically because Apple doesn't want developers to use this function. To harass users. So we can only read the notification document by ourselves and look up the information to try to achieve it.

Is it implemented using the time interval trigger UNTime Interval Notification Trigger?

Since the notifications are cleared or appear one after another, it's natural to think that it's achieved by bypassing Apple's detection and changing the time Interval attribute of UNTimeIntervalNotification Trigger, so write the code:

UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"Push heading";
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"requestIdentifier" content:content trigger:timeTrigger];
[center addNotificationRequest:request withCompletionHandler:nil];

The first notification can be displayed normally after running. After clearing the first notification and displaying the second notification, app crashes and the time interval can not be less than 60 s.

UserNotificationsDemo[14895:860379] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'time interval must be at least 60 if repeating'
*** First throw call stack:
(0x1ae2a3ea0 0x1ad475a40 0x1ae1b9c1c 0x1aeca7140 0x1b8738d0c 0x1b8738bdc 0x102d508ac 0x1db487658 0x1dad09a18 0x1dad09720 0x1dad0e8e0 0x1dad0f840 0x1dad0e798 0x1dad13684 0x1db057090 0x1b0cd96e4 0x1030ccdc8 0x1030d0a10 0x1b0d17a9c 0x1b0d17728 0x1b0d17d44 0x1ae2341cc 0x1ae23414c 0x1ae233a30 0x1ae22e8fc 0x1ae22e1cc 0x1b04a5584 0x1db471054 0x102d517f0 0x1adceebb4)
libc++abi.dylib: terminating with uncaught exception of type NSException

Time Interval is a read-only property, and Apple seems to have taken precautions.
`@property (NS_NONATOMIC_IOSONLY, readonly) NSTimeInterval timeInterval;
`
But this year, who can still do iOS development alive will not use KVC, so it is natural to think of using KVC to change.

UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"Push heading";
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"requestIdentifier" content:content trigger:timeTrigger];
[timeTrigger setValue:@1 forKey:@"timeInterval"];
[center addNotificationRequest:request withCompletionHandler:nil];

And I interrupted to see that the change was successful.

But soon, when I cleared the first notification, the phone became like this.

For a moment, I was very nervous, I must be a good man, not to change the read-only attributes of father Apple.

Apple only judges when the second notification is displayed, and our code can only control the step of adding the notification request to the UNUser Notification Center, so it's not easy to bypass it.

Is it implemented using the UNLocation Notification Trigger?

UNLocation Notification Trigger can trigger notifications by judging whether a user enters an area and leaves an area, but I looked at the permissions in the settings and found that JSBox did not request permission to locate when using this function only, so it should not be triggered according to the location.

Continue reading the document

Then I went to Zhong Ying Dashen's JSBox community to examine the developer's documentation and look at the api related to notification triggering, and found that

Not through the repeats field, but through the renew field to determine whether to create notifications repeatedly, so it is likely to be achieved through a time trigger, through their own code to create a notification, and then send the notification.
In the minds of most iOS developers (including me before), it is generally believed that when app is in operation, such an implementation scheme is naturally no problem, because we can get notification display, user callbacks to notification operation. When the app is not running, we can't get callbacks unless the user clicks the notification to wake up the app, but after iOS 10, Apple's public User Notifications framework allows developers to handle all kinds of clicks on the notification by implementing the proxy method of UNUser Notification Center. See Apple's article specifically. Handling Notifications and Notification-Related Actions
Translate the main paragraph:

You can handle various clicks on notifications by implementing the proxy method of the UNUser Notification Center. When the user performs some action on the notification, the system starts your app in the background and calls the user Notification Center: didReceiveNotification Response: withCompletionHandler: method implemented by the proxy object of the UNUserNotification Center. The parameter response contains the action identifier of the user's action, even if the notification operation is defined by the system, when the user does the same thing. If you click cancel or open to wake up App for notification, the system will also report these actions.
The core is this approach.

// The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from application:didFinishLaunchingWithOptions:.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __OSX_AVAILABLE(10.14) __TVOS_PROHIBITED;

So I wrote a demo to implement this function. The core code is as follows:

AppDelegate.m

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
    [self applyPushNotificationAuthorization:application];//Request to send notification authorization
    [self addNotificationAction];//Adding custom notification operation extensions
    return YES;
}
//Request to send notification authorization
- (void)applyPushNotificationAuthorization:(UIApplication *)application{
    if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0)) {
        UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
        center.delegate = self;
        [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) {
            if (!error && granted) {
                NSLog(@"login was successful");
            }else{
                NSLog(@"login has failed");
            }

        }];
        [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
            NSLog(@"settings========%@",settings);
        }];
    } else if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)){
        [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound ) categories:nil]];
    }
    [application registerForRemoteNotifications];
}

//Adding custom notification operation extensions
- (void)addNotificationAction {
    UNNotificationAction *openAction = [UNNotificationAction actionWithIdentifier:@"NotificationForeverCategory.action.look" title:@"open App" options:UNNotificationActionOptionForeground];
    UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@"NotificationForeverCategory.action.cancel" title:@"cancel" options:UNNotificationActionOptionDestructive];
    UNNotificationCategory *notificationCategory = [UNNotificationCategory categoryWithIdentifier:@"NotificationForeverCategory" actions:@[openAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction];
    [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:[NSSet setWithObject:notificationCategory]];
}


# pragma mark UNUserNotificationCenterDelegate
//When app is in the foreground, the callback method when notification is about to be displayed will not be displayed if the notification is not implemented.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{
    completionHandler(UNNotificationPresentationOptionBadge|
                      UNNotificationPresentationOptionSound|
                      UNNotificationPresentationOptionAlert);
}

//Callback of user click operation when app is in background or not running state
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler {
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0];
    if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {//Click on the Clear button of the system
        UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.0001f repeats:NO];
        UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
        content.title = @"App explore-NotFound";
        content.body = @"[App explore]JSBox Exploration of the Realization Principle of Medium Phantom Trigger";
        content.badge = @1;
        content.categoryIdentifier = @"NotificationForeverCategory";
        UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger];
        [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil];
    }
    completionHandler();
}

- (void)applicationWillResignActive:(UIApplication *)application {
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
}

- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

- (void)applicationWillEnterForeground:(UIApplication *)application {
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}

- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}

- (void)applicationWillTerminate:(UIApplication *)application {
    // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];
    [button addTarget:self action:@selector(sendNotification) forControlEvents:UIControlEventTouchUpInside];
    [button setTitle:@"Send a 3 s Notification shown later" forState:UIControlStateNormal];
    button.frame = CGRectMake(0, 200, [UIScreen mainScreen].bounds.size.width, 100);
    [self.view addSubview:button];
}

//Send a notification
- (void)sendNotification {
    UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:3.0f repeats:NO];
    UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
    content.title = @"App explore-NotFound";
    content.body = @"[App explore]JSBox Exploration of the Realization Principle of Medium Phantom Trigger";
    content.badge = @1;
    content.categoryIdentifier = @"NotificationForeverCategory";
    UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@"requestIdentifier" content:content trigger:timeTrigger];
    UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
    [center addNotificationRequest:request withCompletionHandler: nil];
}

You must set up a proxy for the notification center before the didFinish Launching WithOptions method returns. It is mentioned in this document, as you all know, but there are two difficulties that are not mentioned in this document.

Hidden level 1 must add custom notification operations to notifications

1. A custom notification operation must be added to the notification, and a categoryIdentifier of the custom notification operation must be specified to the notification sent, so that the proxy method will be invoked only when the user operates on the notification.
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler
Custom notification operation is the action Sheet that pops up below the user's notification. In our Demo, the two operations are "Open App" and "Cancel". In fact, without adding these customized operations, the system's "management", "View" and "Clear" are also available, but when the user clicks "Clear", our proxy method is Receive Notifications Re. Ponse won't be called. It's not mentioned in the document. I tried it for a long time.

Hidden Level 2 must use the request Identifier of the previous notification

When the user clicks the Clean button, even if the app is not running, the system will run our app in the background and execute the didReceive Notification Response proxy method, in which we will create an UNNotification Request, add it to the notification center, and the notification will be displayed. However, the system seems to distinguish the UNNotification Request added in the normal operation of the app from the UNNotification Request added in the didReceiveNotification Response method, which does not receive the didReceiveNotification Response callback method after the user clicks the "Clear" button. It may also take into account that developers may use this mechanism to achieve none. Limit the function of notification. So when I create UNNotification Request, the identifier used is the identifier of the previous notification, which is also the most ingenious way to achieve unlimited notification. Maybe many developers know how to implement this proxy method to accept callbacks when users click "Clear", and then do some notification reporting, send notifications again at intervals, but create and send them again. The notification will no longer execute the didReceive Notification Response callback when it is clicked "Clear".


        UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger];

extend

If we do App of efficiency tool type, we can use this function to do some functions such as fixed notification. If we do App of information type, we can do some functions of indefinite interval push, instead of reporting the user operation to the server through the network request after each user clicks "clear", and then the server sends the push to the user according to the situation. More ways to play are yet to be explored.

Demo https://github.com/577528249/...

Demo demo Gif

Writing articles is too time-consuming, if you can, please give me some attention, will regularly write original articles, thank you!

Keywords: Swift iOS shell Attribute less

Added by tomkleijkers on Thu, 16 May 2019 02:40:21 +0300