Resuming execution of code after interruption while app is in background - objective-c

I have spent days researching on SO and other websites for the answer to this but without any luck.
Essentially, the challenge I've set for myself is to create an alarm clock app for iOS that will sound no matter where the user may be (foreground or background). This I have already accomplished by using an AVAudioPlayer instance and starting to play an empty sound file when the user sets the alarm in order for the app to keep running in the background. When it is time for the alarm to go off (ie when the NSTimer is fired) a second player, which has already been initiated and prepared to play, starts playing the ringtone to which the user wakes up.
Also, I have managed to handle interruptions by a phone call, system timer or alarm clock by implementing the AVAudioSessionDelegate methods beginInterruption and endInterruptionWithFlags. It works both in background and foreground modes, but the strangest thing happens:
When the interruption ends, the AVAudioPlayer resumes playing BUT I cannot execute any other code in my app unless I bring the app to the foreground again.
To get to the bottom of this, I have experimented with a much simpler project which I am posting below.
What this app does is, as soon as you enter the app, an instance of the AVAudioPlayer class starts looping a certain sound. Then when you bring it to the background, the player continues to loop the sound. When an interruption occurs I pause the player and when it ends I use a dispatch to wait a couple of seconds before it calls two methods, ie (void)playPlayer, a method that contains the code to resume playing the file and (void)tester, a method that contains a timer, which is set to stop the player 5 seconds after the interruption (or 7 seconds to be exact) has ended. Both the methods get called as indicated by the NSLogs I have put in both of them, but the timer never gets fired and the player continues to play indefinitely.
Here is the code for the .h file:
#import <UIKit/UIKit.h>
#import <AVFoundation/AVFoundation.h>
#import <AudioToolbox/AudioToolbox.h>
#interface InterruptionTest3ViewController : UIViewController <AVAudioSessionDelegate, AVAudioPlayerDelegate>
{
AVAudioSession *mySession;
AVAudioPlayer *myPlayer;
}
-(void) playPlayer;
-(void) pausePlayer;
-(void) tester;
#end
Here is the code for the .m file:
#import "InterruptionTest3ViewController.h"
#interface InterruptionTest3ViewController ()
#end
#implementation InterruptionTest3ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
mySession = [AVAudioSession sharedInstance];
NSError *setActiveError = nil;
[mySession setActive:YES withFlags:AVAudioSessionSetActiveFlags_NotifyOthersOnDeactivation error:&setActiveError];
if (setActiveError) {
NSLog(#"Session failed to activate within viewDidLoad");
}
else {
NSLog(#"Session was activated within viewDidLoad");
}
NSError *setCategoryError = nil;
[mySession setCategory:AVAudioSessionCategoryPlayback error:&setCategoryError];
if (setCategoryError) {
NSLog(#"Category failed to be set");
}
else {
NSLog(#"Category has been set");
}
[mySession setDelegate:self];
NSString *path = [[NSBundle mainBundle] pathForResource:#"headspin" ofType:#"wav"];
NSError *initMyPlayerError = nil;
myPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:path] error:&initMyPlayerError];
if (initMyPlayerError) {
NSLog(#"myPlayer failed to initiate");
}
else {
NSLog(#"myPlayer has been initiated");
}
[myPlayer prepareToPlay];
[self playPlayer];
OSStatus propertySetError = 0;
UInt32 allowMixing = true;
propertySetError = AudioSessionSetProperty (
kAudioSessionProperty_OverrideCategoryMixWithOthers,
sizeof (allowMixing),
&allowMixing
);
[myPlayer setNumberOfLoops:-1];
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
}
-(void) beginInterruption
{
[myPlayer pause];
}
-(void) endInterruptionWithFlags:(NSUInteger)flags
{
if (flags) {
if (AVAudioSessionInterruptionFlags_ShouldResume)
{
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC),dispatch_get_main_queue(), ^{
[self playPlayer];
[self tester];
});
}
}
}
}
-(void) tester
{
[NSTimer timerWithTimeInterval:5.0 target:self selector:#selector(pausePlayer) userInfo:nil repeats:NO];
NSLog(#"tester method has been called");
}
-(void) playPlayer
{
[NSTimer timerWithTimeInterval:5.0 target:myPlayer selector:#selector(stop) userInfo:nil repeats:NO];
[myPlayer play];
NSLog(#"playPlayer method has been called");
}
-(void) pausePlayer
{
[myPlayer pause];
}
//viewDidUnload etc not listed.
So, this is it folks. Again, why is the timer not being fired after an interruption while the app is in the background? Do I need to set something in the applicationDidEnterBackground method?
Thank you very much in advance!

please use "[NSTimer scheduledTimerWithTimeInterval:5.0 target:myPlayer selector:#selector(stop) userInfo:nil repeats:NO]";

Not to avoid the question, but the play-an-empty-sound-in-the-background bit is a hack. The ability to play a sound in the background is provided so that you can play music or other sound for the user's benefit, not to keep a background process alive.
Consider using local notifications instead if you want to play a sound or otherwise alert the user at a particular time.
why is the timer not being fired after an interruption while the app is in the background?
If I remember correctly, timers are either suspended or cancelled when your app enters the background. The documentation explicitly says that you should "stop timers and other periodic tasks" when your app is interrupted, i.e. when it's sent to the background.

Related

Touches are getting noticed twice in objective c

I am implementing session inactivity for my app so that if user is inactive for 30 seconds, then show him a new uiviewcontroller as a formsheet. For touch event, i am using this code
(void)sendEvent:(UIEvent *)event {
[super sendEvent:event];
// Only want to reset the timer on a Began touch or an Ended touch, to reduce the number of timer resets.
NSSet *allTouches = [event allTouches];
if ([allTouches count] > 0) {
// allTouches count only ever seems to be 1, so anyObject works here.
UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded) {
[[BCDTimeManager sharedTimerInstance]resetIdleTimer];
}
}
}
In BCDTimeManager class which is a singleton class i have implemented resetIdleTimer and idleTimerExceed method
#import "BCDTimeManager.h"
#implementation BCDTimeManager
__strong static BCDTimeManager *sharedTimerInstance = nil;
NSTimer *idleTimer;
NSTimeInterval timeinterval;
+ (BCDTimeManager*)sharedTimerInstance
{
static dispatch_once_t predicate = 0;
dispatch_once(&predicate, ^{
sharedTimerInstance = [[self alloc] init];
NSString *timeout = [[NSUserDefaults standardUserDefaults] valueForKey:#"session_timeout_preference"];
timeinterval = [timeout doubleValue];
});
return sharedTimerInstance;
}
- (void)resetIdleTimer {
if (idleTimer) {
[idleTimer invalidate];
}
idleTimer = nil;
NSLog(#"timeout is %ld",(long)timeinterval);
idleTimer = [NSTimer scheduledTimerWithTimeInterval:timeinterval target:self selector:#selector(idleTimerExceeded) userInfo:nil repeats:true];
}
- (void)idleTimerExceeded {
NSLog(#"idle time exceeded");
[[NSNotificationCenter defaultCenter]
postNotificationName:#"ApplicationTimeout" object:nil];
}
But when i do any touch on the screens, in console, i can see NSLog is printed twice which is causing my NSNOtification action to be triggered twice.
I am not sure what i am doing wrong. Please help me to figure out this.
I figured it out. Code is doing right. I am seeing NSLog twice because of two touch event one touch began and one touch ended. So, this code is correct without any issue. Something is wrong with observers add or remove method. I will look into that

Invalidate Timers when user press button home, doesn't work

I have this code in app delegate that invokes an existing method in my view that invalidate timers and pause the music of my application:
Appdelegate.m
- (void)applicationWillResignActive:(UIApplication *)application{
GameViewController *gamer = [[GameViewController alloc] init];
[gamer EnterBackground];
}
GameViewController.m
-(void)EnterBackground{
NSLog(#"App Enter in background mode!");
[t1 invalidate];
[t2 invalidate];
[t3 invalidate];
[_audioPlayer stop];
}
I can not understand why the simulator when I press the lock button or home, the NSLog works, but timers and the audio still continue to run, why this is happening? there is a possible solution?

iOS: Terminating app in background after a certain period of time

I'm trying to implement a passcode lock feature in my app that lets the user choose how much time must go by before the passcode is required for reentry (similar to the passcode functionality of the OS). So for example the user may be able to select that they want the passcode to be required 5, 10, 20 minutes after exiting the app into the background.
I've tried to deal with presenting a passcode view in different ways, but it is often difficult to figure out the best way to present it, and so I had the idea that perhaps it is best to terminate the app after the time is up, and therefore I would only have to present the passcode screen when the app is launched.
Is this possible to do? I had two thoughts about ways to approach this.
1) Have an NSTimer within the app delegate, start it when the app goes into the background, and then when/if the timer reaches the set number of minutes, then terminate the app? I could see a number of things going wrong with this, for example if the OS terminated the app to free up memory sooner than the timer finished. Although that wouldn't be a huge issue.
2) Set an instance of NSDate when the app goes into the background. Then when the app is being launched, see if this date is more than x minutes ago, and present the passcode entry screen depending on that.
I feel like both of these are a little off. I'm inexperienced with Timers, RunLoops, etc, so any advice is appreciated.
Option 2 seems to be a good solution that we have used with success.
Option 2. Use the ApplicationDelegate Lifecycle methods to drive it.
application:didFinishLaunchingWithOptions:
applicationDidBecomeActive:
applicationWillResignActive:
applicationDidEnterBackground:
applicationWillEnterForeground:
applicationWillTerminate:
applicationDidFinishLaunching:
In the applicationWillResignActive method persist the current timestamp to your UserDefaults, and in the applicationWillEnterForeground check this against the current time and if the passcode interval has passed, present your passcode.
(probably best to clear the timestamp when you are active to minimise the chance of false triggering on receiving calls and SMS etc)
Depending on sensitivity you may want to prepare your views before entering foreground to obscure sensitive data, so they do not return in the unlocked state.
you can follow both for better result. for example use option 2 when app active from didFinishLaunchingWithOptions: and option 1 when application enable from
- (void)applicationDidBecomeActive:(UIApplication *)application or - (void)applicationWillEnterForeground:(UIApplication *)application
option 1-Easiest way is to schedule a NSTimer on the background run-loop. I suggest that the following code is implemented on your application delegate, and that you call setupTimer from applicationWillResignActive:.
- (void)applicationWillResignActive:(UIApplication *)application
{
[self performSelectorInBackground:#selector(setupTimerThread) withObject:nil];
}
-(void)setupTimerThread;
{
  NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
  NSTimer* timer = [NSTimer timerWithTimeInterval:10 * 60 target:self selector:#selector(triggerTimer:) userInfo:nil repeats:YES];
  NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
  [runLoop addTimer:timer forModes:NSRunLoopCommonModes];
  [runLoop run];
  [pool release];
}
-(void)triggerTimer:(NSTimer*)timer;
{
  // Do your stuff
}
in appDelegate .h
UIBackgroundTaskIdentifier bgTask;
in appDelegate .m
- (void)applicationDidEnterBackground:(UIApplication *)application
{
UIApplication* app = [UIApplication sharedApplication];
// Request permission to run in the background. Provide an
// expiration handler in case the task runs long.
NSAssert(bgTask == UIBackgroundTaskInvalid, nil);
bgTask = [app beginBackgroundTaskWithExpirationHandler:^{
// Synchronize the cleanup call on the main thread in case
// the task actually finishes at around the same time.
dispatch_async(dispatch_get_main_queue(), ^{
if (bgTask != UIBackgroundTaskInvalid)
{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
});
}];
// Start the long-running task and return immediately.
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Do the work associated with the task.
// Synchronize the cleanup call on the main thread in case
// the expiration handler is fired at the same time.
dispatch_async(dispatch_get_main_queue(), ^{
if (bgTask != UIBackgroundTaskInvalid)
{
[app endBackgroundTask:bgTask];
bgTask = UIBackgroundTaskInvalid;
}
});
});
NSLog(#"app entering background");
/*
Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
*/
}
OR you could run the NSTimer on a background thread by with something like this (I am intentionally leaking the thread object):
-(void)startTimerThread;
{
  NSThread* thread = [[NSThread alloc] initWithTarget:self selector:#selector(setupTimerThread) withObject:nil];
  [thread start];
}
try with this above code. we use both options its works fine for us. good luck

How to make avaudioplayer stop in a different class

In my app delegate I have this code in the application did finish launching method:
NSString *music = [[NSBundle mainBundle]
pathForResource:#"appsong" ofType:#"m4a"];
self.audio = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:music] error:NULL];
audio.delegate = self;
[audio play];
audio.numberOfLoops = -1;
I have a page where another peice of music should play, how can i say that the avaudioplayer in the app delegate should stop and start again WHEN I QUIT THAT VIEW.
Well since your global player is in your app delegate, the view that wants to play it's own music might add:
-(void)viewWillAppear:(BOOL)animated {
// ...
[(YourSpecificAppDelClass*)[UIApplication sharedApplication].delegate pauseAudioPlayer];
}
and
-(void)viewWillDisappear:(BOOL)animated {
// ...
[(YourSpecificAppDelClass*)[UIApplication sharedApplication].delegate startAudioPlayer];
}
where startAudioPlayer and stopAudioPlayer are exposed in your app delegates header file.
Call a function that stops the player thats in the appdelegate class from ur view class?

How can I fade out an AVAudioPlayer on a background thread?

I have an audio file which needs to fade out while the user is scrolling a UIScrollView. However, any performSelector:withObject:afterDelay: method is blocked until the user has stopped scrolling. So I have tried to create some code to perform a fadeout on another thread:
- (void)fadeOut
{
[NSThread detachNewThreadSelector:#selector(fadeOutInBackground:) toTarget:self withObject:self.audioPlayer];
}
- (void)fadeOutInBackground:(AVAudioPlayer *)aPlayer
{
NSAutoreleasePool *myPool = [[NSAutoreleasePool alloc] init];
[self performSelector:#selector(fadeVolumeDown:) withObject:aPlayer afterDelay:0.1];
[myPool release];
}
- (void)fadeVolumeDown:(AVAudioPlayer *)aPlayer
{
aPlayer.volume = aPlayer.volume - 0.1;
if (aPlayer.volume < 0.1) {
[aPlayer stop];
} else {
[self performSelector:#selector(fadeVolumeDown:) withObject:aPlayer afterDelay:0.1];
}
}
It gets as far as the performSelector, but no further because I guess it's trying to perform on a thread it has no access to. I can't even change it for performSelector:onThread:withObject:waitUntilDone: because there is no delay option.
Any ideas? Why have they made it so hard to just fade out a sound? moan
Thanks!
I resolved a similar issue by scheduling the selector in a different run loop mode than the default one. This way it is not interfering with the scrolling events. Using the NSRunLoopCommonModes worked for me:
[self performSelector:#selector(fadeVolumeDown:)
withObject:aPlayer
afterDelay:0.1
inModes:[NSArray arrayWithObject: NSRunLoopCommonModes]];
Inspired by the answer above
while (theAudio.volume > 0.000000)
[self fadeVolumeDown];
- (void) fadeVolumeDown{
theAudio.volume -=0.01;
}