setKeepAliveTimeout and BackgroundTasks

19.8k views Asked by At

I've a big headache with the topic. I'm working on an application that needs to poll a webserver regularly, in order to check for new data. Based on the returned information, I wish to push a local notification to the user.

I know that this approach is slightly different from the one depicted by Apple, in which a remote server makes the work, pushing a remote notification, based on APNS. However, there are many reasons for which i cannot take this approach in consideration. One for all, is the user authentication mechanism. The remote server, for security reasons, cannot take into account the user credentials. All that i can do is to move the login and fetching core, to the client (iPhone).

I noticed that Apple offers an opportunity for applications to wake-up and keep opened a Socket connection (ie. a VoIP application).

So, I started investigate in this way. Added the required information in the plist, I'm able to "wake" my application, using something like this in my appDelegate:

[[UIApplication sharedApplication] setKeepAliveTimeout:1200 handler:^{ 
    NSLog(@"startingKeepAliveTimeout");
    [self contentViewLog:@"startingKeepAliveTimeout"];
    MyPushOperation *op = [[MyPushOperation alloc] initWithNotificationFlag:0 andDataSource:nil];
    [queue addOperation:op];
    [op release];
}];

The NSOperation, then starts a background task using the following block code:

#pragma mark SyncRequests
-(void) main {
    NSLog(@"startSyncRequest");
    [self contentViewLog:@"startSyncRequest"];
    bgTask = [app beginBackgroundTaskWithExpirationHandler:^{ 
        NSLog(@"exipiration handler triggered");
        [app endBackgroundTask:bgTask];
        bgTask = UIBackgroundTaskInvalid;
        [self cancel];
    }];


        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSMutableURLRequest *anURLRequest;
            NSURLResponse *outResponse;
            NSError *exitError;
            NSString *username;
            NSString *password;

            NSLog(@"FirstLogin");
            anURLRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:webserverLogin, username, password]]];
            [anURLRequest setHTTPMethod:@"GET"];
            [anURLRequest setTimeoutInterval:120.00];
            [anURLRequest setCachePolicy:NSURLRequestReloadIgnoringCacheData];

            exitError = nil;
            NSData *tmpData = [NSURLConnection sendSynchronousRequest:anURLRequest returningResponse:&outResponse error:&exitError];
            [anURLRequest setTimeoutInterval:120.00];
            if(exitError != nil) { //somethings goes wrong
                NSLog(@"somethings goes wrong");
                [app endBackgroundTask:bgTask];
                bgTask = UIBackgroundTaskInvalid;
                [self cancel];
                return;
            }

            //do some stuff with NSData and prompt the user with a UILocalNotification

            NSLog(@"AlltasksCompleted");
            [app endBackgroundTask:bgTask];
            bgTask = UIBackgroundTaskInvalid;
            [self cancel];
        });
    }
}

The above code seems to work (sometimes), but many other it crashes my application, with the following log information:

Exception Type:  00000020
Exception Codes: 0x8badf00d
Highlighted Thread:  3

Application Specific Information:
DemoBackApp[5977] has active assertions beyond permitted time: 
{(
    <SBProcessAssertion: 0xa9da0b0> identifier: UIKitBackgroundCompletionTask process: DemoBackApp[5977] permittedBackgroundDuration: 600.000000 reason: finishTask owner pid:5977 preventSuspend  preventIdleSleep 
)}

Elapsed total CPU time (seconds): 0.010 (user 0.010, system 0.000), 100% CPU 
Elapsed application CPU time (seconds): 0.000, 0% CPU

For ones who ask, yes. I've tried the Async NSURLConnection approach, too. No matter. It crash the same, even if I use an async approach with timeout handler and didFinishLoading:WithError.

I'm stuck. Any hints are high appreciated.

4

There are 4 answers

6
Jason Coco On

When you call -setKeepAliveTimeout:handler:, you only get a maximum of 30 seconds to complete everything and suspend. You're not given the same background grace period as you'd be given when your application is first transitioned to the background. That's meant for finishing up long running tasks, shutting things down, etc.

With the VOIP callback, you're just supposed to send whatever ping packet you need to send to your service to keep the network connection alive and not timed-out. After 30 seconds, regardless of starting new background tasks, if your application is still executing, you're gonna be terminated.

Also, it's important to note that if you're not actually a VOIP application or if you do anything during the VOIP callback window that's not related to keeping your network connections open, your app will be rejected from the app store. When you set any of the stay-active flags (VOIP, background music, navigation), they test it pretty rigorously to ensure that it only does what it's flagged to do while in the background. Doing any kind of HTTP GET request and waiting for some large data update to come back is almost guaranteed to get your app rejected.

EDIT: As noted by Patrick in the comments, the current amount of time the block is given to execute has been reduced from 30 seconds to 10 seconds with iOS 5. It's a good idea to keep an eye on these times whenever you re-link your application for a new version of the SDK, always at least quickly check the docs in case they've been updated (with iOS 6 coming out, this number may be tweaked again).

2
BadPirate On

It seems that you may be able to combine the keep alive timeout handler by piggy-backing a request for finite background task execution when you get called. This will allow you a full 10 minutes every time the VOIP keep alive handler gets called (vice the usual 10-30 seconds).

Still same problem as above -- in that you need the VOIP flag in your plist to submit, and Apple isn't likely to accept your application if you have that flag and are not actually a VOIP application, but for internal distribution (enterprise or otherwise), this solution should work fine for giving you background time.

In my tests, the 10 minute finite execution timer is reset every time the VOIP handler is called (whether or not the user has brought the application to the front since then), and this should mean that you could poll your server indefinitely while in the background once every 600 seconds (10 minutes) and the polling process can take up to 10 minutes before going to sleep (meaning nearly constant background operation if that is what you require).

Again, not really an option for the App Store unless you can convince them you are VOIP.

0
mattv123 On

This is an old thread, but may warrant an update.

As of iOS 6, this is the behavior I am seeing with the VoIP timer background methods as discussed in this thread:

  • The VoIP BackgroundMode is still strictly banned from AppStore apps via the App Review Process
  • The minimum KeepAlive time is 600 seconds; anything less than that will cause the handler to fail to install (and a warning is sent to NSLog)
  • Setting the keepAlive time to something significantly larger than 600 seconds will typically result in the handler being fired with a frequency of every time/2 interval. Bonus fact: This is consistent with the SIP REGISTER request, in which the recommended re-registration interval is .5*re-register time.
  • When your keepAlive handler is called, I have observed the following:
    • You get about 10 seconds of "foreground" execution time, in which remaining background time is infinite (as returned by backgroundTimeRemaining)
    • If you kicked off a beginBackgroundTask from within the keepAlive handler, I have observed that you get 60 seconds of background execution time (as returned by backgroundTimeRemaining). This is different than the 600 seconds you get when the user transitions from your app being active to backgrounded. I have not found any way to extend this time (without using other trick like location etc.)

Hope that helps!

0
d0n13 On

Just to update this behaviour for iOS7.

  • When your app first goes into the background you get 180 seconds of task time as reported by backgroundTimeRemaining. However, it stops responding at 5 seconds before this time runs out as reported by backgroundTimeRemaining.
  • When the keepAlive task fires then the backgroundTimeRemaining is 10 seconds of foreground followed by 60 seconds of background timer as reported by backgroundTimeRemaining. It also stops responding at 5 seconds before this time runs out as reported by backgroundTimeRemaining.

So on iOS7 you can get 65 seconds of processing time every 10 minutes.