Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TIMOB-25322] (6_3_X) iOS: Improve new Ti.Geolocation authentication-flow #9538

Merged
merged 6 commits into from
Oct 27, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 8 additions & 4 deletions apidoc/Titanium/Geolocation/Geolocation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ description: |
"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>.
and <Ti.Geolocation.requestLocationPermissions>. Also note that you also need to include the `NSLocationWhenInUseUsageDescription` key
in *any* case when targeting iOS 11 and later. Descriptive error-logs will be thrown if required permissions are missing.

#### Configurating Location Updates on Android

Expand Down Expand Up @@ -262,9 +263,12 @@ methods:
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.
will still ask the user to select between "When in Use", "Always", and "Don't Allow" (primary action) 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.

Finally, iOS 11 and later will require you to *always* include the `NSLocationWhenInUseUsageDescription` permission and Titanium will throw a
descriptive error-log if any required keys are missing.

More infos:
* [Available Info.plist keys](https://developer.apple.com/library/prerelease/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html)
Expand Down
5 changes: 3 additions & 2 deletions iphone/Classes/GeolocationModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ NSString *const kTiGeolocationUsageDescriptionAlwaysAndWhenInUse = @"NSLocationA
BOOL trackingLocation;
BOOL trackSignificantLocationChange;
BOOL allowsBackgroundLocationUpdates;
KrollCallback *authorizationCallback;

KrollCallback *authorizationCallback;
CLAuthorizationStatus requestedAuthorizationStatus;

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_6_0
CLActivityType activityType;
BOOL pauseLocationUpdateAutomatically;
Expand Down
97 changes: 65 additions & 32 deletions iphone/Classes/GeolocationModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -312,18 +312,22 @@ -(CLLocationManager*)locationManager
"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);
"})\nPicking the hightest permission by default.");
if ([TiUtils isIOS11OrGreater] && ![[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionWhenInUse]) {
NSLog(@"[WARN] Apps targeting iOS 11 and later always have to include the \"%@\" key in the tiapp.xml <plist> section in order to use any geolocation-services. This is a constraint Apple introduced to improve user-privacy by suggesting developers to incrementally upgrade the location permissions from \"When in Use\" to \"Always\" only if necessary. You can specify the new iOS 11+ plist-key \"%@\" which is used while upgrading from \"When in Use\" to \"Always\". Use the the Ti.Geolocation.requestLocationPermissions method, which should be called before using any Ti.Geolocation related API. Please verify location permissions and call this method again.", kTiGeolocationUsageDescriptionWhenInUse, kTiGeolocationUsageDescriptionAlwaysAndWhenInUse);
}

if ([[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlways]) {
if ([GeolocationModule hasAlwaysPermissionKeys]) {
[locationManager requestAlwaysAuthorization];
} else if ([[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionWhenInUse] ||
[[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlwaysAndWhenInUse]) {
} else if ([GeolocationModule hasWhenInUsePermissionKeys]) {
[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);
if ([TiUtils isIOS11OrGreater]) {
NSLog(@"[ERROR] If you are only using geolocation-services *when in use*, you only need to specify the %@ key in your tiapp.xml", kTiGeolocationUsageDescriptionWhenInUse);
NSLog(@"[ERROR] If you are *always* using geolocation-servcies, you need to specify the following three keys in your tiapp.xml:\n * %@\n * %@\n * %@", kTiGeolocationUsageDescriptionWhenInUse, kTiGeolocationUsageDescriptionAlways, kTiGeolocationUsageDescriptionAlwaysAndWhenInUse);
} else {
NSLog(@"[ERROR] The keys %@ or %@ are not defined in your tiapp.xml. Starting with iOS 8 this is required.", kTiGeolocationUsageDescriptionAlways, kTiGeolocationUsageDescriptionWhenInUse);
}
}
}

Expand Down Expand Up @@ -847,9 +851,9 @@ -(void)requestLocationPermissions:(id)args
authorizationCallback = [[args objectAtIndex:1] retain];
}

CLAuthorizationStatus requested = [TiUtils intValue: value];
requestedAuthorizationStatus = [TiUtils intValue: value];
CLAuthorizationStatus currentPermissionLevel = [CLLocationManager authorizationStatus];
BOOL permissionsGranted = currentPermissionLevel == requested;
BOOL permissionsGranted = currentPermissionLevel == requestedAuthorizationStatus;

// For iOS < 11, already granted permissions will return with success immediately
if (permissionsGranted) {
Expand All @@ -863,57 +867,73 @@ -(void)requestLocationPermissions:(id)args

NSString *errorMessage = nil;

if(requested == kCLAuthorizationStatusAuthorizedWhenInUse) {
if ([[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionWhenInUse] ||
[[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlwaysAndWhenInUse]) {
if(requestedAuthorizationStatus == kCLAuthorizationStatusAuthorizedWhenInUse) {
if ([GeolocationModule hasWhenInUsePermissionKeys]) {
if ((currentPermissionLevel == kCLAuthorizationStatusAuthorizedAlways) ||
(currentPermissionLevel == kCLAuthorizationStatusAuthorized)) {
errorMessage = @"Cannot change already granted permission from AUTHORIZATION_ALWAYS to AUTHORIZATION_WHEN_IN_USE";
errorMessage = @"Cannot change already granted permission from AUTHORIZATION_ALWAYS to the lower permission-level AUTHORIZATION_WHEN_IN_USE";
} else {
TiThreadPerformOnMainThread(^{
[[self locationPermissionManager] requestWhenInUseAuthorization];
}, NO);
}
} else {
errorMessage = [NSString stringWithFormat:@"The %@ key (or %@ on iOS 11+) must be defined in your tiapp.xml in order to request this permission", kTiGeolocationUsageDescriptionWhenInUse, kTiGeolocationUsageDescriptionAlwaysAndWhenInUse];
errorMessage = [[NSString alloc] initWithFormat:@"The %@ key must be defined in your tiapp.xml in order to request this permission",
kTiGeolocationUsageDescriptionWhenInUse];
}
}
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])) {
if (requestedAuthorizationStatus == kCLAuthorizationStatusAuthorizedAlways) {
if ([GeolocationModule hasAlwaysPermissionKeys]) {
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];
errorMessage = [[NSString alloc] initWithFormat:
@"The %@, %@ and %@ key must be defined in your tiapp.xml in order to request this permission.",
kTiGeolocationUsageDescriptionAlwaysAndWhenInUse,
kTiGeolocationUsageDescriptionAlways,
kTiGeolocationUsageDescriptionAlwaysAndWhenInUse];
} else {
errorMessage = [NSString stringWithFormat:@"The %@ key must be defined in your tiapp.xml in order to request this permission.",
kTiGeolocationUsageDescriptionAlways];
errorMessage = [[NSString alloc] initWithFormat:
@"The %@ key must be defined in your tiapp.xml in order to request this permission.",
kTiGeolocationUsageDescriptionAlways];
}
}

if (errorMessage != nil ) {
NSLog(@"[ERROR] %@", errorMessage);
[self executeAndReleaseCallbackWithCode:(errorMessage == nil) ? 0 : 1 andMessage:errorMessage];
RELEASE_TO_NIL(errorMessage);
RELEASE_TO_NIL(errorMessage);
}
}

#pragma mark Internal

+ (BOOL)hasAlwaysPermissionKeys
{
if (![TiUtils isIOS11OrGreater]) {
return [[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlways];
}

return [[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionWhenInUse] &&
[[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlways] &&
[[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionAlwaysAndWhenInUse];
}

+ (BOOL)hasWhenInUsePermissionKeys
{
return [[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationUsageDescriptionWhenInUse];
}
-(void)executeAndReleaseCallbackWithCode:(NSInteger)code andMessage:(NSString*)message
{
if(authorizationCallback == nil) {
return;
}

NSMutableDictionary * propertiesDict = [TiUtils dictionaryWithCode:code message:message];
NSArray * invocationArray = [[NSArray alloc] initWithObjects:&propertiesDict count:1];
[authorizationCallback call:invocationArray thisObject:self];

[invocationArray release];
RELEASE_TO_NIL(message);
RELEASE_TO_NIL(authorizationCallback);
}

Expand Down Expand Up @@ -1020,15 +1040,18 @@ -(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. Include the %@ or %@ / %@ key in your Info.plist instead", kTiGeolocationUsageDescriptionAlways, kTiGeolocationUsageDescriptionWhenInUse, kTiGeolocationUsageDescriptionAlwaysAndWhenInUse);

if ([TiUtils isIOS11OrGreater]) {
DebugLog(@"[WARN] The Ti.Geolocation.purpose property is deprecated. Include the %@ or %@ and (!) %@ (iOS 11+) key in your Info.plist instead", kTiGeolocationUsageDescriptionWhenInUse, kTiGeolocationUsageDescriptionAlways, kTiGeolocationUsageDescriptionAlwaysAndWhenInUse);
} else {
DebugLog(@"[WARN] The Ti.Geolocation.purpose property is deprecated. Include the %@ or %@ key in your Info.plist instead", kTiGeolocationUsageDescriptionWhenInUse, kTiGeolocationUsageDescriptionAlways);
}

if (locationManager!=nil)
{
if (locationManager != nil) {
if ([locationManager respondsToSelector:@selector(setPurpose:)]) {
[locationManager performSelector:@selector(setPurpose:) withObject:purpose];
}
}

}
}

#pragma mark Geolacation Analytics
Expand Down Expand Up @@ -1078,10 +1101,11 @@ - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatu
if ([self _hasListeners:@"authorization"]) {
[self fireEvent:@"authorization" withObject:event];
}


BOOL requestedStatusMatchesActualStatus = status == requestedAuthorizationStatus;

// The new callback for android parity used inside Ti.Geolocation.requestLocationPermissions()
if (authorizationCallback != nil && status != kCLAuthorizationStatusNotDetermined) {

int code = 0;
NSString* errorStr = nil;

Expand All @@ -1093,6 +1117,15 @@ - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatu
code = 1;
errorStr = @"The user denied access to use location services.";
}

// This is very important for iOS 11+ because even if the user did an incremental authorization before
// (by selecting "when in use", he/she will still get a dialog that includes the "when in use" option.
// In case that one is still selected then, the developer should know about that selection and the following
// statement allows that.
if (!requestedStatusMatchesActualStatus) {
Copy link

@jvandijk jvandijk Oct 20, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hansemannn this addition has zero effect, because when the authorization status remains unchanged and thus this whole method is not fired.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's for the initial "when in use" instead of "always" selection, which would fire but with success: true.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right

code = 1;
errorStr = @"The requested permissions do not match the selected permission (the user likely declined AUTHORIZATION_ALWAYS permissions) in iOS 11+";
}

TiThreadPerformOnMainThread(^{
NSMutableDictionary * propertiesDict = [TiUtils dictionaryWithCode:code message:errorStr];
Expand Down