12

Since the iOS framework doesn't allow local notifications to execute code before they are posted, I'm looking for a way to achieve it on a jailbroken device.

  • Is there built in functionally on a jailbroken device to schedule code execution with no need for the user to interact?
  • The code should download updates and determine if the user should receive a notification.
  • I don't want to use push notifications, which requires an external server to push them to the user.

Update

Well, I've managed to create a daemon which launches on start-up and keeps itself running. However, posting notifications requires the UIApplication object. According to the documentation this singleton is created by the UIApplicationMain() method which, for a regular application is called by main(). Since I want the notification be posted by a daemon, the singleton is nil.

Can I create an instance of UIApplication? Or post the notification any other way?

I've tried calling UIApplicationMain() and then posting the notification in the app delegate, as well as killing the application, but this shows a black screen for a moment; I guess its launching the application. Moreover, it causes the daemon to crash when app launching is impossible (when the phone is yet to fully boot).

Here is a sketch of the code

int main(){
   if(launchedBySpringBoard || launchedBynotification)
      UIApplicationMain(...);
   else if(launchedByDaeamon)
      StartRunLoop();
}

void triggerdByRunLoopEveryXhours(){
    downloadData();
    if(isNewData())
       postNotification();
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
Kirill Kulakov
  • 10,035
  • 9
  • 50
  • 67
  • what does your code do? – nielsbot Mar 16 '13 at 21:28
  • I created a daemon which starts a NSRunLoop, this loop triggers a method which download some data and determines if notification posting is needed. – Kirill Kulakov Mar 16 '13 at 22:00
  • @KirillKulakov: Why do you have launchedBySpringBoard, launchedByNotification and launhedByDaemon? If I am not mistaken, your problems break down to two things: How to run in background continuously (this will allow you to execute some code without a user interaction) and how to show local notification when something happened (I was under impression that you are mainly interested in this, just to let user know that something has ahppened). I don't think you need to check all these flags, because if you are running continuosly then your app just will be brought to a front. – Victor Ronin Mar 17 '13 at 01:36

3 Answers3

12

... Or post the notification any other way?

Yes. You can make this work with a background (launch) daemon that triggers a notification (not necessarily a UILocalNotification). When the notification shows the user an alert, your daemon can then decide to open a normal UI application (or not).

Build a Launch Daemon.

This is the best tutorial I've found. The launch daemon starts when the phone boots, and runs all the time as a non-graphical background process. From there, you can schedule your check for updates. (I have a HelloDaemon class which does all its work in the run: method):

int main(int argc, char *argv[]) {
    @autoreleasepool {
        HelloDaemon* daemon = [[HelloDaemon alloc] init];
        
        // start a timer so that the process does not exit.
        NSTimer* timer = [[NSTimer alloc] initWithFireDate: [NSDate date]
                                                  interval: 1.0
                                                    target: daemon
                                                  selector: @selector(run:)
                                                  userInfo: nil
                                                   repeats: NO];
        
        NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
        [runLoop addTimer: timer forMode: NSDefaultRunLoopMode];
        [runLoop run];
    }    
    return 0;
}

Daemons can use NSTimer normally, so schedule another timer (within run:) to check for updates to download whenever you want.

Notify User from Daemon

If the daemon decides that the user should be notified, then you can either:

1) open the full UI application.

#include <dlfcn.h>
#define SBSERVPATH "/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices"

-(void) openApp {
  
    // the SpringboardServices.framework private framework can launch apps,
    //  so we open it dynamically and find SBSLaunchApplicationWithIdentifier()
    void* sbServices = dlopen(SBSERVPATH, RTLD_LAZY);
    int (*SBSLaunchApplicationWithIdentifier)(CFStringRef identifier, Boolean suspended) = dlsym(sbServices, "SBSLaunchApplicationWithIdentifier");
    int result = SBSLaunchApplicationWithIdentifier(CFSTR("com.mycompany.AppName"), false);
    dlclose(sbServices);
}

This code requires the com.apple.springboard.launchapplications entitlement for your daemon to use it successfully. See here for adding an entitlement. You'd need an entitlements.xml file for your daemon executable, like this:

<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>com.apple.springboard.launchapplications</key>
        <true/>
    </dict>
</plist>

2) show a simple alert window from your daemon, notifying the user of the event, and prompting them to open the UI app

#include "CFUserNotification.h"

-(void) showAlert {
  
    NSMutableDictionary* dict = [NSMutableDictionary dictionary];
    [dict setObject: @"Alert!" forKey: (__bridge NSString*)kCFUserNotificationAlertHeaderKey];
    [dict setObject: @"Updates Ready!" forKey: (__bridge NSString*)kCFUserNotificationAlertMessageKey];
    [dict setObject: @"View" forKey:(__bridge NSString*)kCFUserNotificationDefaultButtonTitleKey];
    [dict setObject: @"Cancel" forKey:(__bridge NSString*)kCFUserNotificationAlternateButtonTitleKey];
    
    SInt32 error = 0;
    CFUserNotificationRef alert =
    CFUserNotificationCreate(NULL, 0, kCFUserNotificationPlainAlertLevel, &error, (__bridge CFDictionaryRef)dict);
    
    CFOptionFlags response;
    // we block, waiting for a response, for up to 10 seconds
    if((error) || (CFUserNotificationReceiveResponse(alert, 10, &response))) {
        NSLog(@"alert error or no user response after 10 seconds");
    } else if((response & 0x3) == kCFUserNotificationAlternateResponse) {
        // user clicked on Cancel ... just do nothing
        NSLog(@"cancel");
    } else if((response & 0x3) == kCFUserNotificationDefaultResponse) {
        // user clicked on View ... so, open the UI App
        NSLog(@"view");
        [self openApp];
    }
    CFRelease(alert);
}

You'll need a CFUserNotification.h header to use the code the way I did above. You can find one by googling, or see one here. This older wiki document also shows some good information for using CFUserNotification from iOS apps.

The answer I linked to from KennyTM above also shows how you can make your alert popup show, even if the device is locked.

Community
  • 1
  • 1
Nate
  • 31,017
  • 13
  • 83
  • 207
  • Great, this bring me some ideas, anyway I prefer the notification to wait for the user in the notification center rather than popup right up. – Kirill Kulakov Mar 17 '13 at 20:05
  • @KirillKulakov, there are problems trying to use `UILocalNotification` in jailbreak **apps**, not just daemons. [See this other, unanswered question](http://stackoverflow.com/q/11216225/119114). So far, I haven't figured out how to use them directly. If you have additional requirements, please edit your question above and put them in for others to see. Ideally, you would do this when you first ask your question, so we don't spend time giving you answers that don't meet your requirements. Thanks. – Nate Mar 17 '13 at 21:04
  • Well, your answer is quite helpful, and having a pop-up window instead of a notification is still better than nothing. – Kirill Kulakov Mar 19 '13 at 08:29
  • @KirillKulakov, unfortunately, in *jailbreak* development, I frequently find myself with only *better-than-nothing* solutions :) – Nate Mar 19 '13 at 08:34
  • @Nate: I did :) Quick question though: the answer you linked to about getting the alert to show up on the lock screen worked, with some modifications, but I can't figure out how to get my NSTimer to trigger to show the alert on the lock screen, if the device has been locked for more than 15 seconds or so. Do you know how that's possible? – drewmm Mar 30 '13 at 20:12
  • @drewmm, I'd be happy to help, but the comments section isn't a great place to do so. Please post a new full-blown question, link to the code or answers you use, explain the problem, and apply appropriate tags to the question. If you'd like to call my attention to the question, post a link to it in a comment here. Thanks! – Nate Mar 30 '13 at 20:21
  • 1
    @Nate: got it, just thought it might be a quick answer and was relevant to this question. I've added a new question here: http://stackoverflow.com/questions/15723432/is-it-possible-to-use-an-nstimer-to-wake-a-jailbroken-iphone-from-deep-sleep – drewmm Mar 30 '13 at 21:00
5

First of all, let me say that BigLex is giving quite interesting information. However, I never tried to write a deamon for jailbroken iphone. So, I am not aware of limitations (and it looks like there are some - like UIApplication sharedApplication is nil.

Couple of thoughts:

Backgrounding

1) In the case if you plan to distribute through Cydia (meaning that applications will end up being on the system volume) you can use two undocument background modes:

"continuos" (this one will keep running in the background) "unboundedTaskCompletion" (this one will have unlimited time, if you will do [UIApplication beginBackgroundTaskWithExpirationHandler]

You can take a look at example Info.plist here, which uses continouse.

2) There are other ways to get permanent background (which don't require even device to be jailbroken).

As example, common method is to run silent audio on the loop. Here is example how to do this.

Just be aware that this method won't be accept to App Store.

3) In the case, if you device to go with route 1) or 2), you will have access to [UIApplication sharedApplication) to post local notifications

4) You may be interested to take a look at the Backgrounder. I believe it implemented backgrounding capabilities for jailbroken devices. However, it may be outdate.

Daemon problems with UIApplication

5) Regarding problems with daemon. If you read carefully that article you will see

The first thing to note is that it is not good to use the UIApplication class to start your daemon (it takes more memory than we need), so we are going to write our own main method.

So, the code there was optimized for a memory. However, I am pretty sure that you can replace it with the common iOS application code:

int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

As result, I think you should have UIApplication singleton and should be able to post local notification.

Yeah... It will eat up addition X kilobytes of memory, but who cares (if you aren't running 100 of such daemons)

Victor Ronin
  • 22,758
  • 18
  • 92
  • 184
  • 1
    running silent audio will surely hurt battery life--and could get your app rejected. (I suspect playing audio turns on the audio amps... I know this is true on the Mac). probably have to go jailbreak. – nielsbot Mar 16 '13 at 22:05
  • well, I'll check 'Backgrounder' and 'beginBackgroundTaskWithExpirationHandler', but I guess it won't be as good as a daemon, which is started on and restarts it if killed. (I hope I'm wrong) – Kirill Kulakov Mar 16 '13 at 22:09
  • 1
    +1 to Victor. I didn't know about `continuous` or `unboundedTaskCompletion`! To Kirill, if you do still want to stick with the *launch daemon* concept, [my answer has additional info on that option](http://stackoverflow.com/a/15455596/119114). – Nate Mar 16 '13 at 23:04
  • @nielsbot: True. However, the question is for jailbreak. – Victor Ronin Mar 17 '13 at 01:29
  • @Kirill: Yes. I believe daemons should be restarted by system automatically. – Victor Ronin Mar 17 '13 at 01:31
  • @KirillKulakov: BTW. If you add VOIP background mode then your application will be started after reboot and restarted automatically. – Victor Ronin Mar 17 '13 at 01:38
  • @victor sure, but you specifically said it wouldn't require a jail break implying it might be acceptable in an App Store app... Just clarifying that it probably wouldn't work. – nielsbot Mar 17 '13 at 01:42
  • @nielsbot: got it. you are correct. It won't be accepted to App store. However, I am correct either. I will run on the jailed device (just an app should be signed with enterprise cert or used only while development). I will add your note into my answer. – Victor Ronin Mar 17 '13 at 17:59
  • +100 more coming to Victor. Worth it for the bit about `continuous` and `unboundedTaskCompletion`! – Nate Mar 22 '13 at 20:47
2

just guessing, this is not a real answer but maybe you could use MobileSubstrate's hooking feature to hook up in the OS's notification handling process and tell the os to execute some code to check if the notification comes from your app and, if that's the case, check for an update and decide if it should show the notification?

Or maybe you could start a background process that every X minutes checks if there is any update and if so sets an immediate local notification. Not sure how you could do this though.

BigLex
  • 2,978
  • 5
  • 19
  • 27
  • Do you have any examples related to MobileSubstrate's hooking? about the background process is is battery efficient? – Kirill Kulakov Feb 22 '13 at 13:42
  • Take a look at this for a simple Mobile Substrate hooking tutorial: http://ios-blog.co.uk/tutorials/how-to-create-a-mobilesubstrate-tweaks-for-ios/ and here's some info about theos (the development environment you should use to hook without many troubles) http://iphonedevwiki.net/index.php/Theos/Getting_Started#Purpose Unfortunately you'll need some research to find out where exactly to hook up in the OS as probably the notification handling process is not very documented. – BigLex Feb 22 '13 at 13:46
  • As for battery efficiency of the second method, I don't think it should be a big deal in terms of battery life (of course it depends on how much you check for notifications) but I'm not sure about that either. Anyways if you find out a way to start a background process (and I'm pretty sure this can be done) the second method IMHO is way easier than the first one as you won't need to find where in the iOS to hook up to. – BigLex Feb 22 '13 at 13:52
  • 1
    http://chrisalvares.com/blog/7/creating-an-iphone-daemon-part-1/ this tutorial explains how to create a background deamon – BigLex Feb 22 '13 at 14:02