Skip to content

Commit

Permalink
Merge pull request #9514 from hansemannn/TIMOB-25322-6_3_X
Browse files Browse the repository at this point in the history
[TIMOB-25322] (6_3_X) iOS: Geolocation should be able to handle iOS 11 permission upgrade
  • Loading branch information
ewieberappc committed Oct 16, 2017
2 parents 81ca136 + eed41e1 commit 2966578
Show file tree
Hide file tree
Showing 6 changed files with 104 additions and 78 deletions.
22 changes: 19 additions & 3 deletions apidoc/Titanium/Geolocation/Geolocation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ description: |
* [ACCURACY_THREE_KILOMETERS](Titanium.Geolocation.ACCURACY_THREE_KILOMETERS) (lowest
accuracy and power consumption).
* [ACCURACY_BEST_FOR_NAVIGATION](Titanium.Geolocation.ACCURACY_BEST_FOR_NAVIGATION)
(Available in iOS 6.0 and above)
Based on the accuracy you choose, iOS uses its own logic to select location providers
and filter location updates to provide location updates that meet your accuracy
Expand Down Expand Up @@ -79,6 +78,13 @@ description: |
</plist>
</ios>
</ti:app>
For iOS 11 and later, also add the [`NSLocationAlwaysAndWhenInUseUsageDescription`](https://developer.apple.com/documentation/corelocation/choosing_the_authorization_level_for_location_services/request_always_authorization)
when planning to request the "Always" permission. Using the above key, you are also able to upgrade your permissions from
"When in Use" to "Always", which is the recommended way for managing location permissions in iOS 11 and later.
Please also remember to request your desired location-permissions before using any geolocation-related API in
order to receive the best usability and permission-control during the app-lifecycle using <Titanium.Geolocation.hasLocationPermissions>
and <Ti.Geolocation.requestLocationPermissions>.
#### Configurating Location Updates on Android
Expand Down Expand Up @@ -251,8 +257,18 @@ methods:
In iOS 8, Apple introduced the Info.plist keys `NSLocationWhenInUseUsageDescription` and `NSLocationAlwaysUsageDescription`
that are used to display an own description while requesting location permissions. In addition to this method, you need to
include one of these keys (based on how your location updates should behave) or the application will crash if your app does not.
Check the [Apple docs](https://developer.apple.com/library/prerelease/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html) for more information.
include one of these keys (based on how your location updates should behave) or the application will crash if your app does not.
In iOS 11, Apple introduced another Info.plist key `NSLocationAlwaysAndWhenInUseUsageDescription` that allows developers to upgrade
their permissions from "When in Use" to "Always". In order to get the best usability for iOS 11 and later, request "When in Use" first
and upgrade your location-permissions by requesting "Always" permissions later if required. If this permission-flow is not used, iOS
will still ask the user to select between "When in Use" and "Always" on iOS 11 when asking for "Always" permissions, which can lead
to a higher frequence of denied permissions, so be careful requesting location permissions. The iOS 11 related upgrade-flow is available
in Titanium SDK 6.3.0 and later.
More infos:
* [Available Info.plist keys](https://developer.apple.com/library/prerelease/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html)
* [Request Always Authorization](https://developer.apple.com/documentation/corelocation/choosing_the_authorization_level_for_location_services/request_always_authorization)
parameters:
- name: authorizationType
summary: Types of geolocation's authorizations. This is an iOS only parameter and is ignored on Android.
Expand Down
7 changes: 6 additions & 1 deletion apidoc/Titanium/Media/Media.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,16 @@ methods:
Media can be passed as either a `Blob` object or a `File` object. If the `media` argument
is not one of these types, an error is generated.
In iOS 11 and later, you also need to specify the Info.plist key `NSPhotoLibraryAddUsageDescription` if you are
intending to write to the photo-gallery. It will be ignored on iOS < 11.
Note that when passing a file (or a blob created from a file), the file name must
have the appropriate extension for the data--for example, `image.jpg` or `video1.mov` work,
but `video1.tmp` does not. Currently, the `.mp4` extension is not supported, but MP4
files may be imported by saving them with the `.mov` extension.
On Android this method *only supports saving images* to the device gallery prior to Titanium SDK 6.1.0.
platforms: [iphone, ipad, android]
parameters:
- name: media
Expand Down Expand Up @@ -286,6 +288,9 @@ methods:
photo gallery and the application will crash if your app does not include the key. Check the [Apple docs](https://developer.apple.com/library/prerelease/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html)
for more information. If the user has, the callback will return the earlier decision. To change the permission the
user needs to change it in the device settings.
In iOS 11 and later, you also need to specify the Info.plist key `NSPhotoLibraryAddUsageDescription` if you are
intending to write to the photo-gallery. It will be ignored on iOS < 11.
This API is available in iOS 8 and later. Earlier versions of iOS, as well as apps that do not include this method,
will present the system-dialog while the dialog is opened with <Titanium.Media.openPhotoGallery>.
Expand Down
9 changes: 6 additions & 3 deletions iphone/Classes/GeolocationModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
#import "TiModule.h"
#import "APSHTTPClient.h"

#ifdef USE_TI_GEOLOCATION

#import "TiModule.h"
#import "APSHTTPClient.h"
#import <CoreLocation/CoreLocation.h>

NSString *const kTiGeolocationUsageDescriptionWhenInUse = @"NSLocationWhenInUseUsageDescription";
NSString *const kTiGeolocationUsageDescriptionAlways = @"NSLocationAlwaysUsageDescription";
NSString *const kTiGeolocationUsageDescriptionAlwaysAndWhenInUse = @"NSLocationAlwaysAndWhenInUseUsageDescription";

@interface GeolocationModule : TiModule<CLLocationManagerDelegate> {
CLLocationManager *locationManager;
CLLocationManager *tempManager; // Our 'fakey' manager for handling certain <=3.2 requests
CLLocationManager *locationPermissionManager; // used for just permissions requests
CLLocationManager *iOS7PermissionManager; // specific to iOS7 to maintain parity with iOS8 permissions behavior.

CLLocationAccuracy accuracy;
CLLocationDistance distance;
Expand Down
106 changes: 45 additions & 61 deletions iphone/Classes/GeolocationModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ -(void)_destroy
[self shutdownLocationManager];
RELEASE_TO_NIL(tempManager);
RELEASE_TO_NIL(locationPermissionManager);
RELEASE_TO_NIL(iOS7PermissionManager);
RELEASE_TO_NIL(singleHeading);
RELEASE_TO_NIL(singleLocation);
RELEASE_TO_NIL(purpose);
Expand Down Expand Up @@ -304,13 +303,29 @@ -(CLLocationManager*)locationManager
}
locationManager.headingFilter = heading;

if ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"]) {
[locationManager requestAlwaysAuthorization];
} else if ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"]) {
[locationManager requestWhenInUseAuthorization];
} else {
NSLog(@"[ERROR] The keys NSLocationAlwaysUsageDescription or NSLocationWhenInUseUsageDescription are not defined in your tiapp.xml. Starting with iOS8 this is required.");
}
if ([CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedAlways &&
[CLLocationManager authorizationStatus] != kCLAuthorizationStatusAuthorizedWhenInUse) {
NSLog(@"[WARN] Trying to use location services without requesting location permissions. Use either:\n\n"
"Ti.Geolocation.requestLocationPermissions(Ti.Geolocation.AUTHORIZATION_ALWAYS, function(e) {\n"
"\t// Handle authorization via e.success\n"
"})\n\n"
"or\n\n"
"Ti.Geolocation.requestLocationPermissions(Ti.Geolocation.AUTHORIZATION_WHEN_IN_USE, function(e) {\n"
"\t// Handle authorization via e.success\n"
"})\n");
if ([TiUtils isIOS11OrGreater] && ![[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlwaysAndWhenInUse]) {
NSLog(@"[WARN] Apps targeting iOS 11 and later have the option to pass the \"%@\" key to the tiapp.xml <plist> section, allowing them to incrementally upgrade the location permissions from \"When in Use\" to \"Always\". This is only possible when using the Ti.Geolocation.requestLocationPermissions method, which should be called before using any Ti.Geolocation related API. Please verify location permissions before and call this method afterwards. Falling back to the old behavior ...", kTiGeolocationUsageDescriptionAlwaysAndWhenInUse);
}

if ([[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlways]) {
[locationManager requestAlwaysAuthorization];
} else if ([[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionWhenInUse] ||
[[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlwaysAndWhenInUse]) {
[locationManager requestWhenInUseAuthorization];
} else {
NSLog(@"[ERROR] The keys %@ or %@ / %@ are not defined in your tiapp.xml. Starting with iOS 8 this is required.", kTiGeolocationUsageDescriptionAlways, kTiGeolocationUsageDescriptionWhenInUse, kTiGeolocationUsageDescriptionAlwaysAndWhenInUse);
}
}

//This is set to NO by default for > iOS9.
if ([TiUtils isIOS9OrGreater]) {
Expand Down Expand Up @@ -820,29 +835,6 @@ -(void)requestAuthorization:(id)value
[self requestLocationPermissions:@[value, [NSNull null]]];
}

- (void)requestLocationPermissioniOS7:(id)args {
// Store the authorization callback for later usage
if([args count] == 2) {
RELEASE_TO_NIL(authorizationCallback);
ENSURE_TYPE([args objectAtIndex:1], KrollCallback);
authorizationCallback = [[args objectAtIndex:1] retain];
}

if (!iOS7PermissionManager) {
iOS7PermissionManager = [CLLocationManager new];
iOS7PermissionManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers;
iOS7PermissionManager.delegate = self;
}

if ([CLLocationManager authorizationStatus] == kCLAuthorizationStatusNotDetermined) {
// iOS7 shows permission alert only when location update is requested. Here we trick iOS7 to show
// permission alert so that our API is in parity with iOS8+ behavior.
[iOS7PermissionManager startUpdatingLocation];
} else {
[self locationManager:iOS7PermissionManager didChangeAuthorizationStatus:[CLLocationManager authorizationStatus]];
}
}

-(void)requestLocationPermissions:(id)args
{
id value = [args objectAtIndex:0];
Expand All @@ -857,8 +849,9 @@ -(void)requestLocationPermissions:(id)args

CLAuthorizationStatus requested = [TiUtils intValue: value];
CLAuthorizationStatus currentPermissionLevel = [CLLocationManager authorizationStatus];
BOOL permissionsGranted = (currentPermissionLevel == kCLAuthorizationStatusAuthorizedAlways) || (currentPermissionLevel == kCLAuthorizationStatusAuthorizedWhenInUse);

BOOL permissionsGranted = currentPermissionLevel == requested;

// For iOS < 11, already granted permissions will return with success immediately
if (permissionsGranted) {
[self executeAndReleaseCallbackWithCode:0 andMessage:nil];
return;
Expand All @@ -871,7 +864,8 @@ -(void)requestLocationPermissions:(id)args
NSString *errorMessage = nil;

if(requested == kCLAuthorizationStatusAuthorizedWhenInUse) {
if ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"]) {
if ([[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionWhenInUse] ||
[[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlwaysAndWhenInUse]) {
if ((currentPermissionLevel == kCLAuthorizationStatusAuthorizedAlways) ||
(currentPermissionLevel == kCLAuthorizationStatusAuthorized)) {
errorMessage = @"Cannot change already granted permission from AUTHORIZATION_ALWAYS to AUTHORIZATION_WHEN_IN_USE";
Expand All @@ -881,20 +875,21 @@ -(void)requestLocationPermissions:(id)args
}, NO);
}
} else {
errorMessage = @"The NSLocationWhenInUseUsageDescription key must be defined in your tiapp.xml in order to request this permission";
errorMessage = [NSString stringWithFormat:@"The %@ key (or %@ on iOS 11+) must be defined in your tiapp.xml in order to request this permission", kTiGeolocationUsageDescriptionWhenInUse, kTiGeolocationUsageDescriptionAlwaysAndWhenInUse];
}
}
if ((requested == kCLAuthorizationStatusAuthorizedAlways) || (requested == kCLAuthorizationStatusAuthorized)) {
if ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"]) {
if (currentPermissionLevel == kCLAuthorizationStatusAuthorizedWhenInUse) {
errorMessage = @"Cannot change already granted permission from AUTHORIZATION_WHEN_IN_USE to AUTHORIZATION_ALWAYS";
} else {
TiThreadPerformOnMainThread(^{
[[self locationPermissionManager] requestAlwaysAuthorization];
}, NO);
}
if (requested == kCLAuthorizationStatusAuthorizedAlways) {
// If iOS 11, the user can only have "NSLocationAlwaysAndWhenInUseUsageDescription" to manage the location-upgrade process
if ([[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlways] || ([TiUtils isIOS11OrGreater] && [[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlwaysAndWhenInUse])) {
TiThreadPerformOnMainThread(^{
[[self locationPermissionManager] requestAlwaysAuthorization];
}, NO);
} else if ([TiUtils isIOS11OrGreater]) {
errorMessage = [NSString stringWithFormat:@"The %@ or %@ key must be defined in your tiapp.xml in order to request this permission.",
kTiGeolocationUsageDescriptionAlways, kTiGeolocationUsageDescriptionAlwaysAndWhenInUse];
} else {
errorMessage = @"The NSLocationAlwaysUsageDescription key must be defined in your tiapp.xml in order to request this permission.";
errorMessage = [NSString stringWithFormat:@"The %@ key must be defined in your tiapp.xml in order to request this permission.",
kTiGeolocationUsageDescriptionAlways];
}
}

Expand Down Expand Up @@ -1025,8 +1020,8 @@ -(void)setPurpose:(NSString *)reason
ENSURE_UI_THREAD(setPurpose,reason);
RELEASE_TO_NIL(purpose);
purpose = [reason retain];
DebugLog(@"[WARN] The Ti.Geolocation.purpose property is deprecated. On iOS6 and above include the NSLocationUsageDescription key in your Info.plist");
DebugLog(@"[WARN] The Ti.Geolocation.purpose property is deprecated. Include the %@ or %@ / %@ key in your Info.plist instead", kTiGeolocationUsageDescriptionAlways, kTiGeolocationUsageDescriptionWhenInUse, kTiGeolocationUsageDescriptionAlwaysAndWhenInUse);

if (locationManager!=nil)
{
if ([locationManager respondsToSelector:@selector(setPurpose:)]) {
Expand Down Expand Up @@ -1078,11 +1073,7 @@ - (void)locationManagerDidResumeLocationUpdates:(CLLocationManager *)manager
- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status {
NSDictionary *event = [NSDictionary dictionaryWithObjectsAndKeys:
NUMINT([CLLocationManager authorizationStatus]),@"authorizationStatus",nil];

if ([manager isEqual:iOS7PermissionManager] && (status != kCLAuthorizationStatusNotDetermined)) {
[manager stopUpdatingLocation];
}


// Still using this event for changes being made outside the app (e.g. disable all location services on the device).
if ([self _hasListeners:@"authorization"]) {
[self fireEvent:@"authorization" withObject:event];
Expand Down Expand Up @@ -1118,12 +1109,7 @@ - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatu
//Using new delegate instead of the old deprecated method - (void)locationManager:didUpdateToLocation:fromLocation:

-(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray *)locations {
if ([manager isEqual:iOS7PermissionManager]) {
// Used only to simulate permission alert. So ignore this update.
return;
}

NSDictionary *todict = [self locationDictionary:[locations lastObject]];
NSDictionary *todict = [self locationDictionary:[locations lastObject]];

//Must use dictionary because of singleshot.
NSMutableDictionary *event = [TiUtils dictionaryWithCode:0 message:nil];
Expand All @@ -1133,12 +1119,10 @@ -(void)locationManager:(CLLocationManager *)manager didUpdateLocations:(NSArray
[self fireEvent:@"location" withObject:event];
}

[self fireApplicationAnalyticsIfNeeded:locations];
[self fireApplicationAnalyticsIfNeeded:locations];
[self fireSingleShotLocationIfNeeded:event stopIfNeeded:YES];
}



- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
if (newLocation != nil) {
Expand Down

0 comments on commit 2966578

Please sign in to comment.