8000 feat(geolocation): android background permission handling improvement… · NativeScript/plugins@00be346 · GitHub
[go: up one dir, main page]

Skip to content

Commit 00be346

Browse files
authored
feat(geolocation): android background permission handling improvements (#149)
* Handle background location permission request on Android which was introduced with SDK v29. Added 'always' and 'openSettingsIfLocationHasBeenDenied' also to Android implementation, so that API is now the same as on iOS. * Proper handling for 'one time' permissions on Android 11. Updated Readme
1 parent 8625acc commit 00be346

File tree

6 files changed

+244
-184
lines changed

6 files changed

+244
-184
lines changed

packages/geolocation/README.md

Lines changed: 49 additions & 42 deletions
Large diffs are not rendered by default.

packages/geolocation/index.android.ts

Lines changed: 82 additions & 29 deletions
F438
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Enums, Application, UnhandledErrorEventData, AndroidApplication } from '@nativescript/core';
1+
import { Enums, Application, UnhandledErrorEventData, AndroidApplication, Device, ApplicationSettings } from '@nativescript/core';
22
import { LocationBase, defaultGetLocationTimeout, fastestTimeUpdate, minTimeUpdate } from './common';
33
import { Options, successCallbackType, errorCallbackType } from '.';
44
import * as permissions from 'nativescript-permissions';
@@ -53,7 +53,7 @@ function errorHandler(errData: UnhandledErrorEventData) {
5353

5454
export function getCurrentLocation(options: Options): Promise<Location> {
5555
return new Promise(function (resolve, reject) {
56-
enableLocationRequest().then(() => {
56+
enableLocationRequest(false, false).then(() => {
5757
if (options.timeout === 0) {
5858
// get last known
5959
LocationManager.getLastLocation(options.maximumAge, resolve, reject);
@@ -119,12 +119,23 @@ function _getLocationRequest(options: Options): any {
119119
return mLocationRequest;
120120
}
121121

122-
function _requestLocationPermissions(): Promise<void> {
122+
function _requestLocationPermissions(always: boolean): Promise<void> {
123123
return new Promise<void>(function (resolve, reject) {
124124
if (LocationManager.shouldSkipChecks()) {
125125
resolve();
126126
} else {
127-
permissions.requestPermission((<any>android).Manifest.permission.ACCESS_FINE_LOCATION).then(resolve, reject);
127+
if (always) {
128+
ApplicationSettings.setBoolean('askedForAlwaysPermission', true);
129+
permissions
130+
.requestPermission((<any>android).Manifest.permission.ACCESS_BACKGROUND_LOCATION)
131+
.then(resolve, reject)
132+
.catch((e) => {
133+
console.error('Failed to request Android background location permission due to: ' + e);
134+
});
135+
} else {
136+
ApplicationSettings.setBoolean('askedForWhileUsePermission', true);
137+
permissions.requestPermission((<any>android).Manifest.permission.ACCESS_FINE_LOCATION).then(resolve, reject);
138+
}
128139
}
129140
});
130141
}
@@ -194,37 +205,48 @@ export function clearWatch(watchId: number): void {
194205
}
195206
}
196207

197-
export function enableLocationRequest(always?: boolean): Promise<void> {
208+
export function enableLocationRequest(always?: boolean, openSettingsIfLocationHasBeenDenied?: boolean): Promise<void> {
198209
return new Promise<void>(function (resolve, reject) {
199-
_requestLocationPermissions().then(() => {
200-
_makeGooglePlayServicesAvailable().then(() => {
201-
_isLocationServiceEnabled().then(
202-
() => {
203-
resolve();
204-
},
205-
(ex) => {
206-
if (typeof ex.getStatusCode === 'function') {
207-
const statusCode = ex.getStatusCode();
208-
if (statusCode === com.google.android.gms.location.LocationSettingsStatusCodes.RESOLUTION_REQUIRED) {
209-
try {
210-
// cache resolve and reject callbacks in order to call them
211-
// on REQUEST_ENABLE_LOCATION Activity Result
212-
_onEnableLocationSuccess = resolve;
213-
_onEnableLocationFail = reject;
214-
return ex.startResolutionForResult(Application.android.foregroundActivity || Application.android.startActivity, REQUEST_ENABLE_LOCATION);
215-
} catch (sendEx) {
216-
// Ignore the error.
210+
// on API level <29 there is no ACCESS_BACKGROUND_LOCATION permission
211+
const _always = Device.sdkVersion >= '29' ? always : false;
212+
if (!_systemDialogWillShow(_always) && !_permissionIsGiven(_always)) {
213+
if (openSettingsIfLocationHasBeenDenied) {
214+
reject(new Error('User needs to enable permission from settings'));
215+
_goToPhoneSettings();
216+
} else {
217+
reject(new Error('User denied location permission previously'));
218+
}
219+
} else {
220+
_requestLocationPermissions(_always).then(() => {
221+
_makeGooglePlayServicesAvailable().then(() => {
222+
_isLocationServiceEnabled().then(
223+
() => {
224+
resolve();
225+
},
226+
(ex) => {
227+
if (typeof ex.getStatusCode === 'function') {
228+
const statusCode = ex.getStatusCode();
229+
if (statusCode === com.google.android.gms.location.LocationSettingsStatusCodes.RESOLUTION_REQUIRED) {
230+
try {
231+
// cache resolve and reject callbacks in order to call them
232+
// on REQUEST_ENABLE_LOCATION Activity Result
233+
_onEnableLocationSuccess = resolve;
234+
_onEnableLocationFail = reject;
235+
return ex.startResolutionForResult(Application.android.foregroundActivity || Application.android.startActivity, REQUEST_ENABLE_LOCATION);
236+
} catch (sendEx) {
237+
// Ignore the error.
238+
return resolve();
239+
}
240+
} else if (statusCode === com.google.android.gms.location.LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE && isAirplaneModeOn() && isProviderEnabled(android.location.LocationManager.GPS_PROVIDER)) {
217241
return resolve();
218242
}
219-
} else if (statusCode === com.google.android.gms.location.LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE && isAirplaneModeOn() && isProviderEnabled(android.location.LocationManager.GPS_PROVIDER)) {
220-
return resolve();
221243
}
244+
reject(new Error('Cannot enable the location service. ' + ex));
222245
}
223-
reject(new Error('Cannot enable the location service. ' + ex));
224-
}
225-
);
246+
);
247+
}, reject);
226248
}, reject);
227-
}, reject);
249+
}
228250
});
229251
}
230252

@@ -274,6 +296,37 @@ function _isLocationServiceEnabled(options?: Options): Promise<boolean> {
274296
});
275297
}
276298

299+
function _goToPhoneSettings() {
300+
const intent = new android.content.Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, android.net.Uri.fromParts('package', Application.android.context.getPackageName(), null));
301+
const activity = Application.android.foregroundActivity || Application.android.startActivity;
302+
intent.addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
303+
activity.startActivity(intent);
304+
}
305+
306+
/**
307+
* The system dialog will not show if we asked for permission previously and the user denied permission with selecting don't ask again (API level <30).
308+
* Starting on Android 11 (API level 30), there is no 'don't ask again' option. If the user taps Deny for a specific permission more than once during the app's lifetime,
309+
* the user doesn't see the system permissions dialog if his app requests that permission again. The user's action implies "don't ask again."
310+
* https://developer.android.com/training/permissions/requesting#one-time
311+
* With API level 30 Android introduced the 'Only this time' or 'Ask every time' permission which will cause shouldShowRequestPermissionRationale() to return false as if the permission has not been requested yet.
312+
* This makes it impossible to differentiate between the case where the user granted one time permission and the 'don't ask again' case. Therefore we always assume askedForPermission to be false for API level >= 30
313+
* to ensure that the system popup is always shown when the user grants one time permission.
314+
* https://github.com/Baseflow/flutter-geolocator/issues/662#issuecomment-778121610
315+
* @param always if true then check will be done for ACCESS_BACKGROUND_LOCATION permission, if false then check will be done for ACCESS_FINE_LOCATION permission
316+
* @returns true if the Android permission dialog will be shown to the user when the location permission is requested the next time, returns false otherwise
317+
*/
318+
function _systemDialogWillShow(always: boolean): boolean {
319+
const permissionType = always ? (<any>android).Manifest.permission.ACCESS_BACKGROUND_LOCATION : (<any>android).Manifest.permission.ACCESS_FINE_LOCATION;
320+
const doNotAskAgain = !androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale(Application.android.foregroundActivity, permissionType);
321+
let askedForPermission = always ? ApplicationSettings.getBoolean('askedForAlwaysPermission', false) : ApplicationSettings.getBoolean('askedForWhileUsePermission', false);
322+
askedForPermission = askedForPermission && Device.sdkVersion < '30';
323+
return !(askedForPermission && doNotAskAgain);
324+
}
325+
326+
function _permissionIsGiven(always: boolean): boolean {
327+
return always ? permissions.hasPermission((<any>android).Manifest.permission.ACCESS_BACKGROUND_LOCATION) : permissions.hasPermission((<any>android).Manifest.permission.ACCESS_FINE_LOCATION);
328+
}
329+
277330
export function isEnabled(options?: Options): Promise<boolean> {
278331
return new Promise(function (resolve, reject) {
279332
if (!_isGooglePlayServicesAvailable() || !permissions.hasPermission((<any>android).Manifest.permission.ACCESS_FINE_LOCATION)) {

0 commit comments

Comments
 (0)
0