MOBILE-4470 location: Fix location in Android and iOS
parent
c4a0a5c7c8
commit
554e5eeb76
|
@ -21,10 +21,6 @@
|
|||
<param name="android-package" value="com.moodle.moodlemobile.Diagnostic"/>
|
||||
<param name="onload" value="true" />
|
||||
</feature>
|
||||
<feature name="Diagnostic_Microphone">
|
||||
<param name="android-package" value="com.moodle.moodlemobile.Diagnostic"/>
|
||||
<param name="onload" value="true" />
|
||||
</feature>
|
||||
<feature name="Diagnostic_Location">
|
||||
<param name="android-package" value="com.moodle.moodlemobile.Diagnostic_Location"/>
|
||||
<param name="onload" value="true" />
|
||||
|
|
|
@ -39,7 +39,8 @@
|
|||
// limitations under the License.
|
||||
|
||||
/**
|
||||
* Checks whether device hardware features are enabled or available to the app, e.g. camera, GPS, wifi
|
||||
* Checks whether device hardware features are enabled or available to the app, e.g. camera, GPS, wifi.
|
||||
* Most of this code was copied from https://github.com/dpa99c/cordova-diagnostic-plugin
|
||||
*/
|
||||
export class Diagnostic {
|
||||
|
||||
|
@ -56,37 +57,249 @@ export class Diagnostic {
|
|||
*/
|
||||
declare permission: typeof permission;
|
||||
|
||||
declare protected requestInProgress: boolean;
|
||||
|
||||
constructor() {
|
||||
this.permissionStatus = permissionStatus;
|
||||
this.permission = permission;
|
||||
|
||||
this.requestInProgress = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the device location setting is enabled.
|
||||
* On iOS, returns true if Location Services is enabled.
|
||||
* On Android, returns true if Location Mode is enabled and any mode is selected (e.g. Battery saving, Device only, ...).
|
||||
*
|
||||
* @returns True if location setting is enabled.
|
||||
*/
|
||||
isLocationEnabled(): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve, reject) => cordova.exec(resolve, reject, 'Diagnostic_Location', 'isLocationEnabled'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Android only. Switches to the Location page in the Settings app.
|
||||
*
|
||||
* @returns Promise resolved when done.
|
||||
*/
|
||||
switchToLocationSettings(): Promise<void> {
|
||||
if (cordova.platformId !== 'android') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
cordova.exec(resolve, reject, 'Diagnostic_Location', 'switchToLocationSettings'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens settings page for this app.
|
||||
*/
|
||||
switchToSettings(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => cordova.exec(resolve, reject, 'Diagnostic', 'switchToSettings'));
|
||||
}
|
||||
|
||||
getLocationAuthorizationStatus(): Promise<unknown> {
|
||||
return new Promise<unknown>((resolve, reject) =>
|
||||
cordova.exec(resolve, reject, 'Diagnostic_Location', 'getLocationAuthorizationStatus'));
|
||||
/**
|
||||
* Returns the location authorization status for the application.
|
||||
*
|
||||
* @returns Authorization status.
|
||||
*/
|
||||
getLocationAuthorizationStatus(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (cordova.platformId === 'ios') {
|
||||
cordova.exec(resolve, reject, 'Diagnostic_Location', 'getLocationAuthorizationStatus');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
requestLocationAuthorization(): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) =>
|
||||
cordova.exec(resolve, reject, 'Diagnostic_Location', 'requestLocationAuthorization'));
|
||||
this.getPermissionsAuthorizationStatus([
|
||||
permission.accessCoarseLocation,
|
||||
permission.accessFineLocation,
|
||||
permission.accessBackgroundLocation,
|
||||
]).then(statuses => {
|
||||
const backgroundStatus = typeof statuses[permission.accessBackgroundLocation] !== 'undefined' ?
|
||||
statuses[permission.accessBackgroundLocation] : true;
|
||||
|
||||
let status = this.combinePermissionStatuses({
|
||||
[permission.accessCoarseLocation]: statuses[permission.accessCoarseLocation],
|
||||
[permission.accessFineLocation]: statuses[permission.accessFineLocation],
|
||||
});
|
||||
if (status === permissionStatus.granted && backgroundStatus !== permissionStatus.granted) {
|
||||
status = permissionStatus.grantedWhenInUse;
|
||||
}
|
||||
|
||||
requestMicrophoneAuthorization(): Promise<string> {
|
||||
resolve(status);
|
||||
|
||||
return;
|
||||
}).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests location authorization for the application.
|
||||
* Authorization can be requested to use location either "when in use" (only foreground) or "always" (foreground & background).
|
||||
* Should only be called if authorization status is NOT_REQUESTED. Calling it when in any other state will have no effect.
|
||||
*
|
||||
* @returns Permission status.
|
||||
*/
|
||||
requestLocationAuthorization(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) =>
|
||||
cordova.exec(resolve, reject, 'Diagnostic_Microphone', 'requestMicrophoneAuthorization'));
|
||||
cordova.exec(
|
||||
status => resolve(this.convertPermissionStatus(status)),
|
||||
reject,
|
||||
'Diagnostic_Location',
|
||||
'requestLocationAuthorization',
|
||||
[false, true],
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests access to microphone if authorization was never granted nor denied, will only return access status otherwise.
|
||||
*
|
||||
* @returns Permission status.
|
||||
*/
|
||||
requestMicrophoneAuthorization(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (cordova.platformId === 'ios') {
|
||||
cordova.exec(
|
||||
(isGranted) => resolve(isGranted ? permissionStatus.granted : permissionStatus.deniedAlways),
|
||||
reject,
|
||||
'Diagnostic_Microphone',
|
||||
'requestMicrophoneAuthorization',
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestRuntimePermission(permission.recordAudio).then(resolve).catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Android only. Given a list of permissions, returns the status for each permission.
|
||||
*
|
||||
* @param permissions Permissions to check.
|
||||
* @returns Status for each permission.
|
||||
*/
|
||||
protected getPermissionsAuthorizationStatus(permissions: string[]): Promise<Record<string, string>> {
|
||||
return new Promise<Record<string, string>>((resolve, reject) => {
|
||||
if (cordova.platformId !== 'android') {
|
||||
resolve({});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
cordova.exec(
|
||||
(statuses) => {
|
||||
for (const permission in statuses) {
|
||||
statuses[permission] = this.convertPermissionStatus(statuses[permission]);
|
||||
}
|
||||
|
||||
resolve(statuses);
|
||||
},
|
||||
reject,
|
||||
'Diagnostic',
|
||||
'getPermissionsAuthorizationStatus',
|
||||
[permissions],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of permissions and their statuses, returns the combined status.
|
||||
*
|
||||
* @param statuses Permissions with statuses.
|
||||
* @returns Combined status.
|
||||
*/
|
||||
protected combinePermissionStatuses(statuses: Record<string, string>): string {
|
||||
if (this.anyStatusIs(statuses, permissionStatus.deniedAlways)) {
|
||||
return permissionStatus.deniedAlways;
|
||||
}
|
||||
|
||||
if (this.anyStatusIs(statuses, permissionStatus.deniedOnce)) {
|
||||
return permissionStatus.deniedOnce;
|
||||
}
|
||||
|
||||
if (this.anyStatusIs(statuses, permissionStatus.granted)) {
|
||||
return permissionStatus.granted;
|
||||
}
|
||||
|
||||
return permissionStatus.notRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any of the permissions in the statuses object has the specified status.
|
||||
*
|
||||
* @param statuses Permissions with status for each permission.
|
||||
* @param status Status to check.
|
||||
* @returns True if any permission has the specified status.
|
||||
*/
|
||||
protected anyStatusIs(statuses: Record<string, string>, status: string): boolean {
|
||||
for (const permission in statuses) {
|
||||
if (statuses[permission] === status) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Android only. Requests app to be granted authorisation for a runtime permission.
|
||||
*
|
||||
* @param permission Permissions to request.
|
||||
* @returns Status for each permission.
|
||||
*/
|
||||
protected requestRuntimePermission(permission: string): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (cordova.platformId !== 'android') {
|
||||
resolve(permissionStatus.granted);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.requestInProgress) {
|
||||
reject('A runtime permissions request is already in progress');
|
||||
}
|
||||
|
||||
this.requestInProgress = true;
|
||||
|
||||
cordova.exec(
|
||||
(statuses) => {
|
||||
this.requestInProgress = false;
|
||||
resolve(this.convertPermissionStatus(statuses[permission]));
|
||||
},
|
||||
(error) => {
|
||||
this.requestInProgress = false;
|
||||
reject(error);
|
||||
},
|
||||
'Diagnostic',
|
||||
'requestRuntimePermission',
|
||||
[permission],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a permission status so it has the same value in all platforms.
|
||||
* Each platform can return a different value for a status, e.g. a granted permission returns 'authorized' in iOS and
|
||||
* 'GRANTED' in Android. This function will convert the status so it uses the iOS value in all platforms, unless it's an
|
||||
* Android specific value.
|
||||
*
|
||||
* @param status Original status.
|
||||
* @returns Converted status.
|
||||
*/
|
||||
protected convertPermissionStatus(status: string): string {
|
||||
for (const name in androidPermissionStatus) {
|
||||
const androidStatus = androidPermissionStatus[name as keyof typeof androidPermissionStatus];
|
||||
const iosStatus = iosPermissionStatus[name as keyof typeof iosPermissionStatus];
|
||||
|
||||
if (status === androidStatus && iosStatus !== undefined) {
|
||||
// Always use the iOS status if the status exists both in Android and iOS.
|
||||
return iosStatus;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -116,18 +329,35 @@ const permission = {
|
|||
writeExternalStorage: 'WRITE_EXTERNAL_STORAGE',
|
||||
} as const;
|
||||
|
||||
const permissionStatus = {
|
||||
// Android only
|
||||
deniedOnce: 'DENIED_ONCE',
|
||||
|
||||
// iOS only
|
||||
restricted: 'restricted',
|
||||
ephimeral: 'ephemeral',
|
||||
provisional: 'provisional',
|
||||
|
||||
// Both iOS and Android
|
||||
granted: 'authorized' || 'GRANTED',
|
||||
const androidPermissionStatus = {
|
||||
// Location permission requested and
|
||||
// app build SDK/user device is Android >10 and user granted background location ("all the time") permission,
|
||||
// or app build SDK/user device is Android 6-9 and user granted location permission,
|
||||
// or non-location permission requested
|
||||
// and app build SDK/user device is Android >=6 and user granted permission
|
||||
// or app build SDK/user device is Android <6
|
||||
granted: 'GRANTED',
|
||||
// Location permission requested
|
||||
// and app build SDK/user device is Android >10
|
||||
// and user granted background foreground location ("while-in-use") permission
|
||||
grantedWhenInUse: 'authorized_when_in_use',
|
||||
notRequested: 'not_determined' || 'NOT_REQUESTED',
|
||||
deniedAlways: 'denied_always' || 'DENIED_ALWAYS',
|
||||
deniedOnce: 'DENIED_ONCE', // User denied access to this permission.
|
||||
deniedAlways: 'DENIED_ALWAYS', // User denied access to this permission and checked "Never Ask Again" box.
|
||||
notRequested: 'NOT_REQUESTED', // App has not yet requested access to this permission.
|
||||
} as const;
|
||||
|
||||
const iosPermissionStatus = {
|
||||
notRequested: 'not_determined', // App has not yet requested this permission
|
||||
deniedAlways: 'denied_always', // User denied access to this permission
|
||||
restricted: 'restricted', // Permission is unavailable and user cannot enable it. For example, when parental controls are on.
|
||||
granted: 'authorized', // User granted access to this permission.
|
||||
grantedWhenInUse: 'authorized_when_in_use', // User granted access use location permission only when app is in use
|
||||
ephimeral: 'ephemeral', // The app is authorized to schedule or receive notifications for a limited amount of time.
|
||||
provisional: 'provisional', // The application is provisionally authorized to post non-interruptive user notifications.
|
||||
limited: 'limited', // The app has limited access to the Photo Library.
|
||||
} as const;
|
||||
|
||||
const permissionStatus = {
|
||||
...androidPermissionStatus,
|
||||
...iosPermissionStatus,
|
||||
} as const;
|
||||
|
|
|
@ -122,16 +122,16 @@ export class CoreGeolocationProvider {
|
|||
this.logger.log(`Authorize location: status ${authorizationStatus}`);
|
||||
|
||||
switch (authorizationStatus) {
|
||||
case diagnostic.permissionStatus.deniedOnce:
|
||||
if (failOnDeniedOnce) {
|
||||
throw new CoreGeolocationError(CoreGeolocationErrorReason.PERMISSION_DENIED);
|
||||
}
|
||||
|
||||
case diagnostic.permissionStatus.granted:
|
||||
case diagnostic.permissionStatus.grantedWhenInUse:
|
||||
// Location is authorized.
|
||||
return;
|
||||
|
||||
case diagnostic.permissionStatus.deniedOnce:
|
||||
if (failOnDeniedOnce) {
|
||||
throw new CoreGeolocationError(CoreGeolocationErrorReason.PERMISSION_DENIED);
|
||||
}
|
||||
|
||||
// Fall through.
|
||||
case diagnostic.permissionStatus.notRequested:
|
||||
this.logger.log('Request location authorization.');
|
||||
|
|
Loading…
Reference in New Issue