MOBILE-4470 location: Fix location in Android and iOS
This commit is contained in:
		
							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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user