10000 feat(geolocation): correct handling of location permission on iOS (#163) · NativeScript/plugins@8da52ae · GitHub
[go: up one dir, main page]

Skip to content

Commit 8da52ae

Browse files
authored
feat(geolocation): correct handling of location permission on iOS (#163)
This was broken with the introduction of new location permission handling in iOS 13.4. Now it is possible to request "always" permission when "when use" permission is already granted. Also added function to monitor the location permission changes on iOS.
1 parent 0e718ee commit 8da52ae

File tree

4 files changed

+88
-5
lines changed

4 files changed

+88
-5
lines changed

packages/geolocation/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ geolocation.getCurrentLocation({ desiredAccuracy: Accuracy.high, maximumAge: 500
8686
| ------------------------------------------------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8787
| getCurrentLocation(options: Options) | Promise<Location> | Get current location applying the specified options (if any). Since version 5.0 of the plugin, it will use [requestLocation](https://developer.apple.com/documentation/corelocation/cllocationmanager/1620548-requestlocation?language=objc) API for devices using iOS 9.0+. In situation of poor or no GPS signal, but available Wi-Fi it will take 10 sec to return location. |
8888
| watchLocation(successCallback: successCallbackType, errorCallback: errorCallbackType, options: Options) | number | Monitor for location change. |
89+
| watchPermissionStatus(permissionCallback: permissionCallbackType, errorCallback: errorCallbackType)| number | Monitor for location permission change. Only on iOS!
8990
| clearWatch(watchId: number) | void | Stop monitoring for location change. Parameter expected is the watchId returned from `watchLocation`. |
9091
| enableLocationRequest(always?: boolean, openSettingsIfLocationHasBeenDenied?: boolean) | Promise\<void\> | Ask for permissions to use location services. On iOS when `always` is true, it opens a custom prompt message and the following keys are required: [NSLocationAlwaysAndWhenInUseUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nslocationalwaysandwheninuseusagedescription) (iOS 11.0+) OR [NSLocationAlwaysUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nslocationalwaysusagedescription?language=objc) (iOS 8.0-10.0) and [NSLocationWhenInUseUsageDescription](https://developer.apple.com/documentation/bundleresources/information_property_list/nslocationwheninuseusagedescription). Read more about [request always usage](https://developer.apple.com/documentation/corelocation/cllocationmanager/1620551-requestalwaysauthorization) On Android always (ACCESS_BACKGROUND_LOCATION) permission needs to be requeseted for SDK >= 29. Read about Android location permissions [here](https://developer.android.com/training/location/permissions). On Android SDK >= 29 the user gets prompted a system dialog with the option 'allow all the time' when the always option is selected [read more](https://developer.android.com/training/location/permissions#request-background-location) When `openSettingsIfLocationHasBeenDenied` is true and the permission has previously been denied then the settings app will open so the user can change the location services permission. |
9192
| isEnabled | Promise\<boolean\> | Resolves `true` or `false` based on the location services availability. |

packages/geolocation/index.android.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Enums, Application, UnhandledErrorEventData, AndroidApplication, Device, ApplicationSettings } from '@nativescript/core';
22
import { LocationBase, defaultGetLocationTimeout, fastestTimeUpdate, minTimeUpdate } from './common';
3-
import { Options, successCallbackType, errorCallbackType } from '.';
3+
import { Options, successCallbackType, errorCallbackType, permissionCallbackType } from '.';
44
import * as permissions from 'nativescript-permissions';
55
export * from './common';
66

@@ -197,6 +197,12 @@ export function watchLocation(successCallback: successCallbackType, errorCallbac
197197
return watchId;
198198
}
199199

200+
export function watchPermissionStatus(permissionCallback: permissionCallbackType, errorCallback: errorCallbackType) {
201+
const zonedErrorCallback = zonedCallback(errorCallback);
202+
zonedErrorCallback(new Error("watchPermissionStatus() is not available on Android"));
203+
return null;
204+
}
205+
200206
export function clearWatch(watchId: number): void {
201207
let listener = locationListeners[watchId];
202208
if (listener) {

packages/geolocation/index.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export interface Options {
115115
openSettingsIfLocationHasBeenDenied?: boolean;
116116
}
117117

118+
declare type permissionCallbackType = (permissionState: number) => void;
118119
declare type successCallbackType = (location: Location) => void;
119120
declare type errorCallbackType = (error: Error) => void;
120121

@@ -130,6 +131,15 @@ export function getCurrentLocation(options: Options): Promise<Location>;
130131
*/
131132
export function watchLocation(successCallback: successCallbackType, errorCallback: errorCallbackType, options: Options): number;
132133

134+
135+
/**
136+
* Monitor for location permission change. Only on iOS!
137+
* @param permissionCallback gets called on location permission state change
138+
* @param errorCallback gets called on error
139+
* @returns {number} the watch id
140+
*/
141+
export function watchPermissionStatus(permissionCallback: permissionCallbackType, errorCallback: errorCallbackType): number;
142+
133143
/**
134144
* Stop monitoring for location change. Parameter expected is the watchId returned from `watchLocation`.
135145
* @param watchId The watch id returned when watchLocation was called

packages/geolocation/index.ios.ts

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1-
import { Application, Enums, UnhandledErrorEventData, Device } from '@nativescript/core';
1+
import { Application, Enums, UnhandledErrorEventData, Device, ApplicationSettings } from '@nativescript/core';
22

33
import { LocationBase, defaultGetLocationTimeout, minRangeUpdate } from './common';
4-
import { Options, successCallbackType, errorCallbackType } from '.';
4+
import { Options, successCallbackType, errorCallbackType, permissionCallbackType } from '.';
55
export * from './common';
66

77
const locationManagers = {};
88
const locationListeners = {};
9+
const permissionListeners = {};
910
let watchId = 0;
1011
let attachedForErrorHandling = false;
1112

@@ -15,6 +16,7 @@ class LocationListenerImpl extends NSObject implements CLLocationManagerDelegate
1516

1617
public authorizeAlways: boolean;
1718
public id: number;
19+
private _onPermissionChange: permissionCallbackType;
1820
private _onLocation: successCallbackType;
1921
private _onError: errorCallbackType;
2022
private _resolve: () => void;
@@ -30,6 +32,16 @@ class LocationListenerImpl extends NSObject implements CLLocationManagerDelegate
3032
return listener;
3133
}
3234

35+
public static initWithPermissionError(permissionCallback: permissionCallbackType, error?: errorCallbackType): LocationListenerImpl {
36+
let listener = <LocationListenerImpl>LocationListenerImpl.new();
37+
watchId++;
38+
listener.id = watchId;
39+
listener._onPermissionChange = permissionCallback;
40+
listener._onError = error;
41+
42+
return listener;
43+
}
44+
3345
public static initWithPromiseCallbacks(resolve: () => void, reject: (error: Error) => void, authorizeAlways: boolean = false): LocationListenerImpl {
3446
let listener = <LocationListenerImpl>LocationListenerImpl.new();
3547
watchId++;
@@ -56,7 +68,28 @@ class LocationListenerImpl extends NSObject implements CLLocationManagerDelegate
5668
}
5769
}
5870

71+
public locationManagerDidChangeAuthorization(manager: CLLocationManager) {
72+
this._handleAuthorizationChange(getIOSLocationManagerStatus());
73+
}
74+
5975
public locationManagerDidChangeAuthorizationStatus(manager: CLLocationManager, status: CLAuthorizationStatus) {
76+
if (getVersionMaj() < 14) {
77+
this._handleAuthorizationChange(status);
78+
}
79+
}
80+
81+
private _handleAuthorizationChange(status: CLAuthorizationStatus) {
82+
83+
// the permisssion listener doesn't resolve
84+
if (this._onPermissionChange) {
85+
this._onPermissionChange(status);
86+
return;
87+
}
88+
89+
if (this.authorizeAlways) {
90+
ApplicationSettings.setBoolean('hasRequestedAlwaysAuthorization', true);
91+
}
92+
6093
switch (status) {
6194
case CLAuthorizationStatus.kCLAuthorizationStatusNotDetermined:
6295
break;
@@ -72,12 +105,17 @@ class LocationListenerImpl extends NSObject implements CLLocationManagerDelegate
72105
break;
73106

74107
case CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedAlways:
75-
case CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse:
76108
if (this._resolve) {
77109
LocationMonitor.stopLocationMonitoring(this.id);
78110
this._resolve();
79111
}
80112
break;
113+
case CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse:
114+
if (this._resolve && !this.authorizeAlways) {
115+
LocationMonitor.stopLocationMonitoring(this.id);
116+
this._resolve();
117+
}
118+
break;
81119

82120
default:
83121
break;
@@ -217,6 +255,26 @@ export function watchLocation(successCallback: successCallbackType, errorCallbac
217255
}
218256
}
219257

258+
export function watchPermissionStatus(permissionCallback: permissionCallbackType, errorCallback: errorCallbackType): number {
259+
let zonedPermissionCallback = (<any>global).zonedCallback(permissionCallback);
260+
let zonedErrorCallback = (<any>global).zonedCallback(errorCallback);
261+
let permListener = LocationListenerImpl.initWithPermissionError(zonedPermissionCallback, zonedErrorCallback);
262+
263+
try {
264+
let iosLocManager = new CLLocationManager();
265+
iosLocManager.delegate = permListener;
266+
267+
locationManagers[permListener.id] = iosLocManager;
268+
permissionListeners[permListener.id] = permListener;
269+
zonedPermissionCallback(iosLocManager.authorizationStatus);
270+
return permListener.id;
271+
} catch (e) {
272+
LocationMonitor.stopLocationMonitoring(permListener.id);
273+
zonedErrorCallback(e);
274+
return null;
275+
}
276+
}
277+
220278
export function clearWatch(_watchId: number): void {
221279
LocationMonitor.stopLocationMonitoring(_watchId);
222280
}
@@ -230,9 +288,11 @@ export function enableLocationRequest(always?: boolean, openSettingsIfLocationHa
230288
return;
231289
} else {
232290
const status = getIOSLocationManagerStatus();
233-
if (status === CLAuthorizationStatus.kCLAuthorizationStatusDenied && openSettingsIfLocationHasBeenDenied) {
291+
if ((status === CLAuthorizationStatus.kCLAuthorizationStatusDenied && openSettingsIfLocationHasBeenDenied) ||
292+
(!_systemDialogWillShow(always, status) && openSettingsIfLocationHasBeenDenied)) {
234293
// now open the Settings so the user can toggle the Location permission
235294
UIApplication.sharedApplication.openURL(NSURL.URLWithString(UIApplicationOpenSettingsURLString));
295+
reject();
236296
} else {
237297
let listener = LocationListenerImpl.initWithPromiseCallbacks(resolve, reject, always);
238298
try {
@@ -251,6 +311,11 @@ export function enableLocationRequest(always?: boolean, openSettingsIfLocationHa
251311
});
252312
}
253313

314+
function _systemDialogWillShow(always: boolean, status: CLAuthorizationStatus): boolean {
315+
// the system dialog for "always" permission will not show if we requested it previously and currently have "when use" permission
316+
return !(status === CLAuthorizationStatus.kCLAuthorizationStatusAuthorizedWhenInUse && always && ApplicationSettings.getBoolean('hasRequestedAlwaysAuthorization', false))
317+
}
318+
254319
function _isEnabled(always?: boolean): boolean {
255320
if (CLLocationManager.locationServicesEnabled()) {
256321
const status = getIOSLocationManagerStatus();
@@ -331,6 +396,7 @@ export class LocationMonitor {
331396
locationManagers[iosLocManagerId].delegate = null;
332397
delete locationManagers[iosLocManagerId];
333398
delete locationListeners[iosLocManagerId];
399+
delete permissionListeners[iosLocManagerId];
334400
}
335401
}
336402

0 commit comments

Comments
 (0)
0