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

feat(ios): expose new APIs to use location AccuracyAuthorization #11813

Merged
merged 3 commits into from
Sep 8, 2020
Merged
Show file tree
Hide file tree
Changes from all 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
117 changes: 117 additions & 0 deletions apidoc/Titanium/Geolocation/Geolocation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,63 @@ methods:
platforms: [iphone, ipad, android]
since: "5.1.0"

- name: requestTemporaryFullAccuracyAuthorization
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we have to create a new method? From a cross-platform standpoint, it might be nice to re-use our existing requestLocationPermissions() and hasLocationPermissions() methods and add the new "purpose" string key as a parameter... or even better add support for a settings dictionary to these methods with a new "purpose" field.

On Android, location permission can be granted temporarily by the end-user too. We can still leverage the hasLocationPermission() method to check if it's still granted. And the Ti.Location.accuracy property defines the accuracy of the permission we need to look for. On Android, the ACCESS_FINE_LOCATION sounds like the equivalent of the "accurate" permission on iOS (which means read from the GPS).
https://developer.android.com/preview/privacy/permissions#one-time

So, can we do something like this?

const permissionSettings = {
	authorization: Ti.Geolocation.AUTHORIZATION_ALWAYS,
	purpose: 'PurposeKey1',
};
if (Ti.Geolocation.hasLocationPermissions(permissionSettings)) {
	fetchLocationInfo();
} else {
	Ti.Geolocation.requestLocationPermissions(permissionSettings, (e) => {
		if (e.success) {
			fetchLocationInfo();
		}
	});
}

Question:
On iOS 14, is the "purpose" key required? Are people going to have location accessing problem if they don't update their location permission handling code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jquick-axway This new API is for requesting full accuracy if user requires it in app. And it get expires. It is not mandatory. e.g your app do not need full accurate location, you should simply need location access . let's say at some screen you need full accuracy to show use'r location, you have to go for it. See here for more details

I think changes in Android 11 is same as of 'allow once' permission given in iOS 13. It is location permission's life not accuracy. See here.

'purpose' key is only required if one is going to access location with full accuracy. It will not break existing implementation.

Copy link
Contributor

Choose a reason for hiding this comment

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

So my only beef with this is if the app developer sets Ti.Location.accuracy to ACCURACY_HIGH, then I think hasLocationPermissions() should return false if it has high accuracy has expired, because that's what the app desires. That's why I'm question the necessity of these new APIs... that and I think we're making this more complicated for app developers than it needs to be. A portable API that works between all iOS versions and Android would be the ideal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If user has granted location permission hasLocationPermissions() will be true. ACURRACY_HIGH (kCLLocationAccuracyBest) is desirable not guaranteed.
In iOS 14+ when app request for location permission there comes a popup (see attached Image1) which have a button mentioning 'Precise: On'. If user sets it off, app will not get location data with full accuracy. If at some point it is required to get 'full accuracy', with this new API app can ask for permission to get full accuracy in location data with a valid reason (see image2 ). And it will be temporary.
Image1- IMG_1632

image2 -
IMG_1634

summary: Requests the user's permission to temporarily use location services with full accuracy.
description: |
If your app doesn't have permission to access accurate location (see [locationAccuracyAuthorization](Ti.Geolocation.locationAccuracyAuthorization),
you can use this method to request temporary access to accurate location. This access will expire automatically,
but it won't expire while the user is still engaged with your app. So, for example, while your app is in
the foreground your app will retain the temporary access it was granted. Similarly, if your app starts a
Continuous Background Location session with the background location indicator enabled (see [showBackgroundLocationIndicator](Ti.Geolocation.showBackgroundLocationIndicator),
your access to accurate location will remain as long as the background location indicator remains enabled.
This allows your app to provide session-oriented experiences which require accurate location (e.g. fitness or navigation),
even if the user has decided not to grant your app persistent access to accurate location.

You need to add key `NSLocationTemporaryUsageDescriptionDictionary` in `tiapp.xml`. See following example -

``` xml
<ti:app>
<ios>
<plist>
<dict>
<key>NSLocationTemporaryUsageDescriptionDictionary</key>
<dict>
<key>PurposeKey1</key>
<string>
Specify the reason for accessing the user's location data with full accuracy.
This appears in the alert dialog when asking the user for permission to
access their location.
</string>
<key>PurposeKey2</key>
<string>
Specify the reason for accessing the user's location data with full accuracy.
This appears in the alert dialog when asking the user for permission to
access their location.
</string>
</dict>
</dict>
</plist>
</ios>
</ti:app>
```

When CoreLocation prepares the prompt for display, it will look at the NSLocationTemporaryUsageDescriptionDictionary key
in your tiapp.xml. The value should be a dictionary containing usage descriptions. The purposeKey you provide to
this method must correspond to an entry in that dictionary.
parameters:
- name: purposeKey
summary: |
A key in the NSLocationTemporaryUsageDescriptionDictionary dictionary of the app's tiapp.xml file.
The value for this key is an app-provided string that describes the reason for accessing
location data with full accuracy.
type: String
- name: callback
summary: Function to call upon user decision to grant authorization.
type: Callback<LocationAccuracyAuthorizationResponse>
platforms: [iphone, ipad]
osver: {ios: {min: "14.0"}}
since: "9.2.0"

- name: reverseGeocoder
summary: Tries to resolve a location to an address.
description: |
Expand Down Expand Up @@ -485,6 +542,23 @@ properties:
platforms: [android, iphone, ipad]
since: "2.0.0"

- name: ACCURACY_REDUCED
summary: |
The level of accuracy used when an app isn’t authorized for full accuracy location data.
description: |
Use with [accuracy](Titanium.Geolocation.accuracy).
The accuracy of location data is reduced in both space and time using approaches like
selecting a nearby point of interest and updating the location at most a few times per hour.
The approximate location preserves the user’s country, typically preserves the city,
and is usually within 1–20 kilometers of the actual location.
If your app is authorized to access location information with full accuracy, you can use
this constant to access location data as if the app didn’t have that authorization.
type: Number
permission: read-only
platforms: [iphone, ipad]
osver: {ios: {min: "14.0"}}
since: "9.2.0"

- name: AUTHORIZATION_AUTHORIZED
summary: |
A [locationServicesAuthorization](Titanium.Geolocation.locationServicesAuthorization) value
Expand Down Expand Up @@ -630,6 +704,22 @@ properties:
platforms: [iphone,ipad]
since: "3.1.0"

- name: ACCURACY_AUTHORIZATION_FULL
summary: The user authorized the app to access location data with full accuracy.
type: Number
permission: read-only
osver: {ios: {min: "14.0"}}
platforms: [iphone,ipad]
since: "9.2.0"

- name: ACCURACY_AUTHORIZATION_REDUCED
summary: The user authorized the app to access location data with reduced accuracy.
type: Number
permission: read-only
osver: {ios: {min: "14.0"}}
platforms: [iphone,ipad]
since: "9.2.0"

- name: accuracy
summary: Specifies the requested accuracy for location updates.
description: |
Expand Down Expand Up @@ -809,6 +899,21 @@ properties:
since: "3.1.0"
platforms: [iphone,ipad]

- name: locationAccuracyAuthorization
summary: A value that indicates the level of location accuracy the app has permission to use.
description: |
If the value of this property is <Titanium.Geolocation.ACCURACY_AUTHORIZATION_FULL>,
you can set any constant to [accuracy](Titanium.Geolocation.accuracy). If value is
<Titanium.Geolocation.ACCURACY_AUTHORIZATION_REDUCED> setting [accuracy](Titanium.Geolocation.accuracy)
to a value other than <Titanium.Geolocation.ACCURACY_REDUCED> has no effect on the location information.
type: Number
constants: [Titanium.Geolocation.ACCURACY_AUTHORIZATION_FULL, Titanium.Geolocation.ACCURACY_AUTHORIZATION_REDUCED]
default: <Titanium.Geolocation.ACCURACY_AUTHORIZATION_REDUCED>
osver: {ios: {min: "14.0"}}
since: "9.2.0"
platforms: [iphone,ipad]
permission: read-only

- name: pauseLocationUpdateAutomatically
summary: Indicates whether the location updates may be paused.
description: |
Expand Down Expand Up @@ -1258,3 +1363,15 @@ properties:
name: LocationAuthorizationResponse
summary: Argument passed to the callback when a request finishes successfully or erroneously.
extends: ErrorResponse

---
name: LocationAccuracyAuthorizationResponse
summary: Argument passed to the callback when a request finishes successfully or erroneously.
extends: ErrorResponse
properties:
- name: accuracyAuthorization
summary: The level of location accuracy the app has granted.
description: Will be undefined if `error` is `true`.
optional: true
type: Number
constants: [Titanium.Geolocation.ACCURACY_AUTHORIZATION_FULL, Titanium.Geolocation.ACCURACY_AUTHORIZATION_REDUCED]
15 changes: 15 additions & 0 deletions iphone/Classes/GeolocationModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
NSString *const kTiGeolocationUsageDescriptionWhenInUse = @"NSLocationWhenInUseUsageDescription";
NSString *const kTiGeolocationUsageDescriptionAlways = @"NSLocationAlwaysUsageDescription";
NSString *const kTiGeolocationUsageDescriptionAlwaysAndWhenInUse = @"NSLocationAlwaysAndWhenInUseUsageDescription";
NSString *const kTiGeolocationTemporaryUsageDescriptionDictionary = @"NSLocationTemporaryUsageDescriptionDictionary";

@protocol GeolocationExports <JSExport>

// accuracy constants
CONSTANT(NSNumber *, ACCURACY_BEST_FOR_NAVIGATION);
CONSTANT(NSNumber *, ACCURACY_HIGH);
CONSTANT(NSNumber *, ACCURACY_LOW);
CONSTANT(NSNumber *, ACCURACY_REDUCED);

// iOS-specific values, (deprecated on Android)
CONSTANT(NSNumber *, ACCURACY_BEST);
Expand All @@ -42,6 +44,10 @@ CONSTANT(NSNumber *, AUTHORIZATION_RESTRICTED);
CONSTANT(NSNumber *, AUTHORIZATION_UNKNOWN);
CONSTANT(NSNumber *, AUTHORIZATION_WHEN_IN_USE);

//Accuracy Authorization to use location
CONSTANT(NSNumber *, ACCURACY_AUTHORIZATION_FULL);
CONSTANT(NSNumber *, ACCURACY_AUTHORIZATION_REDUCED);

// Error codes
CONSTANT(NSNumber *, ERROR_DENIED);
CONSTANT(NSNumber *, ERROR_HEADING_FAILURE);
Expand All @@ -61,6 +67,9 @@ READONLY_PROPERTY(bool, hasCompass, HasCompass);
PROPERTY(CLLocationDegrees, headingFilter, HeadingFilter);
READONLY_PROPERTY(NSString *, lastGeolocation, LastGeolocation);
READONLY_PROPERTY(CLAuthorizationStatus, locationServicesAuthorization, LocationServicesAuthorization);
#if IS_SDK_IOS_14
READONLY_PROPERTY(CLAccuracyAuthorization, locationAccuracyAuthorization, AccuracyAuthorization);
#endif
READONLY_PROPERTY(bool, locationServicesEnabled, LocationServicesEnabled);
PROPERTY(bool, pauseLocationUpdateAutomatically, PauseLocationUpdateAutomatically);
PROPERTY(bool, showBackgroundLocationIndicator, ShowBackgroundLocationIndicator);
Expand All @@ -85,6 +94,12 @@ JSExportAs(reverseGeocoder,
: (double)longitude withCallback
: (JSValue *)callback);

#if IS_SDK_IOS_14
JSExportAs(requestTemporaryFullAccuracyAuthorization,
-(void)requestTemporaryFullAccuracyAuthorization
: (NSString *)purposeString withCallback
: (JSValue *)callback);
#endif
@end

@interface GeolocationModule : ObjcProxy <GeolocationExports, CLLocationManagerDelegate> {
Expand Down
42 changes: 41 additions & 1 deletion iphone/Classes/GeolocationModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -733,7 +733,13 @@ - (void)restart:(id)arg
MAKE_SYSTEM_PROP_DBL(ACCURACY_KILOMETER, kCLLocationAccuracyKilometer);
MAKE_SYSTEM_PROP_DBL(ACCURACY_THREE_KILOMETERS, kCLLocationAccuracyThreeKilometers);
MAKE_SYSTEM_PROP_DBL(ACCURACY_LOW, kCLLocationAccuracyThreeKilometers);
MAKE_SYSTEM_PROP(ACCURACY_BEST_FOR_NAVIGATION, kCLLocationAccuracyBestForNavigation); //Since 2.1.3
MAKE_SYSTEM_PROP_DBL(ACCURACY_BEST_FOR_NAVIGATION, kCLLocationAccuracyBestForNavigation);
#if IS_SDK_IOS_14
MAKE_SYSTEM_PROP_DBL(ACCURACY_REDUCED, kCLLocationAccuracyReduced);

MAKE_SYSTEM_PROP(ACCURACY_AUTHORIZATION_FULL, CLAccuracyAuthorizationFullAccuracy);
MAKE_SYSTEM_PROP(ACCURACY_AUTHORIZATION_REDUCED, CLAccuracyAuthorizationReducedAccuracy);
#endif

MAKE_SYSTEM_PROP(AUTHORIZATION_UNKNOWN, kCLAuthorizationStatusNotDetermined);
MAKE_SYSTEM_PROP(AUTHORIZATION_AUTHORIZED, kCLAuthorizationStatusAuthorizedAlways);
Expand Down Expand Up @@ -847,6 +853,40 @@ - (void)requestLocationPermissions:(CLAuthorizationStatus)authorizationType with
}
}

#if IS_SDK_IOS_14
- (void)requestTemporaryFullAccuracyAuthorization:(NSString *)purposeKey withCallback:(JSValue *)callback
{
if (![TiUtils isIOSVersionOrGreater:@"14.0"]) {
NSMutableDictionary *propertiesDict = [TiUtils dictionaryWithCode:1 message:@"Supported on iOS 14+"];
[callback callWithArguments:@[ propertiesDict ]];
return;
}
NSDictionary *descriptionDict = [[NSBundle mainBundle] objectForInfoDictionaryKey:kTiGeolocationTemporaryUsageDescriptionDictionary];
if (!descriptionDict || ![descriptionDict valueForKey:purposeKey]) {
DebugLog(@"[WARN] Add %@ key with purpose key %@ in info.plist", kTiGeolocationTemporaryUsageDescriptionDictionary, purposeKey);
}
[[self locationPermissionManager] requestTemporaryFullAccuracyAuthorizationWithPurposeKey:purposeKey
completion:^(NSError *_Nullable error) {
NSMutableDictionary *propertiesDict = [TiUtils dictionaryWithCode:0 message:nil];
if (error != nil) {
propertiesDict = [TiUtils dictionaryWithCode:1 message:error.description];
} else {
propertiesDict[@"accuracyAuthorization"] = @([[self locationPermissionManager] accuracyAuthorization]);
}
[callback callWithArguments:@[ propertiesDict ]];
}];
}

- (CLAccuracyAuthorization)locationAccuracyAuthorization
{
if (![TiUtils isIOSVersionOrGreater:@"14.0"]) {
DebugLog(@"[ERROR] This property is available on iOS 14 and above.");
return -1;
}
return [[self locationPermissionManager] accuracyAuthorization];
}
#endif

READWRITE_IMPL(bool, allowsBackgroundLocationUpdates, AllowsBackgroundLocationUpdates);
GETTER_IMPL(NSString *, lastGeolocation, LastGeolocation);
READWRITE_IMPL(bool, showCalibration, ShowCalibration);
Expand Down
47 changes: 47 additions & 0 deletions tests/Resources/ti.geolocation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Licensed under the terms of the Apache Public License
* Please see the LICENSE included with this distribution for details.
*/
/* globals OS_VERSION_MAJOR */
/* eslint-env mocha */
/* eslint no-unused-expressions: "off" */
'use strict';
Expand All @@ -13,6 +14,7 @@ var should = require('./utilities/assertions');
// Skip on Windows 10 Mobile device family due to prompt,
// however we might be able to run some tests?
describe.windowsBroken('Titanium.Geolocation', function () {

it('.apiName', function () {
should(Ti.Geolocation).have.readOnlyProperty('apiName').which.is.a.String();
should(Ti.Geolocation.apiName).be.eql('Ti.Geolocation');
Expand Down Expand Up @@ -244,7 +246,52 @@ describe.windowsBroken('Titanium.Geolocation', function () {
should(Ti.Geolocation.trackSignificantLocationChange).be.true();
});

it.ios('.ACCURACY_REDUCED', function () {
if (OS_VERSION_MAJOR >= 14) {
Copy link
Contributor

Choose a reason for hiding this comment

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

So we define these special filters like it.ios, where it skips on other platforms. It may make sense to expand to add ones like it.ios14Plus because technically we'd want to skip on older versions, not have a no-op test that always passes. (Alternatively, once we upgrade from ti-mocha to the real mocha package, I think you can explicitly call this.skip() inside a test to mark it at runtime)

should(Ti.Geolocation).have.constant('ACCURACY_REDUCED').which.is.a.Number();
}
});

it.ios('.ACCURACY_AUTHORIZATION_FULL', function () {
if (OS_VERSION_MAJOR >= 14) {
should(Ti.Geolocation).have.constant('ACCURACY_AUTHORIZATION_FULL').which.is.a.Number();
}
});

it.ios('.ACCURACY_AUTHORIZATION_REDUCED', function () {
if (OS_VERSION_MAJOR >= 14) {
should(Ti.Geolocation).have.constant('ACCURACY_AUTHORIZATION_REDUCED').which.is.a.Number();
}
});

it.ios('.locationAccuracyAuthorization', function () {
vijaysingh-axway marked this conversation as resolved.
Show resolved Hide resolved
if (OS_VERSION_MAJOR >= 14) {
should(Ti.Geolocation).have.a.property('locationAccuracyAuthorization').which.is.a.Number();
}
});

// Methods
it.ios('#requestTemporaryFullAccuracyAuthorization()', function (finish) {
this.timeout(6e4); // 60 sec
if (OS_VERSION_MAJOR < 14) {
return finish();
}

should(Ti.Geolocation.requestTemporaryFullAccuracyAuthorization).be.a.Function();
Ti.Geolocation.requestTemporaryFullAccuracyAuthorization('purposekey', function (e) {
try {
// It will always give error because 'purposekey' is not in tiapp.xml.
should(e).have.property('success').which.is.a.Boolean();
should(e.success).be.false();
should(e).have.property('code').which.is.a.Number();
should(e.code).be.eql(1);
should(e).have.property('error').which.is.a.String();
finish();
Copy link
Contributor

Choose a reason for hiding this comment

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

typically I'd move this outside the try/catch block, because if an after test hook fails this could cause the catch block to fire and call finish again.

} catch (err) {
return finish(err);
}
});
});

it('#forwardGeocoder()', function (finish) {
this.timeout(6e4); // 60 sec
Expand Down