MOBILE-4470 location: Fix location in Android and iOS

main
Dani Palou 2024-06-13 10:42:54 +02:00
parent c4a0a5c7c8
commit 554e5eeb76
3 changed files with 259 additions and 33 deletions

View File

@ -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" />

View File

@ -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;

View File

@ -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.');