Concurrent beginBackgroundTaskWithName on push notifications - ios

I want to start and background task on receipt of a push notification. So far I have the following:
-(void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
NSString* taskName = [NSString stringWithFormat:#"PushNotificationTask-%#", [[NSUUID UUID] UUIDString]];
NSLog(#"Starting bg-task for push notification %#", taskName);
self.pnBgTask = [application beginBackgroundTaskWithName:taskName expirationHandler:^{
NSLog(#"Expiring bg-task for push notification %#", taskName);
[application endBackgroundTask:self.pnBgTask];
self.pnBgTask = UIBackgroundTaskInvalid;
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// do work here
NSLog(#"Ending bg-task for push notification %#", taskName);
[application endBackgroundTask:self.pnBgTask];
self.pnBgTask = UIBackgroundTaskInvalid;
});
In the situation where a push notification arrives before the background task of the previous one has finished, this code fails because one task will overwrite the other.
Can someone suggest a good pattern to resolve this problem?

Related

Send silent push notification to app, update location and send to server in background

I want to send a silent push notification to an application that is in background, then fetch the current user location and send it to a web service.
I implemented push notification methods and also those two:
- (void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSDate *fetchStart = [NSDate date];
[self sendLocationToServerWithCompletionHandler:^(UIBackgroundFetchResult result) {
completionHandler(result);
NSDate *fetchEnd = [NSDate date];
NSTimeInterval timeElapsed = [fetchEnd timeIntervalSinceDate:fetchStart];
NSLog(#"Background Fetch Duration: %f seconds", timeElapsed);
}];
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
}
I've also created a method that will send the location to the server:
- (void)sendLocationToServerWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSDictionary *params = #{
#"UserId" : self.userId,
#"Latitude" : self.latitude,
#"Longitude" : self.longitude
}
ServerManager *manager = [ServerManager sharedManager];
[manager sendLocationToServerWithCompletion:^(BOOL success) {
if (success)
{
completionHandler(UIBackgroundFetchResultNewData);
}
else
{
completionHandler(UIBackgroundFetchResultFailed);
}
}];
}
I just can't understand how they all work together, will Apple approve that, is it even possible and where does the location background fetch goes into.
Thanks in advance.
Here's a brief sketch of what you can do to give you an idea. Its assuming there is a model class implemented as a singleton and there's some pseudo code.
// App delegate
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
completionHandler(UIBackgroundFetchResultNewData);
[[YourModel singleton] pushNotificationReceived: userInfo];
}
// Model
- (void) pushNotificationReceived:(NSDictionary *) userInfo
{
[self registerBackgroundTaskHandler];
get the location here, or start getting the location
[self sendLocationToServerWithCompletionHandler: your completion handler];
}
- (void) registerBackgroundTaskHandler
{
__block UIApplication *app = [UIApplication sharedApplication];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
DDLogInfo(#"BACKGROUND Background task expiration handler called");
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = 0;
}];
}
- (void) endBackgroundTask
{
if (self.backgroundTaskId)
{
UIApplication *app = [UIApplication sharedApplication];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = 0;
}
}
You'll need to get the location before you can send it. If you are just getting one location and you're using iOS9 you can use CLLocationManager:requestLocation: and you could fit this in relatively easily into where I've said "get the location here".
If you're not using iOS 9 (requestLocation is new with iOS 9) its a bit more complex.
How to use the location manager is a topic in itself and too much code to post here. You need to read and study all about using the location manger before you can incorporate it.
If you need a stream of location updates it gets more complex and where it says "or start getting the location" is a lot more involved then is implied in the pseudo code.
My recommendation, start with iOS9 and getting one instance of the location, then when thats working, add more functionality or iOS8 support if you need it.

AFNetworking background downloading automatically stop after some time, I need to resume it

I am using AFNetwork (its base on NAFDownloadRequestOperation) and my task in downloading multiple zip files one by one from amazon bucket.
When app is in foreground every thing is working very well, but when app goes in background mode that time downloading is running for some time only and it will automatically stop. I read some blog about it in that I get that following method called before downloading will stop.
[self.operationUpdate setShouldExecuteAsBackgroundTaskWithExpirationHandler:^{
NSLog(#"downloading will stop");
}];
Problem in background mode downloading is automatically stop
What I want: If downloading is stop in background and when app again comes to foreground I need to resume downloading from that point.
I also use following code in AppDelegate but I am not understand how to resume previous downloading.
__block UIBackgroundTaskIdentifier backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^(void) {
[application endBackgroundTask:backgroundTaskIdentifier];
NSLog(#"applicationWillResignActive");
[__SERVER_INSTANCE cancellAllDownloading];
// [[YourRestClient sharedClient] cancelAllHTTPOperations];
}];
If any one have any solution please let me know, Thanks in advance.
You should use AFDownloadRequestOperation
Your request will look like
AFDownloadRequestOperation *operation = [[AFDownloadRequestOperation alloc] initWithRequest:request targetPath:path shouldResume:YES];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Successfully downloaded file to %#", path);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operations addObject:operation];
After you restart your app, and generate the request having a same url, it will resume downloading. "shouldResume:YES" works.
So, on your background task you can recreate request to finish download
__block UIBackgroundTaskIdentifier backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^(void) {
[application endBackgroundTask:backgroundTaskIdentifier];
NSLog(#"applicationWillResignActive");
[__SERVER_INSTANCE cancellAllDownloading];
// recreate here your request to finish fownload,
//or recreate in when app will enter foreground
}];
Hope this helps
The background downloading stops because after sometime because you might not have enable Background Fetch in Capabilities in xcode.
Please see attached screenshot
And Appdelegate You have write this code,
-(BOOL)application:(UIApplication *)application willFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[[UIApplication sharedApplication] setMinimumBackgroundFetchInterval:UIApplicationBackgroundFetchIntervalMinimum];
return true;
}
-(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
completionHandler(UIBackgroundFetchResultNewData);
}
OR
- (void)applicationDidEnterBackground:(UIApplication *)application
{
UIApplication *app = [UIApplication sharedApplication];
UIBackgroundTaskIdentifier bgTask;
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask];
NSTimeInterval ti = [[UIApplication sharedApplication]backgroundTimeRemaining];
NSLog(#"backgroundTimeRemaining: %f", ti);
// just for debug
}];
}
Hope this will help!

handleWatchKitExtensionRequest is not called

I am trying to launch the parent ios app from watchkit app. I'm using url scheme to launch the app.But it seems like
-(void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
is nevered called. It seems like watch app does launch the app in backgound. But the parent app does not handle the watchkit request. I tried my approach in a new project and it works perfectly. Is there any thing I need to pay attention?
I've already tried to Debug>Attach to process>myapp and put a breakpoint inside handleWatchKitExtensionRequest method to confirm if it is called and it isn't called.
Here is the progress, I call openParentApplication when a button is clicked in watch app.
#IBAction func viewOniPhoneAction() {
let userInfo: [NSObject : AnyObject] = [
"userID" : user.userID
]
WKInterfaceController.openParentApplication(userInfo, reply: { (userInfo : [NSObject : AnyObject]!, error : NSError!) -> Void in
})
}
Here is my app delegeate
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply
{
NSDictionary *replyDict = #{#"response": #"done"};
reply(replyDict);
}
I tried reply() in handleWatchKitExtensionRequest but I got this error in reply block from watch app
Error Error Domain=com.apple.watchkit.errors Code=2 "The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate application:handleWatchKitExtensionRequest:reply:]" UserInfo=0x60800026e0c0 {NSLocalizedDescription=The UIApplicationDelegate in the iPhone App never called reply() in -[UIApplicationDelegate application:handleWatchKitExtensionRequest:reply:]}
I got it to work!!! Having the same issue....
Just increase the beginBackgroundTaskWithExpirationHandler time to a larger value if you still don't get the data!!! I used 2 secs previously but my network is too weak!!!
I call openParentApplication when a button is clicked in watch app:
[WKInterfaceController openParentApplication:loadDetailChatDataDictionary reply:^(NSDictionary *replyInfo, NSError *error) {
Here is my app delegate:
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply {
__block UIBackgroundTaskIdentifier bogusWorkaroundTask;
bogusWorkaroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:bogusWorkaroundTask];
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // increase the time to a larger value if you still don't get the data!!! I used 2 secs previously but my network is too weak!!!
[[UIApplication sharedApplication] endBackgroundTask:bogusWorkaroundTask];
});
// --------------------
__block UIBackgroundTaskIdentifier realBackgroundTask;
realBackgroundTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
reply(nil);
[[UIApplication sharedApplication] endBackgroundTask:realBackgroundTask];
}];
NSString *value = userInfo[#"key"];
if ([value isEqualToString:#"loadRecentChatData"]) {
reply(#{#"recents":recents}); // Add your reply here
}
handleWatchKitRequest isn't called when you open the app via a URL scheme. It is only called in response to requests made in the WatchKit extension made using openParentApplication:reply:. That's why you aren't seeing it being executed.
You will need to wrap your reply in a background task to ensure your parent app has time to respond.
-
(void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *replyInfo))reply{
UIApplication *app = [UIApplication sharedApplication];
UIBackgroundTaskIdentifier bgTask __block = [app beginBackgroundTaskWithName:#"watchAppRequest" expirationHandler:^{
[[UIApplication sharedApplication] endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
//make your calls here to your tasks, when finished, send the reply then terminate the background task
//send reply back to watch
reply(replyInfo);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[app endBackgroundTask:bgTask];
bgTask=UIBackgroundTaskInvalid;
});
}

iOS 8 Silent Push Notification doesn't fire didReceiveRemoteNotification method when application not connected to xcode

I have seen too many questions about the silent push notification does not work if the device is not connected to xcode, but I could not find the real answer.
I'm using Silent APN to start a process in background and then fire a local Push notification
Server sends this info:
"_metadata" = {
bagde = 1;
pushText = "semeone has sent you a message!!";
sender = "semeone";
};
aps = {
"content-available" = 1;
};
And _metadata is customized info to fire the local notification, I did not included badge, pushText.. in aps because I it is a silent push notification.
Client should get the info in didReceiveRemoteNotification,
-(void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
if(application.applicationState != UIApplicationStateActive ){
if([userInfo[#"aps"][#"content-available"] intValue]== 1) //it's the silent notification
{
//start a background task
UIBackgroundTaskIdentifier preLoadPNTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
NSLog(#"Background task to start a process ");
}];
//end completionHandler regarding to fetchCompletionHandler
completionHandler(UIBackgroundFetchResultNewData);
// doing my process...... and fire a local notification
if(preLoadPNTask){
NSLog(#"End Background task ");
[[UIApplication sharedApplication] endBackgroundTask:preLoadPNTask];
preLoadPNTask = 0;
}
return;
}
else
{
NSLog(#"didReceiveRemoteNotification it's NOT the silent notification ");
completionHandler(UIBackgroundFetchResultNoData);
return;
}
}
else {
if(preLoadPNTask){
NSLog(#"End Background task ");
[[UIApplication sharedApplication] endBackgroundTask:preLoadPNTask];
preLoadPNTask = 0;
}
completionHandler(UIBackgroundFetchResultNewData);
}
}
It works perfectly fine when the device is connecting to xcode, but when it doesn't, the didReceiveRemoteNotification doesn't start :(
Any ideas?
Thank you in advance!!
What I end up is a cable USB was cause me some issues apparently every time that I plugged in the iphone device said that "this accessory may not be supported" but it continue working normally , so I replace for a new one, but that not solve my issue, however can be part of this. so I looked in the code, and I did some changes, after receive 2 o more silent push notification preLoadPNTask (UIBackgroundTaskIdentifier) was creating many times so I added a validation before it start,
if(!preLoadPNTask){
//start a background task
UIBackgroundTaskIdentifier preLoadPNTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
NSLog(#"Background task to start a process ");
}];
}
I hope this help you
Regards
In ios 8, You need to the following steps to fire didReceiveRemoteNotification: method
Select project target goto Capabilities tab
Select 'Background modes' turn on.
It'll add a key (Required background modes)in your project info.plist
After these modifications, when you get a apple push notification and if the app is in background already then didReceiveRemoteNotification will be fired.
Probably because under iOS 8 you have to ask for push notifications in a different way. Try this:
-(void) registerForPushNotifications {
UIApplication* application=[UIApplication sharedApplication] ;
// Register for Push Notitications, if running iOS 8
if ([application respondsToSelector:#selector(registerUserNotificationSettings:)]) {
UIUserNotificationType userNotificationTypes = (UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound);
UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes categories:nil];
[application registerUserNotificationSettings:settings];
[application registerForRemoteNotifications];
} else {
// Register for Push Notifications before iOS 8
[application registerForRemoteNotificationTypes:(UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeSound)];
}
}

Start background task after receiving push in Suspended mode

It may seem that this question was asked several times, but I'm facing a weird problem.
I have server configured to send push notification with content-available = 1 flag.
I have configured my app to work in background Background Modes on for Location Update, Background fetch and Remote Notifications.
Also I have implemented all necessary code to receive push notifications in background and to start background task.
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
__block UIBackgroundTaskIdentifier bg_task = background_task;
background_task = [application beginBackgroundTaskWithExpirationHandler:^ {
//Clean up code. Tell the system that we are done.
[application endBackgroundTask: bg_task];
bg_task = UIBackgroundTaskInvalid;
}];
//### background task starts
[self updateLocationToServer];
//#### background task ends
completionHandler(UIBackgroundFetchResultNewData);
}
- (void)updateLocationToServer{
[locationManager updateLocationWithCompletionHandler:^(CLLocation *location, NSError *error, BOOL locationServicesDisabled) {
if (error)
{
// Handle error here
if (locationServicesDisabled) {
// Location services are disabled, you can ask the user to enable them for example
}
}
else
{
// Do whatever you want with the current user's location
NSString *deviceID = [userDefs objectForKey:#"deviceID"];
isConnected = [[userDefs objectForKey:#"connected"] boolValue];
if (isConnected) {
if (deviceID) {
[self sendLocation:deviceID];
}
}
localNotif = [[UILocalNotification alloc] init];
localNotif.fireDate = [NSDate dateWithTimeIntervalSinceNow:0.1];
localNotif.timeZone = [NSTimeZone defaultTimeZone];
localNotif.alertBody = [NSString stringWithFormat:#"Lat: %# Long:%#",[NSNumber numberWithFloat:location.coordinate.latitude],[NSNumber numberWithFloat:location.coordinate.longitude]];
[[UIApplication sharedApplication] scheduleLocalNotification:localNotif];
NSLog(#"Lat: %# Long:%#",[NSNumber numberWithFloat:location.coordinate.latitude],[NSNumber numberWithFloat:location.coordinate.longitude]);
//Clean up code. Tell the system that we are done.
[[UIApplication sharedApplication] endBackgroundTask: background_task];
background_task = UIBackgroundTaskInvalid;}}];}
EDIT: Added code where I end background task. background_task variable is global.
The app receives push in background normally until it goes to suspended mode. The problem is that, after background task ends, and the app goes to suspended mode it does not run the code again when it receives push notification but didReceiveRemoteNotification: fetchCompletionHandler: does not get called. But when I open the app and exit with home button it will work again within "that" 3 minutes until it goes to suspended mode.

Resources