Weird NSURLSessionDownloadTask behavior over cellular (not wifi)
Asked Answered
W

4

13

I've enabled Background Modes with remote-notification tasks to download a small file (100kb) in background when the app receives a push notification. I've configured the download Session using

NSURLSessionConfiguration *backgroundConfiguration = [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
[backgroundConfiguration setAllowsCellularAccess:YES];


self.backgroundSession = [NSURLSession sessionWithConfiguration:backgroundConfiguration
                                                       delegate:self
                                                  delegateQueue:[NSOperationQueue mainQueue]];

and activate it using

 NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[hostComponents URL]];

[request setAllowsCellularAccess:YES];


NSMutableData *bodyMutableData = [NSMutableData data];
[bodyMutableData appendData:[params dataUsingEncoding:NSUTF8StringEncoding]];
[request setHTTPMethod:@"POST"];
[request setHTTPBody:[bodyMutableData copy]];


_downloadTask =  [self.backgroundSession downloadTaskWithRequest:request];

[self.downloadTask resume];

Now everything works correctly only if I'm connected over Wifi or over Cellular but with the iPhone connected with the cable to xCode, if I disconnect the iPhone and receive a push notification over cellular the code stop at [self.downloadTask resume]; line without call the URL.

The class that handles these operation is a NSURLSessionDataDelegate, NSURLSessionDownloadDelegate, NSURLSessionTaskDelegate and so implements:

    - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location

I've try to insert a debug line with a UILocalNotification (presented 'now') after the [self.downloadTask resume] but is called after 5 minutes and says that the self.downloadTask.state is 'suspended'

What is due this weird behavior ?

Weatherly answered 9/7, 2014 at 23:16 Comment(3)
I am experiencing this exact same behavior on ATT network with IOS 7.0. Since doesn't happen when connected to Xcode I save logs on the phone to see when I restart the app. Found that the NSURLSessionDownloadTasks remain with State=NSURLSessionTaskStateRunning but act like they are suspended. If connect to wifi while the app is still in the background these stalled downloads complete successfully. If i bring the app into the foreground they remain stalled.Ignominy
From Sani Elfshishawy answer below "when plugged into power and on Wi-Fi" It seems to me that the plugging into XCODE changes the plugged into power condition.Juni
I hit the same case too. I am combining the iBeacon with the background NSURLSession background task to perform some checking when the user near the beacons. And I found that the download task is not available when the user using cellular network. Do you guys have some suggestions?Shayneshays
I
9

The documentation for NSURLSessionConfiguration Class Reference here:

https://developer.apple.com/Library/ios/documentation/Foundation/Reference/NSURLSessionConfiguration_class/Reference/Reference.html#//apple_ref/occ/instp/NSURLSessionConfiguration/discretionary

Says: for the discretionary property:

Discussion

When this flag is set, transfers are more likely to occur when plugged into power and on Wi-Fi. This value is false by default.

This property is used only if a session’s configuration object was originally constructed by calling the backgroundSessionConfiguration: method, and only for tasks started while the app is in the foreground. If a task is started while the app is in the background, that task is treated as though discretionary were true, regardless of the actual value of this property. For sessions created based on other configurations, this property is ignored.

This seems to imply that if a download is started in the background the OS always has discretion as to whether and when to proceed with the download. It seems that the OS is always waiting for a wifi connection before completing these tasks.

My experience supports this conjecture. I find that I can send several notifications for downloads while device is on cellular. They remain stuck. When I switch the device to wifi they all go through.

Ignominy answered 3/9, 2014 at 20:27 Comment(2)
WTF?! Why Apple is doing THIS?! Of course the user is often not in WiFi when download tasks are started. In my app I rely on downloading VERY small amounts of data when in background AND in cell network (for example due to a app-wake-up by location change). Is that somehow possible?Funds
This should be marked as the answer, its spot on. I have seen several mysterious issues where downloads magically start only when connected via cable and always its been due to this setting.Giacinta
H
3

I got the same problem ,finally i set

configuration.discretionary = NO;

the everything works fine, for backgroundConfiguration , discretionary = YES default, it seems the task begin in connection with WIFI and battery both. hope helpful

Hagen answered 13/3, 2016 at 6:28 Comment(0)
I
0

What are you doing in

  • (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler{}

Are you calling the completionHandler right away before your download completes? I believe that doing this does not affect operation in Wifi mode or when connected to Xcode. But somehow when in background on Cellular it makes the download stall until you go to Wifi.

Ignominy answered 3/9, 2014 at 0:30 Comment(0)
R
0

The only real way around this is to dump NSURLSession when the app is in the background and use CF sockets. I can successfully do HTTP requests over cellular while the app is in background if I use CFStreamCreatePairWithSocketToHost to open CFStream

#import "Communicator.h"

@implementation Communicator {
    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;

    NSInputStream *inputStream;
    NSOutputStream *outputStream;
    CompletionBlock _complete;
}

- (void)setupWithCallBack:(CompletionBlock) completionBlock {
    _complete = completionBlock;
    NSURL *url = [NSURL URLWithString:_host];

    //NSLog(@"Setting up connection to %@ : %i", [url absoluteString], _port);

    CFStreamCreatePairWithSocketToHost(kCFAllocatorDefault, (__bridge CFStringRef)[url host], _port, &readStream, &writeStream);

    if(!CFWriteStreamOpen(writeStream)) {
        NSLog(@"Error, writeStream not open");

        return;
    }
    [self open]; 

    //NSLog(@"Status of outputStream: %lu", (unsigned long)[outputStream streamStatus]);

    return;
}

- (void)open {
    //NSLog(@"Opening streams.");

    inputStream = (__bridge NSInputStream *)readStream;
    outputStream = (__bridge NSOutputStream *)writeStream;

    [inputStream setDelegate:self];
    [outputStream setDelegate:self];

    [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    [inputStream open];
    [outputStream open];
}

- (void)close {
    //NSLog(@"Closing streams.");

    [inputStream close];
    [outputStream close];

    [inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

    [inputStream setDelegate:nil];
    [outputStream setDelegate:nil];

    inputStream = nil;
    outputStream = nil;
}

- (void)stream:(NSStream *)stream handleEvent:(NSStreamEvent)event {
    //NSLog(@"Stream triggered.");

    switch(event) {
        case NSStreamEventHasSpaceAvailable: {
            if(stream == outputStream) {
                if (_complete) {
                    CompletionBlock copyComplete = [_complete copy];
                    _complete = nil;
                    copyComplete();
                }
            }
            break;
        }
        case NSStreamEventHasBytesAvailable: {
            if(stream == inputStream) {
                //NSLog(@"inputStream is ready.");

                uint8_t buf[1024];
                NSInteger len = 0;

                len = [inputStream read:buf maxLength:1024];

                if(len > 0) {
                    NSMutableData* data=[[NSMutableData alloc] initWithLength:0];

                    [data appendBytes: (const void *)buf length:len];

                    NSString *s = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];

                    [self readIn:s];

                }
            } 
            break;
        }
        default: {
            //NSLog(@"Stream is sending an Event: %lu", (unsigned long)event);

            break;
        }
    }
}

- (void)readIn:(NSString *)s {
    //NSLog(@"reading : %@",s);
}

- (void)writeOut:(NSString *)s{
    uint8_t *buf = (uint8_t *)[s UTF8String];

    [outputStream write:buf maxLength:strlen((char *)buf)];

    NSLog(@"Writing out the following:");
    NSLog(@"%@", s);
}

@end
Rilke answered 15/7, 2016 at 9:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.