How to resume multiple background downloads using AFURLSessionManager in iOS app - objective-c

I have an iOS app that provides multiple book download at the same time, Its a reader app actually. Earlier I was using AFDownloadRequestOperation for download the book. When user launch the app after app crashes or after user force quit (kill) the app, It automatically resume the download from the offset where it left downloading and for this to achieve I was using the following method:
AFDownloadRequestOperation* operation = [[AFDownloadRequestOperation
alloc] initWithRequest:request targetPath:path shouldResume:YES];
Now I am using AFNetworking 2.0 API AFHTTPSessionManager for background download. I have created the background session in the following way:
NSString* strIdentifier = [NSString stringWithFormat:#"someUniqueIdentifier"];
NSURLSessionConfiguration *sessionConfig;
if ([[[UIDevice currentDevice] systemVersion] floatValue] >=8.0f)
{
sessionConfig =[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:strIdentifier];
}
else
{
sessionConfig = [NSURLSessionConfiguration backgroundSessionConfiguration:strIdentifier];
}
self.backgroundSessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:sessionConfig];
Then I have started the background download task as follows with delegate callbacks:
self.bookDownloadTask = [self.backgroundSessionManager downloadTaskWithRequest:request progress:nil destination:nil completionHandler:nil];
[self.bookDownloadTask resume];
So again considering the earlier situation -
When user launch the app after app crashes or after user force quit (kill) the app, It does automatically start (not resume) the download but not from the offset where it left downloading, it start downloading form starting of the book.
Is there any way to automatically resume the download from the offset where it left downloading using AFHTTPSessionManager ??
Any help would be appreciated. Thanks !!

Related

Native cellular call fails on VoIP incoming call in iOS 13

I have implemented CallKit for audio and video call with VoIP PushKit in iOS and it is working fine in iOS 12 and prior versions, and also it is working fine normally in iOS 13 and 13.1.
But it is failing in 2 scenarios:
1) Our App is in foreground state. When cellular call is running and VoIP push is received, then Call kit incoming call screen is showing for 5 - 10 seconds, and then both Cellular and VOIP calls are failing with Alert "Call Failed".
2) Our App is in Background or Killed state. When cellular call is running and VoIP push is received, then both Cellular and VOIP calls are failing with Alert "Call Failed". No incoming call UI is showing this time.
I am showing my code here:
- (void)registerAppForVOIPPush {
PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
pushRegistry.delegate = self;
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
}
Then Push delegates
#pragma mark PKPushRegistryDelegate ----
- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials: (PKPushCredentials *)credentials forType:(NSString *)type {
NSString *newToken = [self hexadecimalStringFromData:credentials.token];
//Make a note of this token to a server to send VOIP for a particular device
NSLog(#"VOIP token ::: %#", newToken);
_voipToken = newToken;
}
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type {
//available(iOS, introduced: 8.0, deprecated: 11.0)
[self pushRegistryDidReceivedPushWithPayload:payload forType:type withCompletionHandler:NULL];
}
- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
//available(iOS 11.0, *)
[self pushRegistryDidReceivedPushWithPayload:payload forType:type withCompletionHandler:completion];
}
- (void)pushRegistryDidReceivedPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion {
//Call kit configration
CXProviderConfiguration *providerConfig = [[CXProviderConfiguration alloc] initWithLocalizedName:#"my app Call"];
providerConfig.supportsVideo = NO;
providerConfig.maximumCallGroups = 1;
providerConfig.maximumCallsPerCallGroup = 1;
providerConfig.supportedHandleTypes = [[NSSet alloc] initWithObjects:[NSNumber numberWithInteger:CXHandleTypeGeneric], nil];
providerConfig.iconTemplateImageData = UIImagePNGRepresentation([UIImage imageNamed:#"IconMask"]);
CXProvider *provider = [[CXProvider alloc] initWithConfiguration:providerConfig];
[provider setDelegate:self queue:nil];
//generate token
NSUUID *callbackUUIDToken = [NSUUID UUID];
//Display callkit
NSString *uniqueIdentifier = #"Max test";
CXCallUpdate *update = [[CXCallUpdate alloc] init];
update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:uniqueIdentifier];
update.supportsGrouping = FALSE;
update.supportsUngrouping = FALSE;
update.supportsHolding = FALSE;
update.localizedCallerName = uniqueIdentifier;
update.hasVideo = NO;
[provider reportNewIncomingCallWithUUID:callbackUUIDToken update:update completion:^(NSError * _Nullable error) {
NSLog(#"reportNewIncomingCallWithUUID error: %#",error);
}];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
}
I have implemented CXProvider delegate method perfectly
- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action{
[action fulfill];
}
- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action{
[action fulfill];
}
and also managed other delegate methods to manage call and everything, and it is working perfectly in all conditions.
I have checked these two scenarios with other apps like Google Duo, Whatsapp and FaceTime and it's showing CallKit properly without failing, but in my app it is failing. I have no clue where it is failing.
So, I have this 2 stated issues for iOS 13 and later versions. Any help will be appreciated.
Thanks.
This is probably an iOS 13 bug and, if you haven't already done it, you should report it to Apple.
I think that the reason why apps like Whatapp (and the one I develop) are working, is that we build the app against the iOS 12 SDK. We do this because of the limitations of VoIP push notifications introduced in iOS 13. So, you can try to work around the issue—at least until April 2020—building against the iOS 12 SDK. Hopefully, Apple we'll soon fix this issue.
#Max I have faced the same issue that you faced in iOS version 13.0 to 13.2.0.
As many developers have reported this issue to Apple. The latest iOS version that released last week(iOS 13.2.2) has this bug resolved. So, now instead of building from older SDK, you can start working with latest SDK and xCode 11.2.1.

How to trigger AVAudioPlayer playback using a remote push notification?

I'm creating an app that has a remotely-triggered alarm. Essentially, I'm trying to trigger a looping MP3 file to play (while the app is backgrounded) when a remote push notification arrives with a particular payload.
I've tried using didReceiveRemoteNotification: fetchCompletionHandler:, so that code can be run as a result of receiving a remote notification with a particular userInfo payload.
Here is my attempted didReceiveRemoteNotification: fetchCompletionHandler: from my AppDelegate.m:
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
NSString *command = [userInfo valueForKeyPath:#"custom.a.command"];
if (command) {
UIApplicationState applicationState = [[UIApplication sharedApplication] applicationState];
if ([command isEqualToString:#"alarm"] && applicationState != UIApplicationStateActive) {
// Play alarm sound on loop until app is opened by user
NSLog(#"playing alarm.mp3");
NSString *soundFilePath = [[NSBundle mainBundle] pathForResource:#"alarm" ofType:#"mp3"];
NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath];
NSError *error;
self.player = nil;
self.player = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:&error];
self.player.numberOfLoops = -1; // Infinitely loop while self.player is playing
self.player.delegate = self;
[self.player play];
}
}
completionHandler(UIBackgroundFetchResultNewData);
}
I expected the looping audio file to start playing as soon as the push notification arrived (with the app inactive or backgrounded), but it didn't. Instead, the audio playback surprisingly began when I then bring the app to the foreground.
What is missing from this approach, and/or can a different way work better?
You cannot start an audio session with the app in background. Audio sessions have to be initialized/started while the app is in foreground. An audio session properly initialized and running can continue if the app is pushed to background provided another app in foreground does not interrupt it.
Based on this information, I would say that your application likely has to start an audio session while you are in control and in foreground, keep the audio session alive while in background. Upon receiving the push notification, use the existing opened audio session to deliver audio out.
This has serious limitations since any other app, like Netflix, that uses a dedicated audio session may interrupt your app's audio session and prevent it from being able to play the MP3 when it arrives.
You may want to consider pre-packaging and/or downloading the MP3 ahead of time, and refer to them direcly in the Sound parameters of your push notification.
You may follow this tutorial to see how you can play custom sounds using push notifications: https://medium.com/#dmennis/the-3-ps-to-custom-alert-sounds-in-ios-push-notifications-9ea2a2956c11
func pushNotificationHandler(userInfo: Dictionary<AnyHashable,Any>) {
// Parse the aps payload
let apsPayload = userInfo["aps"] as! [String: AnyObject]
// Play custom push notification sound (if exists) by parsing out the "sound" key and playing the audio file specified
// For example, if the incoming payload is: { "sound":"tarzanwut.aiff" } the app will look for the tarzanwut.aiff file in the app bundle and play it
if let mySoundFile : String = apsPayload["sound"] as? String {
playSound(fileName: mySoundFile)
}
}
// Play the specified audio file with extension
func playSound(fileName: String) {
var sound: SystemSoundID = 0
if let soundURL = Bundle.main.url(forAuxiliaryExecutable: fileName) {
AudioServicesCreateSystemSoundID(soundURL as CFURL, &sound)
AudioServicesPlaySystemSound(sound)
}
}

Spawning Quicktime in a Sandboxed Mac App

I had asked a previous question about an issue I was having with AVPlayerView. After toying with it a bit, I actually like my mac app looks and runs a lot better when, instead of opening a new window with an AVPlayerView, it launches QuickTime, and tells it to open my http url of a video. I have come up with a few ways to do this, all of which work without sandboxing and none of which work with. I am currently using an NSTask to essentially
open -a "Quicktime Player" "http://example.com/video.m4v"
Again, this works, but only when my app is not sandboxed. Is there any way to do this in a sandboxed app?
Thanks in advance for any input or suggestions.
Sandbox doesn't allow to work with NSTask by default. Use the appropriate entitlement.
To open quicktime and start playing a movie you're probably better off using NSWorkspace like this:
[[NSWorkspace sharedWorkspace] openURLs:#[url]
withAppBundleIdentifier:#"com.apple.QuickTimePlayerX"
options:NSWorkspaceLaunchAsync
additionalEventParamDescriptor:NULL
launchIdentifiers:nil];
Alternatively go straight down to Launch Services where you even have more control of what and how things are launched:
NSURL *appToOpenWith = // get the URL of Quicktime using NSWorkspace URLForApplicationWithBundleIdentifier;
LSLaunchURLSpec inLaunchSpec;
inLaunchSpec.appURL = (__bridge CFURLRef) appToOpenWith;
inLaunchSpec.itemURLs = (__bridge CFArrayRef) ([NSArray arrayWithObject:theNSURLPointingToYourM4V ]);
inLaunchSpec.passThruParams = NULL;
inLaunchSpec.launchFlags = kLSLaunchDefaults; // could be done async... are we here in the main thread?
inLaunchSpec.asyncRefCon = NULL;
CFURLRef outLaunchedURL;
OSStatus diditOpen = LSOpenFromURLSpec (&inLaunchSpec, &outLaunchedURL);
if (noErr != diditOpen) {
NSLog(#"couldn't open selected items with error: %i", diditOpen);
} else {
NSLog(#"opened with: %#", [(__bridge NSURL*)outLaunchedURL description]);
}

Background uploading concerns (objective c)

We are working on one iOS application that needs to upload photos, videos to amazon s3. We need to do below tasks for this application:
Upload video, image in the background
If network connection is lost, alert the user
If user has low signal, give a message to the user for trying this upload later (we are storing all upload files in local till it we complete upload)
We are using NSURLRequest for uploading all files in the background. But we are not sure if this is the correct approach because it has lot of issues. Can someone recommend best approach and methods to use in iOS for above tasks.
thats fine if you want to support ios6 as well -- for ios7+ switch to NSURLSession
beware though that iOS does not guarantee background time and may cancel your process at any time
to check for that, register a backgroundHandler which offers an expiry block that is called before the os does kill your process
tell os you want to run a bg task:
UIBackgroundTaskIdentifier token = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
NSLog(#"Ranging for region %# killed", region.identifier);
}];
if(token == UIBackgroundTaskInvalid) {
NSLog(#"cant start background task");
}
do whatever you want to do ..
when done, tell os:
[[UIApplication sharedApplication] endBackgroundTask:token];
Note: this rarely works indefinitely and it meant to be used that way. The OS can still decide to kill you! (chances for your app to run a longer time are better if you don't use much memory and as little cpu / bandwidth as possible)
regarding your use case:
if your NSURLConnection fails your completionBlock or the delegate gets an error.
you can show a UILocalNotification then
dummy code using blocks:
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *connectionError) {
if(connectionError) {
UILocalNotification *theNotification = [[UILocalNotification alloc] init];
theNotification.alertBody = connectionError.description;
theNotification.fireDate = [NSDate dateWithTimeIntervalSinceNow:1];
[[UIApplication sharedApplication] scheduleLocalNotification:theNotification];
}
}];

Can't see Google Analytics data from my iOS app

I did follow the tutorial in Google developer website and put this code on my App delegate:
[GAI sharedInstance].trackUncaughtExceptions = YES;
[GAI sharedInstance].dispatchInterval = 20;
[GAI sharedInstance].debug = NO;
id<GAITracker> tracker = [[GAI sharedInstance] trackerWithTrackingId:#"My tracker code"];
[[GAI sharedInstance] trackerWithTrackingId:KGATrackerId];
Then in my viewcontrollers I placed this code in the viewDidLoad method:
id<GAITracker> tracker = [[GAI sharedInstance] defaultTracker];
[tracker trackView:#"Test View"];
When the GA debugger is on it actually states it's "hitting" the server with info; however in my Googla Analytics dashboard I can't see any new data in the newly created app section.
In my AppDelegate I have the following code in my didFinishLaunchingWithOptions
[[GANTracker sharedTracker] startTrackerWithAccountID:#"YOUR-ID-HERE"
dispatchPeriod:10
delegate:nil];
And to track events, like button clicks, I use this snippet in various places:
NSError *error;
if (![[GANTracker sharedTracker] trackPageview:#"/login/TouchedSubmit"
withError:&error]) {
// Handle error here
}
I have used this in multiple apps and I have never had any issues with my events not getting sent to Google-- Hope this helps