Our app explicitly blocks user form using remote-control, e.g., old springboard from pre-iOS7, earbud, by becoming the first responder to the remote-control events. However, on iOS7, the same code fails to bypass the control centre music controls.
From out tests, control centre seems to have bypassed ALL music control events including UIEventSubtypeRemoteControlPause and UIEventSubtypeRemoteControlPlay, and UIEventSubtypeRemoteControlTogglePlayPause.
Is it that control centre has its own protocol for remote control or that the way to intercept remote-control events has changed in iOS7?
The same blocking code still works perfectly with iOS6 devices. Here is what we do:
Added a method in our appDelegate:
(BOOL)canBecomeFirstResponder {
return YES;
}
Call this in applicationDidBecomeActive:
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
// Set itself as the first responder
[self becomeFirstResponder];
Call this in applicationWillResignActive
// Turn off remote control event delivery
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
// Resign as first responder
[self resignFirstResponder];
Finally added
(void)remoteControlReceivedWithEvent:(UIEvent *)receivedEvent {
if (receivedEvent.type == UIEventTypeRemoteControl) {
switch (receivedEvent.subtype) {
case UIEventSubtypeRemoteControlTogglePlayPause:
NSLog(#"Received: UIEventSubtypeRemoteControlTogglePlayPause\n");
break;
case UIEventSubtypeRemoteControlPreviousTrack:
NSLog(#"Received: UIEventSubtypeRemoteControlPreviousTrack\n");
break;
case UIEventSubtypeRemoteControlNextTrack:
NSLog(#"Received: UIEventSubtypeRemoteControlNextTrack\n");
break;
case UIEventSubtypeRemoteControlPlay:
NSLog(#"Received: UIEventSubtypeRemoteControlPlay\n");
break;
case UIEventSubtypeRemoteControlPause:
NSLog(#"Received: UIEventSubtypeRemoteControlPause\n");
break;
case UIEventSubtypeRemoteControlStop:
NSLog(#"Received: UIEventSubtypeRemoteControlStop\n");
break;
default:
NSLog(#"Received: Some remove control events\n");
break;
}
}
}
Any pointer will be appreciated.
you can't block the music app. your app can become one though (apple won't like that) and then the control center would control yours
I think I have a better idea of what happened, at least at the CoreAudio level.
When the app's audio session category is solo-ambient, the music app's play event triggers an audio session interruption similar to an alarm clock or a phone call. This
will trigger app's audio session interruption listener callback with the "enter-interruption" state.
However, the music app's pause event does not trigger the listener callback with the "exit-interruption" state, as one would expect. This missing exit call effectively freezes our app's audio session. Quitting the control centre does not trigger it either. Same thing applies to a physical remote-control, except that the physical remote-control can be blocked using the firstResponder trick said in my last email. It does not work with Control Centre.
Unless I'm missing something obvious, I am more convinced that there are two bugs in either CoreAudio or other frameworks in the chain of command.
Bug 1: Audio session interruption listener's exit call cannot be made from music remote control if the entrance call is made first there.
Bug 2: Control Centre's music remote control does not conform to remote-control event mechanism.
I'm just surprised that no one ever reported this.
I think I'm going to file a bug report unless someone suggests differently.
UPDATE
Bug 2 was a false alarm. After clean rebuilding everything over iOS7 SDK for a couple times, we found that the problem went away. Bug 1 still holds.
Related
I saw people use: [[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; to handle remote control event when managing audio session.
My question:
Does this code only can be used in either a UIViewController class or AppDelegate class? Because I see everyone in Internet using it in one of the two classes. Can I use it in a class which is not subclass of UIViewController or AppDelegate?
For apps that only want to handle remote control events while in a certain state--ready to play media, for example, as opposed to being on the settings or registration screen--invoking beginReceivingRemoteControlEvents from a view controller makes a certain amount of sense. Other apps may wish to suppress remote control behavior altogether (because they take over the audio session and don't want background audio being triggered by the user and corrupting session data).
However, there's nothing that prevents you from factoring that behavior into another area of the app, especially if it is shared. beginReceivingRemoteControlEvents is a method on UIApplication, so as long as you're able to get a handle to [UIApplication sharedApplication], you can flag the app to begin/end remote control event handling.
However, it's worth noting that method is deprecated:
In iOS 7.1 and later, use the shared MPRemoteCommandCenter object to register for remote control events. You do not need to call this method when using the shared command center object. [1]
My experience with remote control event handling is a couple of years old at this point, but I recall there was some non-intuitive behavior dealing with beginReceivingRemoteControlEvents. A quick glance at MPRemoteCommandCenter makes this look like a better API for handling remote control events. If your use cases don't require iOS 7.0 support, you should investigate that API.
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents]; can be used at any point in your application, such as right before starting audio playback, or on launch in application:didFinishLaunchingWithOptions: delegate method.
For applications that want to use the controls for background audio playback throughout the entire application lifecycle (and backgrounded), I would recommend calling beginReceivingRemoteControlEvents in your AppDelegate. That way, you're explicitly ready to receive remote control events at any time during the application lifecycle.
Palpatim mentioned MPRemoteCommandCenter. As someone who has built a complete application around background audio playback (think radio), I would highly recommend this method over the old UIEvent callbacks. I dive into this approach in another answer, but the gist is use MPRemoteCommandCenter.sharedCommandCenter() to enable or disable playback controls on the lock screen and control center, as well as provide selectors:
let commandCenter = MPRemoteCommandCenter.sharedCommandCenter()
commandCenter.previousTrackCommand.enabled = true;
commandCenter.previousTrackCommand.addTarget(self, action: #selector(previousTrack))
commandCenter.nextTrackCommand.enabled = false
commandCenter.nextTrackCommand.addTarget(self, action: #selector(nextTrack))
Note that if you explicitly want to disable controls, you must provide a [dummy] selector for the action in addition to setting enabled = false on the command.
To address your point about becomeFirstResponder:
Any object that inherits from UIResponder, such as a UIViewController or the UIApplicationDelegate can become the first responder, as long as that object implements the required method remoteControlReceivedWithEvent: to handle the remote control events. For example, I could have the AppDelegate become the first responder and handle all remote control events in there, and send out notifications that a UIViewController listens to to pause the player or skip to the next track. You could have a UIViewController become the first responder (the controller holding a reference to the player) and control the player directly. The architecture is pretty open and it really depends on how you structured your application. You haven't provided any code, so I don't know what your player setup looks like.
Your UIResponder handling code will probably look something like this:
- (void)remoteControlReceivedWithEvent:(UIEvent *)event {
if (event.type == UIEventTypeRemoteControl) {
switch (event.subtype) {
case UIEventSubtypeRemoteControlTogglePlayPause:
// Pause or play action
break;
case UIEventSubtypeRemoteControlNextTrack:
// Next track action
break;
case UIEventSubtypeRemoteControlPreviousTrack:
// Previous track action
break;
case UIEventSubtypeRemoteControlStop:
// Stop action
break;
default:
// catch all action
break;
}
}
}
Again, whether you place this in a controller or in your AppDelegate is up to you.
Can I use it in a class which is not subclass of UIViewController or AppDelegate?
Yes, you implement this in any class that inherits from UIResponder. People normally use the AppDelegate or a view controller for convenience.
I have a button which when pressed creates an object thing. It then sleeps for ten seconds and then calls thing.go which gets the application state like
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
Then following this question's answer and comments I check if the app is in the background, and if it is, it displays a local notification.
So I press the button and immediately press the home key to go to the main screen (thereby putting the app in the background)
However, I can tell that the if statement is returning false and is therefore not executing any code within (by putting an NSLog inside the if statement).
So my next thought process was to somehow display the date in the console using NSLog(state). Obviously I cannot do this.
So how can I display the applicationState to resolve my issue? Or is there another way I can check to see if the app is running in the background within this class's method?
Here is the go function which is called inside the IBAction when the button is pressed
-(void)go {
if (state == UIApplicationStateBackground) {
NSLog(#"App is in background");
}
}
Here is the IBAction which is linked to the button, i.e. runs when button is pressed
- (IBAction)button_help {
myclass* thingy = [[myclass alloc] init];
sleep(10);
thingy.go;
}
You should pretty much NEVER use sleep. Forget it exists. It locks up your app, and nothing changes, including transitions to the background.
What you want to do is to set a timer for 10 seconds that then invokes thing.go.
However, that's going to be complicated by the fact that normally timers don't run in the background. In fact, unless you take special steps, your app doesn't get any processor time in the background. You get told that you are going to the background, and then the next call you get is the return-to-foregraound.
When your app gets a message that it is being sent to the background, it will need to make the system call that asks for background processing time. (I don't remember the call off the top of my head, and don't have Xcode running at the moment.)
Apple changed the rules for background processing in iOS 7, but you say this is iOS 6, so those changes don't apply.
If I understand correctly, you're checking to see if the app is returning from a local notification or if it was already active?
if (state == UIApplicationStateActive) {
// do something
NSLog(#"application was active ");
} else {
NSLog(#"sent from notification");
}
My app plays audio streams, it works fine for all cases such as background etc. I am using AudioToolbox.framework and MediaPlayer.framework to play the audio, my query is when the app starts playing audio i would want the indicator on the status bar to be shown as it does for the default iPod player.
Can anyone guide me on how to display the play indication icon on the status bar as soon as my app starts playing audio and disappears when its paused/ stopped or terminated.
As is described by iHemantk:
You just have to register for remote control events and icon will show
up:
https://developer.apple.com/library/ios/documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/Remote-ControlEvents/Remote-ControlEvents.html#//apple_ref/doc/uid/TP40009541-CH7-SW3
they added this in 4.0... works well. whoever is first responder now
controls the icon... just won't work for anything pre-iOS4: put the
following in your -viewDidAppear method:
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
[self becomeFirstResponder];
and include this method in the code:
- (BOOL) canBecomeFirstResponder {
return YES;
}
this goes in `-dealloc`:
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
and while not necessary, you may want to include this in the
-viewWillDisappear:
[self resignFirstResponder];
I am currently developing an app that will need to terminate after running in the background for more than five minutes. In order to do this, I will have to have a timer running in the background after the the Home button has been pressed or in case of an interruptions such as an SMS or a telephone call, then, after five minutes the applicationWillTerminate method will be called. My first question is should I put the applicationWillTerminate in the applicationWillResignActive method or in the applicationDidEnterBackground method? My second question is since this is an app with more that one view, Should I write these things in the AppDelegate class or elsewhere? Thank you for your response.
1) You can't force your app to finish programatically.
2) You should never call these AppDelegate methods by yourself. They're meant to be called only by the system.
Reference: UIApplicationDelegate Protocol Reference.
This is pretty ghetto, but what you can do is make your app crash when you want it to exit, and it will close automatically, granted that's not closing the app, but there's no real harm in it as long as you are in control of how it crashes try to go for a bad access error, aka trying to access something that has been deallocated
as for running a timer in the background, i don't know per say if you can do that, but as an alternative you can save the time when they leave the app aka the app goes into the background and then you can have all the events return to your app of the view controller that is first responder, and each UIEvent has a time stamp, and regardless of which event it is you can compare the time stamps and see if it's greater than 5 minutes
Regardless i don't suggest any of the above, but that is the best answer i can come up with for your question
the code for receiving events out side of your app
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
will start the event tracking and the call back is:
- (void)remoteControlReceivedWithEvent:(UIEvent *)event { }
but you have to remember to
[self becomeFirstResponder];
this tells the device which view controller to go to for the event tracking, oh and don't forget to resign first responder, and endReceivingRemotecontrolEvents
I have an app that needs to do something when it’s sent to background using the Home button and something else when the device is locked using the top hardware button. The standard way of solving these requirements are the notifications and delegate methods sent out by UIApplication. On iOS 4 they look like this:
// Pressing the home button
Will resign active.
Did enter background.
// Tapping app icon on Springboard
Will enter foreground.
Did become active.
// Pressing the lock button
Will resign active.
// Unlocking the device
Did become active.
In other words, it’s quite easy to tell between locking and backgrounding. On iOS 5 the behaviour changed:
// Pressing the home button
Will resign active.
Did enter background.
// Tapping app icon on Springboard
Will enter foreground.
Did become active.
// Pressing the lock button
Will resign active.
Did enter background.
// Unlocking the device
Will enter foreground.
Did become active.
Notice that the didEnterBackground and willEnterForeground notifications are now sent out even when (un)locking the device, making it impossible to tell between locking and backgrounding. Is this change documented somewhere? Is it a regression? Do you know another way to distinguish the two cases?
iOS 6
In my preliminary testing via the simulator, checking the application state with
[[UIApplication sharedApplication] applicationState]
in either
- (void)applicationWillEnterForeground:(UIApplication *)application
- (void)applicationDidEnterBackground:(UIApplication *)application
allows you to differentiate between a call to lock the device and just switching back to the homescreen. A lock screen will return 1 (UIApplicationStateInactive), whereas a home button press will register as a 2 (UIApplicationStateBackground).
It seems consistent and should work on an iOS device just as reliably as it does in the simulator.
iOS 7
The iOS 6 method no longer works in iOS 7. In order to do this now, you have to utilize CFNotificationCenter and listen for a darwin notification (labeled: com.apple.springboard.lockcomplete). You can find the github repo with the sample project here: https://github.com/binarydev/ios-home-vs-lock-button
Credit for the iOS 7 fix goes out to wqq
I have looked into this quite a bit so I would love to be wrong here if someone knows something I don't, but technically, there is no documented way to tell the difference between locking the device, and sending to background.
One thing you can check however, is the UIApplicationState during the transition from foreground to background. Locking a device will give UIApplicationStateInactive and moving the App to the background will give UIApplicationStateBackground. But, since this behaviour is not officially documented it may change in the future.
A basic example:
- (void)applicationDidEnterBackground:(UIApplication *)application {
UIApplicationState state = [[UIApplication sharedApplication] applicationState];
NSLog(#"Device state: %#", state);
switch (state) {
case UIApplicationStateActive:
/* ... */
break;
case UIApplicationStateInactive:
/* Device was/is locked */
break;
case UIApplicationStateBackground:
/* User pressed home button or opened another App (from an alert/email/etc) */
break;
}
}
UIApplicationState - The running states of an application
typedef enum {
UIApplicationStateActive,
UIApplicationStateInactive,
UIApplicationStateBackground
}
UIApplicationState
Constants
UIApplicationStateActive - The application
is running in the foreground and currently receiving events. Available
in iOS 4.0 and later.
UIApplicationStateInactive - The application is running in the
foreground but is not receiving events. This might happen as a result
of an interruption or because the application is transitioning to or
from the background.
UIApplicationStateBackground - The application is
running in the background.
According to the UIApplicationDelegate Protocol Reference:
applicationWillResignActive:
didEnterBackground:
// ...
willEnterForeground:
applicationDidBecomeActive:
are the only methods that ever get called in both situations.
According to the iOS 4.3 to iOS 5.0 API Diff, these are the ONLY changes regarding UIApplication or UIApplicationDelegate, so I couldn't find where they documented any of these notification changes:
UIApplication.h
Added -[UIApplication setNewsstandIconImage:]
Added UIApplication.userInterfaceLayoutDirection
Added UIApplicationDelegate.window
Added UIApplication(UINewsstand)
Added UIApplicationLaunchOptionsNewsstandDownloadsKey
Added UIRemoteNotificationTypeNewsstandContentAvailability
Added UIUserInterfaceLayoutDirection
Added UIUserInterfaceLayoutDirectionLeftToRight
Added UIUserInterfaceLayoutDirectionRightToLeft
This is more of a workaround/hack, but according to my experience it's very reliable.
When the device is screen-locked (not just home button-ed, if that's a word :)) - bound network (UDP) sockets are broken.
I was using GCDAsyncUDPSocket (also AsyncUDPSocket before) and they both fire a network/broken pipe error reliably when the device is turned off.
In my case I need the UDP socket anyway, for other apps it might be a bit smelly, however, just binding/listening on a UDP socket without any action is not too terrible if you really need to differentiate here.
This note will [self destruct]; is 5 minutes (so Apple won't find out).
There’s a thread about this issue on Apple Developer Forums (registered developers only, sorry). The gist is that the new behaviour is by design. There are requests for a new API feature to distinguish between the two use cases, but nothing working yet.
Here is what Apple's iOS Programming Guide says:
Pressing the Sleep/Wake button is another type of interruption that
causes your app to be deactivated temporarily. When the user presses
this button, the system disables touch events, moves the app to the
background but sets the value of the app’s applicationState property
to UIApplicationStateInactive (as opposed to
UIApplicationStateBackground), and finally locks the screen.
http://developer.apple.com/library/ios/#DOCUMENTATION/iPhone/conceptual/iPhoneOSProgrammingGuide/ManagingYourApplicationsFlow/ManagingYourApplicationsFlow.html
So, you should check the UIApplication's applicationState property in applicationDidEnterBackground:. If it is UIApplicationStateBackground the user pressed the home button. But if it is UIApplicationStateInactive the user locked the device.