Play a background sound in WatchKit app - ios

I have a button in WatchKit that sends a notification to the main iPhone app like this.
-(IBAction) startSound
{
//turn sound on
NSString *requestString = [NSString stringWithFormat:#"startSound"]; // This string is arbitrary, just must match here and at the iPhone side of the implementation.
NSDictionary *applicationData = [[NSDictionary alloc] initWithObjects:#[requestString] forKeys:#[#"startSound"]];
[WKInterfaceController openParentApplication:applicationData reply:^(NSDictionary *replyInfo, NSError *error) {
//NSLog(#"\nReply info: %#\nError: %#",replyInfo, error);
}];
}
In my iPhone app delegate I have added the following code.
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo))reply
{
NSLog(#"handleWatchKitExtensionRequest ...");
NSMutableDictionary *mutDic = [[NSMutableDictionary alloc] init];
//This block just asks the code put after it to be run in background for 10 mins max
__block UIBackgroundTaskIdentifier bgTask;
bgTask = [application beginBackgroundTaskWithName:#"MyTask" expirationHandler:^{
bgTask = UIBackgroundTaskInvalid;
}];
NSString *request = [userInfo objectForKey:#"startSound"];
if ([request isEqualToString:#"startSound"])
{
NSString *soundFilePath = [[NSBundle mainBundle] pathForResource: #"warning" ofType: #"mp3"];
NSURL *fileURL = [[NSURL alloc] initFileURLWithPath: soundFilePath];
myAudioPlayer1 = [[AVAudioPlayer alloc] initWithContentsOfURL:fileURL error:nil];
myAudioPlayer1.numberOfLoops = -1; //inifinite
[myAudioPlayer1 play];
}
reply(nil); //must reply with something no matter what
//once code is all done and the reply has been sent only then end the bg-handler
[application endBackgroundTask:bgTask];
}
Yet, when my app went for apple review, it got rejected for reasons that my app had to be running in the foreground for the sound feature to work. What did I miss?
10.6 - Apple and our customers place a high value on simple, refined, creative, well thought through interfaces. They take more work but are
worth it. Apple sets a high bar. If your user interface is complex or
less than very good, it may be rejected
10.6 Details
We still found that your Apple Watch app requires the containing app
to be running in the foreground on iPhone in order to play siren
sounds, which provides a poor user experience.
Next Steps
Please see the UIApplicationDelegate Protocol Reference to implement
this method and use it to respond to requests from the Apple Watch
app.
Because this method is likely to be called while your app is in the
background, call the beginBackgroundTaskWithName:expirationHandler:
method at the start of your implementation and the endBackgroundTask:
method after you have processed the reply and executed the reply
block. Starting a background task ensures that your app is not
suspended before it has a chance to send its reply.

How long is the audio clip? In the code you've shared it looks like reply() would be called almost immediately, which wouldn't give the clip a chance to play. You should delay calling reply() until your audio clip has completed.

I do something very similar in my WatchKit app. When the user taps on a button I play audio from my iPhone app and my iPhone app does not need to foreground for it to work. There were two things I had to do to get it to work. The first was setting up a background task with beginBackgroundTaskWithName, which I can see you are doing. The second was to enable background audio capabilities in my background modes.
I don't remember doing anything other than those two to get it to work. I did already have background audio working in my app before I added support so it could be controlled with MPNowPlayingInfoCenter.

Related

How to get and send location(even if application is terminated or suspended) to server

i need to get and send location to server even if application is terminated, i found solution how to get location, but how i can send it to server - i cant found. I think i can send it with background task, but how it together work, i can't imagine, i try with this code indidFinishLaunchingWithOptions:
if ([launchOptions objectForKey:UIApplicationLaunchOptionsLocationKey]) {
// This "afterResume" flag is just to show that he receiving location updates
// are actually from the key "UIApplicationLaunchOptionsLocationKey"
self.shareModel.afterResume = YES;
[self.shareModel startMonitoringLocation];
[self.shareModel addResumeLocationToPList];
UIApplication *application = [UIApplication sharedApplication];
__block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://google.com"]];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse * _Nullable response, NSData * _Nullable data, NSError * _Nullable connectionError) {
NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
[[NSUserDefaults standardUserDefaults] setObject:str forKey:#"DOWNLOADTASK"];
[application endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
but than i print NSUserDefaults its empty by this key, what i doing wrong, may be i'm using the wrong background task ??
How to Get Location Updates for iOS 7 and 8 Even when the App is Suspended
https://github.com/voyage11/GettingLocationWhenSuspended
iOS 9 how get locations even if app terminated
According to the apple in the document about the description of the background, any app background task execution time of 10 minutes.After 10 minutes, the app will be iOS forced to hang up.
But there are five types of app allows for "unlimited" background running time.
Audio。
Location/GPS。
VoIP。
Newsstand。
Exernal Accessory 。
You can take any app statement for the above five types in order to obtain the background of infinite time, but when you submit the app to the app Store, apple will review your app, once found you "abuse" of the backend API, your app will be rejected.
However, in the case of enterprise development, there is no "abuse" problem - enterprise app can through OTA deployment, not through the apple store review.
In enterprise deployment, you can be a statement for VoIP app, but the program simply has nothing to do with VoIP, we only for the purpose of the iOS give us infinite background execute permissions.Declaration process is in the app Info. The file to add the following key:
In the first place in the file the Required background modes that add the following two: in a App play audio or streams audio/video using AirPlay and App provides Voice over IP services.
UIBackgroundModes
voip
I tested the following code:
- (void)backgroundHandler {
NSLog(#"### -->backgroundinghandler");
UIApplication* app = [UIApplicationsharedApplication];
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}];
}
Through the test, I have received an "infinite" background execution time.I don't know how long do you think is "infinite", but in this case, the background task to run at least 55 hours, until I lose patience to stop testing.
my english is very poor,this blogs are reprinted!
You can't perform any task in suspended or terminated state as per apple's guideline. you can get location updates in background state but can't do it when app is terminated or suspended. Refer this link and this tutorial for background location fetch. hope this will help :)

AFNetworking api call after geofence or beacon event

I'm trying to make an API call after getting "didEnterRegion" call. When the app is on front, it works great. But if the app is deactivated or even in background, the call is never made. I tried to debug it but once "reportLocationEventToServerWithType" is called the debugger "dies" and the call is never made. I don't think it matters, but i'm using Estimote sdk for the beacon monitoring.
Here are the code snippets:
- (void)beaconManager:(id)manager didEnterRegion:(CLBeaconRegion *)region{
UIApplication *app = [UIApplication sharedApplication];
NSNumber * mallID = [self.dictBeaconsPerMall objectForKey:region.identifier];
Mall * mall = [Mall getMallWithID:mallID];
[ApplicationManager sharedInstance].locationManager.beaconRecognizedMall=mall;
[[NSNotificationCenter defaultCenter]postNotificationName:nLocationMallUpdated object:nil];
UILocalNotification * not = [[UILocalNotification alloc]init];
not.alertBody = [NSString stringWithFormat:#"DID Enter beacon region at: %#",mall.strTitle];
not.alertAction = #"alertAction";
not.soundName = UILocalNotificationDefaultSoundName;
[app presentLocalNotificationNow:not];
[[ApplicationManager sharedInstance].crashAndReportsManager reportLocationEventToServerWithType:#"beacon" andSubParam:region.identifier extraInfo:#{#"paramKey":#"key"}];
}
-(void)reportLocationEventToServerWithType: (NSString*)type andSubParam:(NSString*) strID extraInfo: (NSDictionary*)extraInfo{
NSString* udid = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
NSMutableDictionary * params = [[NSMutableDictionary alloc]init];
[params setObject:type forKey:#"type"];
[params setObject:strID forKey:#"subParam"];
[params setObject:udid forKey:#"udid"];
if(extraInfo)
[params addEntriesFromDictionary:extraInfo];
UserLogRequest * req = [[UserLogRequest alloc]initWithCallerObject:self andParams:params];
req.showHud=NO;
req.showMessage=NO;
[[ApplicationManager sharedInstance].requestManager sendRequestForRequest:req];
}
The request initialization and the request manager "sendRequestForRequest" are working fine in million other situations so their implementation is irrelevant.
Thanks!
Background execution is little bit different in ios. you can't execute in background without some settings from capabilities under project target. you should enable background execution from it for specific requirement like location, audio etc. and then you should implement code accordingly.
You have limited options to execute in background like audioplaying, location updating, finite length task, voice over ip etc.
refer this apple documentation for more detail on background execution and you can google background execution you will got some good results.
So, reason of not working your task in background is this that you have not properly configured it.
hope this will help :)

Can we open the contacts on watch extension

How to open up the contacts of iPhone programmatically in watch extension as we do in iOS using AddressBook.
Thanks in advance
In general to communicate with iPhone from your WatchKit extension you use
+ (BOOL)openParentApplication:(NSDictionary *)userInfo reply:(void(^)(NSDictionary *replyInfo, NSError *error)) reply; // launches containing iOS application on the phone. userInfo must be non-nil
method of WKInterfaceController class.
So for example, you can attach IBAction from your button in Storyboard to this method
- (IBAction)callPhoneAppButtonTapped
{
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:#"text to display on iPhone", #"key", nil];
[InterfaceController openParentApplication:dictionary reply:^(NSDictionary *replyInfo, NSError *error) {
NSLog(#"Reply received by Watch app: %#", replyInfo);
}];
}
Note: In order to fetch data from Address Book user needs to grant your app permission. But app will be launched in background and user will be focused on Watch so it will be better to ask for this permission in your iPhone app.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
ABAddressBookRequestAccessWithCompletion(ABAddressBookCreateWithOptions(NULL, nil), ^(bool granted, CFErrorRef error) {
if (!granted){
NSLog(#"Access denied");
return;
}
NSLog(#"Access granted");
});
}
In order to handle message sent by openParentApplication:reply in your AppDelegate implement
- (void)application:(UIApplication *)application handleWatchKitExtensionRequest:(NSDictionary *)userInfo reply:(void (^)(NSDictionary *))reply
{
NSLog(#"Request received by iOS app");
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:#"your value to return to Apple Watch", #"key", nil];
// Here your app will be launch in background. Fetch AddressBook or other data you need.
// Remember to call reply block in the end.
// Example of saving data to Address Book
NSString *firstName;
NSString *lastName;
firstName = #"Maggie";
lastName = #"Peggie";
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, nil);
ABRecordRef contact = ABPersonCreate();
ABRecordSetValue(contact, kABPersonFirstNameProperty, (__bridge CFStringRef) firstName, nil);
ABRecordSetValue(contact, kABPersonLastNameProperty, (__bridge CFStringRef)lastName, nil);
ABAddressBookAddRecord(addressBookRef, contact, nil);
ABAddressBookSave(addressBookRef, nil);
reply(dictionary);
}
While the current versions of Apple Watch apps cannot themselves execute code, your WatchKit Extension runs on the phone and can access all of the iPhone APIs that a standard iOS application can. As developers, we are much more limited in how we can programmatically change the interface, but not in what is done in terms of accessing services.
Therefore, there is no technical requirement to access Address Book data via your iOS app—you could make these requests for Address Book data directly. If these methods execute rapidly, the choice of whether to do this directly in the Extension or in your iPhone app would come down to decisions about what would minimise code complexity and thus maximise code maintainability. Apple have indicated that latency in communication between the iPhone app and WatchKit Extension can largely be ignored as it will be trivial. (It is latency between the Extension, running on the phone, and the Watch app that we need to be focussed on.)
However, we have also been told that WatchKit Extensions may be immediately terminated when Watch apps are, and we need to be prepared for engagement time measured in seconds, not minutes. WatchKit Extensions are not given the kind of latitude that iPhone apps are to complete things in the background after the user interface has terminated. Therefore, the recommendation is that anything that may be more time consuming or which needs to be completed for data integrity should be run in the iPhone app. lvp's answer gives code that could assist with that.
Your app runs on the phone, so you can fetch the contacts and send it to watch

Starting AVQueuePlayer from background

Can't make AVQueuePlayer start playing sound queue when it's starting after the app went into the background.
The basic question is: how to start sound from newly created AVQueuePlayer instance from background?
It's for navi-like app that need to play couple of combined sounds with appropriate directions when the time comes. And most of the time the app works in background...
The details are below...
It plays just fine when I start it from active application, and finishes playing sound even after app went to background.
What I did so far:
In AppDelegate inside didFinishLaunchingWithOptions I added:
NSError *sessionError = nil;
[[AVAudioSession sharedInstance] setDelegate:self];
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:&sessionError];
// Change the default output audio route
UInt32 doChangeDefaultRoute = 1;
AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,
sizeof(doChangeDefaultRoute), &doChangeDefaultRoute);
After the app started I clicked the home button so the app went into the background.
When the time came this code executes (the app is still in background, but note, that I have enabled Audio and AirPlay background mode):
-(void)playTrainingFinishedSound
{
NSMutableArray *queue = [NSMutableArray array];
[queue addObject:[AVPlayerItem playerItemWithURL:[[NSBundle mainBundle] URLForResource:#"alertkoniectreningu" withExtension:#"m4a"]]];
[self initializeAudioPlayerWithQueue:queue];
[self.appDelegate.audioPlayer play];
}
-(void)initializeAudioPlayerWithQueue:(NSArray *)queue
{
self.appDelegate.audioPlayer = [[AVQueuePlayer alloc] initWithItems:queue];
self.appDelegate.audioPlayer.actionAtItemEnd = AVPlayerActionAtItemEndAdvance;
}
Unfortunately this code doesn't make any sound, opposite to the situation when the app was in foreground.
Oh God, there was just one line missing in the AppDelegate didFinishLaunchingWithOptions [[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
so it looks now like:
audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
if (audioSession) [audioSession setActive:YES error:nil];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
// Change the default output audio route
UInt32 doChangeDefaultRoute = 1;
AudioSessionSetProperty(kAudioSessionProperty_OverrideCategoryDefaultToSpeaker,
sizeof(doChangeDefaultRoute), &doChangeDefaultRoute);
The reason the AVPlayer is failing is because you can't create/initialize an Audio Unit while an application is in the background, which is what the AVPlayer is attempting to do under the hood. The exception to this rule is of course audio-playing applications which can be started/resumed via the "Play" buttons built into the OS, while they are in the background. Thus, applications which subscribe to remote control events have the capability to start Audio Units in the background, which is why subscribing to these events appears to solve the problem.
It's unclear whether Apple is OK with using the API feature in this way.

Is there a way to last the ring forever on incoming call in VoIP application?

I'm working on a VoIP-based IOS App.
There are two ways to play some sound to notify the user when a call come in:
Send UILocalNotification with sound. The sound will last for at most 30 seconds;
Play a local Music Asset in setKeepAliveTimeout:handler: function. But System just gives me 10 seconds to do the operation.
Is there any way to play the sound forever like the native Phone app?
I'm afraid #Dan2552 is correct.
Here's what Apple states:
Sounds that last longer than 30 seconds are not supported. If you
specify a file with a sound that plays over 30 seconds, the default
sound is played instead.
EDIT:
Playing an audio file for more than 30 seconds (or forever, for that sake) can be achieved by using AVAudioPlayer from AVFoundation
#import AVFoundation; // module -> no need to link the framework
// #import <AVFoundation/AVFoundation.h> // old style
- (void)playAudio
{
NSString *path = [[NSBundle mainBundle] pathForResource:#"test" ofType:#"mp3"];
NSError *error = nil;
AVAudioPlayer *player = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:&error];
if (!error) {
player.numberOfLoops = -1; // infinite loop
[player play];
} else {
NSLog(#"Audio player init error: %#", error.localizedDescription);
}
}
Then instead of setting the soundName property of the local notification you have to call this method on the main thread:
[self performSelectorOnMainThread:#selector(playAudio) withObject:nil waitUntilDone:NO];
The problem with Islam Q.'s answer is that AVAudioPlayer won't work for a notification since the app will be muted when you're not using the app. Only a UILocalNotification can let your app produce sound when it's not active. The 30 second limit is real. It should be done by repeating the notification with another 30 seconds notification. I don't know how long other VOIP applications will keep ringing but even with a traditional phone there is limit usually, something like a minute.
My strategy would be:
Send initial notification with 29 seconds alarm that somebody tries
to contact
Send second notification with a different 29 seconds alarm after 40 seconds that you are about to miss a call
Send a third notification that you missed a call after 100 seconds
This would allow the user 1:40 of time to respond to a call.
Your code would look something like this:
UILocalNotification* notification = [[UILocalNotification alloc] init];
notification.alertBody = NSLocalizedString(#"FIRST_CALL", #"%# is calling you!");
notification.soundName = #"normal_ring.mp3";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(40 * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
if (!phone.isRinging)
{
// Somebody picked it up already in the mean time
return;
}
UILocalNotification* notification = [[UILocalNotification alloc] init];
notification.alertBody = NSLocalizedString(#"HURRY_CALL", #"Hurry, you are about to miss a call from %#!");
notification.soundName = #"hurry_ring.mp3";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
if (!phone.isRinging)
{
// Somebody picked it up already in the mean time
return;
}
UILocalNotification* notification = [[UILocalNotification alloc] init];
notification.alertBody = NSLocalizedString(#"MISSED_CALL", #"You've just missed a call from %#");
notification.soundName = #"missed_call.mp3";
[[UIApplication sharedApplication] presentLocalNotificationNow:notification];
});
});
If you use the code, keep in mind you first need to make sure the app is in the background so the UILocalNotification would be the only option you have. Use a normal AVAudioPlayer when it's in the foreground, much more flexible. Second thing is that the app becomes foreground when the user responds to the call so it should mute the alert and let the AVAudioPlayer take over.
You want more or less a smooth transition between the alert and the in app sound to have a fully polished experience for your users so the best thing to do would be to measure the time between rings and let AVAudioPlayer start in exact the right time at the start of a new loop.
Your second problem is keeping the app alive in the background. This simple code will simply keep on polling the app every now and then to keep it alive. It comes straight from a working application that does just this, this code polls if a timer is still running and sleeps for five seconds when it is:
UIBackgroundTaskIdentifier backgroundTask = [application beginBackgroundTaskWithExpirationHandler:^{
}];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[timerExecuted timerUpdate];
while (!timerExecuted.isStopped)
{
[NSThread sleepForTimeInterval:5];
}
[application endBackgroundTask:backgroundTask];
});

Resources