I want to show an alert view when my iOS app is in the background (and it's using location).
For example, the Uber Partner (Driver) app shows an alert and plays a sound even when:
I have turned off notifications!
My iPhone is in Silent mode!
I am aware of the local notifications approach and it doesn't work if the user turns off/ changes the Notifications in Settings. I am looking for something different.
Actions performed to reach the above state:
Go online on Uber Partner App (you are the driver!)
Disable Notifications for the app in Settings
Move the app to background and wait for a Ride Request
After some time, a ride Request is popped up as an Alert view and a sound is played in the background
Of course, silent remote notifications can be tapped in by the app using the didReceiveRemoteNotification: fetchCompletionHandler: API even if the user disables Notifications in Settings. But, how the alert is popped up, that's what I am trying to find out.
I would imagine that Uber has some special permissions or uses some private API that allow them to achieve this behavior without using local notifications. While I don't know how Uber implemented this in their partner app, I can talk a little bit about how alerts work on the home screen.
SpringBoard is the singleton class that manages the SpringBoard application (SpringBoard.app), the application launcher for the iPhone. SpringBoard doesn't use the standard UIAlertView/UIAlertController classes, since they don't participate in the SpringBoard-wide alert system. iOS 5 introduced SBAlertItem the which is used to display UIAlertViews on SpringBoard (Battery Notification Alerts, Sim Unlock Alert, etc.). Apple uses SBAlertItem for their lock and home screen alerts, I'll be working on the assumption that Uber is using an SBAlertItem for this answer.
SBAlertItem has a protected ivar UIAlertView *_alertSheet. Assuming this acts as a normal UIAlertView, you should be able to change the properties on this alert to fit your needs. I would also read through saurik's Cydia Substrate project, specifically MobileSafety.mm to see some use cases. I've also found noweibogoodsleep which provides an example of using SBAlertItem on the SpringBoard.
I've also found SBUserNotificationAlert, a subclass of SBAlertItem. This appears to have more methods to facilitate alert customization that may fit your needs better than the standard SBAlertItem.
I realize hooking into private APIs is probably not what you were expecting when asking this question. Since I don't know how Uber works, I can only provide an answer from my personal experience working with the runtime and jailbroken devices.
After some static analysis of the binary, it became clear that they are not using PKPushRegistry (VOIP), undocumented NSNotificationCenter calls or SBAlertItem.
Took a little while to find it, but they are actually using CFUserNotification for the alerts. The class is documented for Mac, but private for iOS.
I found the usage by doing this:
nm -u ~/Downloads/Payload/UberDriver.app/UberDriver | grep CFUserNotification
The output is:
_CFUserNotificationCancel
_CFUserNotificationCreate
_CFUserNotificationCreateRunLoopSource
_kCFUserNotificationAlertHeaderKey
_kCFUserNotificationAlertMessageKey
_kCFUserNotificationAlertTopMostKey
_kCFUserNotificationAlternateButtonTitleKey
_kCFUserNotificationDefaultButtonTitleKey
_kCFUserNotificationSoundURLKey
If I grep for PKPushRegistry or for SBAlertItem, both return no results.
Can use the class by importing this file to your project.
UPDATE
I have 'working' code, however it immediately calls the callback function (responseFlags set to kCFUserNotificationCancelResponse) without showing the alert..
I am using the same keys and calls as the Uber app (compare code below to list above), so there must be something extra. Will keep looking.
#import "CFUserNotification.h"
#interface AppDelegate ()
#property (nonatomic) CFRunLoopSourceRef runLoopSource;
#property (nonatomic) CFUserNotificationRef notification;
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
SInt32 error;
NSDictionary *keys = #{(__bridge NSString*)kCFUserNotificationAlertHeaderKey: #"Hello",
(__bridge NSString*)kCFUserNotificationAlertMessageKey: #"World",
(__bridge NSString*)kCFUserNotificationAlertTopMostKey: #YES,
(__bridge NSString*)kCFUserNotificationDefaultButtonTitleKey: #"asdf",
(__bridge NSString*)kCFUserNotificationAlternateButtonTitleKey: #"asdf",
};
self.notification = CFUserNotificationCreate(NULL, 10, kCFUserNotificationPlainAlertLevel, &error, (__bridge CFDictionaryRef)keys);
self.runLoopSource = CFUserNotificationCreateRunLoopSource(NULL, self.notification, NotificationCallback, 0);
CFRunLoopAddSource(CFRunLoopGetMain(), self.runLoopSource, kCFRunLoopCommonModes);
return YES;
}
void NotificationCallback(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
NSLog(#"got response: %lu", responseFlags);
}
Your question missed the most important part regarding the "Uber Partner" app that would make things a lot clearer. The "Uber Partner" app is an Enterprise app and is not restricted to the Appstore guide lines.
it didn't got any special permissions like other answers suggested.
It is possible to display an alert view using SBAlertItem regardless of Sound \ Notification settings but if your end goal is to make it to the appstore, unfortunately, your app will be rejected for using private API.
Related
I tested:
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
which is for putting app on background and it works.
How do I put app back on foreground?
I tried:
UIControl().sendAction(#selector(URLSessionTask.resume), to: UIApplication.shared, for: nil)
But eventually it crashes...
Thank you
Update:
Since you've indicated that you're looking for any technical solution, even those not compatible with the App Store or Apple's terms, this should be possible using the Private API LSApplicationWorkspace: openApplicationWithBundleID. Try something like this:
Create a .h file and set up an interface to the LSApplicationWorkspace class and list the required method. You will need to #import "PrivateHeaders.h" in your bridging header.
//
// PrivateHeaders.h
//
#ifndef PrivateHeaders_h
#define PrivateHeaders_h
#interface LSApplicationWorkspace : NSObject
- (bool)openApplicationWithBundleID:(id)arg1;
#end
#endif /* PrivateHeaders_h */
You should then be able to call this function and pass in the Bundle Identifier of your app as an string.
//
// SomeClass.swift
//
import MobileCoreServices
let workspace = LSApplicationWorkspace()
/**
Launch an App given its bundle identifier
- parameter bundleIdentifier: The bundle identifier of the app to launch
- returns: True if app is launched, otherwise false
*/
func openApp(withBundleIdentifier bundleIdentifier: String) -> Bool {
// Call the Private API LSApplicationWorkspace method
return workspace.openApplication(withBundleID: bundleIdentifier)
}
Original:
What you are doing is likely a violation of the iOS Human Interface Guidelines (although the "Don’t Quit Programmatically" is no longer specifically defined), so as the comments have said, it is not suited to the App Store. Regardless, once your app is suspended in this way, I don't expect that there is a way to resume it programmatically, unless you can hook into a Background Operation to run URLSessionTask.resume, but I have not tested it and am unsure whether it can work.
Apps can be launched (and hence brought into the foreground) programmatically from another app or today extension by using a Custom URL Scheme, or via a Push Notification. It isn't possible to launch the app from the Background Operation via a URL Scheme, since it is part of the UIKit framework, which must be run in the main thread.
In summary, I think your best option is to try to use a Notification. This just means that the user will need to click on the notification to bring your app back into the foreground.
Closing/opening the app should be done explicitly by the user. Any other way of closing or opening the app is not supported by Apple and will be rejected when uploaded to app store. iOS Human Interface Guideline states:
Don’t Quit Programmatically
Never quit an iOS application
programmatically because people tend to interpret this as a crash.
However, if external circumstances prevent your application from
functioning as intended, you need to tell your users about the
situation and explain what they can do about it. Depending on how
severe the application malfunction is, you have two choices.
*Display
an attractive screen that describes the problem and suggests a
correction. A screen provides feedback that reassures usersthat
there’s nothing wrong with your application. It puts usersin control,
letting them decide whether they want to take corrective action and
continue using your application or press the Home button and open a
different application
*If only some of your application's features are
not working, display either a screen or an alert when people activate
the feature. Display the alert only when people try to accessthe
feature that isn’t functioning
Just as a follow up to Jordan's excellent answer I want to give an explanation for why your code works in the first place and why that alone will get your app rejected, even without any functionality to make it active again and bring it to the foreground.
As maddy pointed out in a comment, you're basically calling a method from UIApplication's private API. This works due to the Objective-C runtime's dynamic linking. You might wonder "But I am using Swift, what does that have to do with Objective-C?" The answer lies in #selector mechanism. A Selector is basically just a symbol that the Objective-C runtime looks up in a table to get a method it invokes (for you). This is why it's technically not correct to say you "call a method" when you do something like myObjectInstance.someMethod(). The correct way to phrase that would be to "send a message" to the object, because that's what is happening in the runtime. The target-action mechanism is build around that. The sendAction(_: Selector?, to: Any?) method does the same thing. So in effect your code does the following:
Get the symbol that corresponds to URLSessionTask's suspend() method.
Tell the shared instance of UIApplication to invoke the method that it has for that symbol.
Now usually that would result in a crash with the typical "unknown selector sent to instance..." error message. But here, by sure coincidence UIApplication also has a method for that instance (or rather, the runtime also has one of its methods listed in its table for that symbol). You kind of "found" a method that is not declared in its public header. You successfully circumvented a compile-time check for this and invoke a method that is part of a private API. This is explicitly forbidden in the Apple Developer Program License Agreement
Besides all that, I would strongly advise against trying to design an app that way in the first place. As maddy pointed out it's also likely considered to violate the HIGs. Even if you're not trying to do anything malicious and properly explain the feature in your app's description, that won't make Apple let it slide (I assume). Personally, as a user, I'd also find it annoying if the app did something the system already has a specific mechanic for in a different manner, at least in terms of app's coming to background and foreground.
I don't think it can be done without user interaction
The option is you can generate a push notification to tell the user to bring the application to foreground
When the operating system delivers push notification and the target application is not running in the foreground, it presents the notification.
If there is a notification alert and the user taps or clicks the action button (or moves the action slider), the application launches and calls a method to pass in the local-notification object or remote-notification payload.
I tested:
UIControl().sendAction(#selector(URLSessionTask.suspend), to: UIApplication.shared, for: nil)
which is for putting app on background and it works.
How do I put app back on foreground?
I tried:
UIControl().sendAction(#selector(URLSessionTask.resume), to: UIApplication.shared, for: nil)
But eventually it crashes...
Thank you
Update:
Since you've indicated that you're looking for any technical solution, even those not compatible with the App Store or Apple's terms, this should be possible using the Private API LSApplicationWorkspace: openApplicationWithBundleID. Try something like this:
Create a .h file and set up an interface to the LSApplicationWorkspace class and list the required method. You will need to #import "PrivateHeaders.h" in your bridging header.
//
// PrivateHeaders.h
//
#ifndef PrivateHeaders_h
#define PrivateHeaders_h
#interface LSApplicationWorkspace : NSObject
- (bool)openApplicationWithBundleID:(id)arg1;
#end
#endif /* PrivateHeaders_h */
You should then be able to call this function and pass in the Bundle Identifier of your app as an string.
//
// SomeClass.swift
//
import MobileCoreServices
let workspace = LSApplicationWorkspace()
/**
Launch an App given its bundle identifier
- parameter bundleIdentifier: The bundle identifier of the app to launch
- returns: True if app is launched, otherwise false
*/
func openApp(withBundleIdentifier bundleIdentifier: String) -> Bool {
// Call the Private API LSApplicationWorkspace method
return workspace.openApplication(withBundleID: bundleIdentifier)
}
Original:
What you are doing is likely a violation of the iOS Human Interface Guidelines (although the "Don’t Quit Programmatically" is no longer specifically defined), so as the comments have said, it is not suited to the App Store. Regardless, once your app is suspended in this way, I don't expect that there is a way to resume it programmatically, unless you can hook into a Background Operation to run URLSessionTask.resume, but I have not tested it and am unsure whether it can work.
Apps can be launched (and hence brought into the foreground) programmatically from another app or today extension by using a Custom URL Scheme, or via a Push Notification. It isn't possible to launch the app from the Background Operation via a URL Scheme, since it is part of the UIKit framework, which must be run in the main thread.
In summary, I think your best option is to try to use a Notification. This just means that the user will need to click on the notification to bring your app back into the foreground.
Closing/opening the app should be done explicitly by the user. Any other way of closing or opening the app is not supported by Apple and will be rejected when uploaded to app store. iOS Human Interface Guideline states:
Don’t Quit Programmatically
Never quit an iOS application
programmatically because people tend to interpret this as a crash.
However, if external circumstances prevent your application from
functioning as intended, you need to tell your users about the
situation and explain what they can do about it. Depending on how
severe the application malfunction is, you have two choices.
*Display
an attractive screen that describes the problem and suggests a
correction. A screen provides feedback that reassures usersthat
there’s nothing wrong with your application. It puts usersin control,
letting them decide whether they want to take corrective action and
continue using your application or press the Home button and open a
different application
*If only some of your application's features are
not working, display either a screen or an alert when people activate
the feature. Display the alert only when people try to accessthe
feature that isn’t functioning
Just as a follow up to Jordan's excellent answer I want to give an explanation for why your code works in the first place and why that alone will get your app rejected, even without any functionality to make it active again and bring it to the foreground.
As maddy pointed out in a comment, you're basically calling a method from UIApplication's private API. This works due to the Objective-C runtime's dynamic linking. You might wonder "But I am using Swift, what does that have to do with Objective-C?" The answer lies in #selector mechanism. A Selector is basically just a symbol that the Objective-C runtime looks up in a table to get a method it invokes (for you). This is why it's technically not correct to say you "call a method" when you do something like myObjectInstance.someMethod(). The correct way to phrase that would be to "send a message" to the object, because that's what is happening in the runtime. The target-action mechanism is build around that. The sendAction(_: Selector?, to: Any?) method does the same thing. So in effect your code does the following:
Get the symbol that corresponds to URLSessionTask's suspend() method.
Tell the shared instance of UIApplication to invoke the method that it has for that symbol.
Now usually that would result in a crash with the typical "unknown selector sent to instance..." error message. But here, by sure coincidence UIApplication also has a method for that instance (or rather, the runtime also has one of its methods listed in its table for that symbol). You kind of "found" a method that is not declared in its public header. You successfully circumvented a compile-time check for this and invoke a method that is part of a private API. This is explicitly forbidden in the Apple Developer Program License Agreement
Besides all that, I would strongly advise against trying to design an app that way in the first place. As maddy pointed out it's also likely considered to violate the HIGs. Even if you're not trying to do anything malicious and properly explain the feature in your app's description, that won't make Apple let it slide (I assume). Personally, as a user, I'd also find it annoying if the app did something the system already has a specific mechanic for in a different manner, at least in terms of app's coming to background and foreground.
I don't think it can be done without user interaction
The option is you can generate a push notification to tell the user to bring the application to foreground
When the operating system delivers push notification and the target application is not running in the foreground, it presents the notification.
If there is a notification alert and the user taps or clicks the action button (or moves the action slider), the application launches and calls a method to pass in the local-notification object or remote-notification payload.
I want to show an alert view when my iOS app is in the background (and it's using location).
For example, the Uber Partner (Driver) app shows an alert and plays a sound even when:
I have turned off notifications!
My iPhone is in Silent mode!
I am aware of the local notifications approach and it doesn't work if the user turns off/ changes the Notifications in Settings. I am looking for something different.
Actions performed to reach the above state:
Go online on Uber Partner App (you are the driver!)
Disable Notifications for the app in Settings
Move the app to background and wait for a Ride Request
After some time, a ride Request is popped up as an Alert view and a sound is played in the background
Of course, silent remote notifications can be tapped in by the app using the didReceiveRemoteNotification: fetchCompletionHandler: API even if the user disables Notifications in Settings. But, how the alert is popped up, that's what I am trying to find out.
I would imagine that Uber has some special permissions or uses some private API that allow them to achieve this behavior without using local notifications. While I don't know how Uber implemented this in their partner app, I can talk a little bit about how alerts work on the home screen.
SpringBoard is the singleton class that manages the SpringBoard application (SpringBoard.app), the application launcher for the iPhone. SpringBoard doesn't use the standard UIAlertView/UIAlertController classes, since they don't participate in the SpringBoard-wide alert system. iOS 5 introduced SBAlertItem the which is used to display UIAlertViews on SpringBoard (Battery Notification Alerts, Sim Unlock Alert, etc.). Apple uses SBAlertItem for their lock and home screen alerts, I'll be working on the assumption that Uber is using an SBAlertItem for this answer.
SBAlertItem has a protected ivar UIAlertView *_alertSheet. Assuming this acts as a normal UIAlertView, you should be able to change the properties on this alert to fit your needs. I would also read through saurik's Cydia Substrate project, specifically MobileSafety.mm to see some use cases. I've also found noweibogoodsleep which provides an example of using SBAlertItem on the SpringBoard.
I've also found SBUserNotificationAlert, a subclass of SBAlertItem. This appears to have more methods to facilitate alert customization that may fit your needs better than the standard SBAlertItem.
I realize hooking into private APIs is probably not what you were expecting when asking this question. Since I don't know how Uber works, I can only provide an answer from my personal experience working with the runtime and jailbroken devices.
After some static analysis of the binary, it became clear that they are not using PKPushRegistry (VOIP), undocumented NSNotificationCenter calls or SBAlertItem.
Took a little while to find it, but they are actually using CFUserNotification for the alerts. The class is documented for Mac, but private for iOS.
I found the usage by doing this:
nm -u ~/Downloads/Payload/UberDriver.app/UberDriver | grep CFUserNotification
The output is:
_CFUserNotificationCancel
_CFUserNotificationCreate
_CFUserNotificationCreateRunLoopSource
_kCFUserNotificationAlertHeaderKey
_kCFUserNotificationAlertMessageKey
_kCFUserNotificationAlertTopMostKey
_kCFUserNotificationAlternateButtonTitleKey
_kCFUserNotificationDefaultButtonTitleKey
_kCFUserNotificationSoundURLKey
If I grep for PKPushRegistry or for SBAlertItem, both return no results.
Can use the class by importing this file to your project.
UPDATE
I have 'working' code, however it immediately calls the callback function (responseFlags set to kCFUserNotificationCancelResponse) without showing the alert..
I am using the same keys and calls as the Uber app (compare code below to list above), so there must be something extra. Will keep looking.
#import "CFUserNotification.h"
#interface AppDelegate ()
#property (nonatomic) CFRunLoopSourceRef runLoopSource;
#property (nonatomic) CFUserNotificationRef notification;
#end
#implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
SInt32 error;
NSDictionary *keys = #{(__bridge NSString*)kCFUserNotificationAlertHeaderKey: #"Hello",
(__bridge NSString*)kCFUserNotificationAlertMessageKey: #"World",
(__bridge NSString*)kCFUserNotificationAlertTopMostKey: #YES,
(__bridge NSString*)kCFUserNotificationDefaultButtonTitleKey: #"asdf",
(__bridge NSString*)kCFUserNotificationAlternateButtonTitleKey: #"asdf",
};
self.notification = CFUserNotificationCreate(NULL, 10, kCFUserNotificationPlainAlertLevel, &error, (__bridge CFDictionaryRef)keys);
self.runLoopSource = CFUserNotificationCreateRunLoopSource(NULL, self.notification, NotificationCallback, 0);
CFRunLoopAddSource(CFRunLoopGetMain(), self.runLoopSource, kCFRunLoopCommonModes);
return YES;
}
void NotificationCallback(CFUserNotificationRef userNotification, CFOptionFlags responseFlags) {
NSLog(#"got response: %lu", responseFlags);
}
Your question missed the most important part regarding the "Uber Partner" app that would make things a lot clearer. The "Uber Partner" app is an Enterprise app and is not restricted to the Appstore guide lines.
it didn't got any special permissions like other answers suggested.
It is possible to display an alert view using SBAlertItem regardless of Sound \ Notification settings but if your end goal is to make it to the appstore, unfortunately, your app will be rejected for using private API.
I have a application running in background and I need to know if device is sleeping in order to start a sincronisation process, but I didn't find information about this.
Does anyone know if it is posible and how do it?
Thanks.
You cannot know if the device is asleep because you have no control over the OS.
You can, otherwise, use the App Delegate method:
- (void)applicationWillResignActive:(UIApplication *)application
{
//your code goes here
}
if you want to wait till your app goes to background
I believe you can't do this using public API. The only thing which you can check whether your application is active or in background (using AppDelegate callbacks). And as Luke pointed out in comments, checking whether device "falls asleep" isn't iOS best design practice.
There are some private API's to do what you want, you can look at following questions:
Is there a way to check if the iOS device is locked/unlocked?
Detect screen on/off from iOS service
However, you should be aware that your app won't be accepted in AppStore in such case.
Is it possible that my iOS app can auto-restart, each and every time the user accesses their home screen? This is for a jailbroken device -- the app is not destined for the App store.
In general, how can I make my app restart given specific user actions outside the app?
Accelerometer
If all you want to do is make your app run when you encounter certain accelerometer conditions, you can use Activator for that. Activator is a great app, by Ryan Petrich, available on Cydia for free. It lets you configure your device to run any app (or toggle) whenever a certain user action is taken. That could be a home button press, power/lock button press, or accelerometer shake.
If a basic shake isn't what you want, or you are building an app to give to many users, and don't want them to have to setup Activator themselves, then you probably need to write some code yourself.
For example, you could write a Launch Daemon, in addition to your main UI app, and have the launch daemon monitor the accelerometer.
When you detect the specific kind of motion you're interested in, you can launch your UI app with the open command. If this is just for your own use, just download the open package from Cydia. If this is for release to others, make sure your app depends on open to ensure that it's installed. For example, if packaging in a Debian .deb package, the DEBIAN/control file might have this:
Depends: open
to make sure users installing your app will also automatically get open, which your app needs.
Unlock
Your other problem concerns launching the app when the user unlocks the phone. Again, I would use your Launch Daemon to listen for this condition. On iOS 5, I see this notification when I unlock the phone:
Notification intercepted: com.apple.springboard.lockstate
(I detected this by running the notificationWatcher utility from the command line, while SSH'd into my phone. NotificationWatcher is also available from Cydia, as part of Erica Sadun's Erica Utilities package)
So, I would have your launch daemon register for Darwin notifications for "com.apple.springboard.lockstate". Something like this:
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), //center
self, // observer: can be NULL if callback doesn't need self
onLockStateChanged, // callback
CFSTR("com.apple.springboard.lockstate"), // name
NULL, // object
CFNotificationSuspensionBehaviorDeliverImmediately);
where the callback function is here:
static void onLockStateChanged(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) {
// if you need access to member data (ivars):
MyLaunchDaemon* this = (MyLaunchDaemon*)observer;
//if (userInfo != nil) {
// CFShow(userInfo);
//}
NSDictionary* info = (NSDictionary*)userInfo;
// I'm not sure if the userInfo object has any useful
// description for the lock state event
if (/* unlocked */) {
// force app to open, or resume from the background
system("/usr/bin/open com.mycompany.MyAppName");
}
}
I see this same notification when the screen is locked, or unlocked, so you may need to have the launch daemon keep track of the locked/unlocked state, or inspect the userInfo object to see if that tells you whether this is a lock or unlock event. I'm sure there's other ways, too.
Update: if you want help sorting out whether the notification occurs when the screen is locked or unlocked, you can see my Update 2 in this other SO answer. notify_get_state() can be used to determine whether the event is an on, or off, event.
Set the value of UIApplicationExitsOnSuspend to YES in your app's Info.plist file.
UIApplicationExitsOnSuspend (Boolean - iOS) specifies that the app
should be terminated rather than moved to the background when it is
quit. Apps linked against iOS SDK 4.0 or later can include this key
and set its value to YES to prevent being automatically opted-in to
background execution and app suspension.