diff --git a/cordova-plugin-moodleapp/plugin.xml b/cordova-plugin-moodleapp/plugin.xml
index a1a93f296..bbce2501c 100644
--- a/cordova-plugin-moodleapp/plugin.xml
+++ b/cordova-plugin-moodleapp/plugin.xml
@@ -21,10 +21,6 @@
-
-
-
-
diff --git a/cordova-plugin-moodleapp/src/ts/plugins/Diagnostic.ts b/cordova-plugin-moodleapp/src/ts/plugins/Diagnostic.ts
index df446b68f..9ac2f3aeb 100644
--- a/cordova-plugin-moodleapp/src/ts/plugins/Diagnostic.ts
+++ b/cordova-plugin-moodleapp/src/ts/plugins/Diagnostic.ts
@@ -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 {
return new Promise((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 {
+ if (cordova.platformId !== 'android') {
+ return Promise.resolve();
+ }
+
return new Promise((resolve, reject) =>
cordova.exec(resolve, reject, 'Diagnostic_Location', 'switchToLocationSettings'));
}
+ /**
+ * Opens settings page for this app.
+ */
switchToSettings(): Promise {
return new Promise((resolve, reject) => cordova.exec(resolve, reject, 'Diagnostic', 'switchToSettings'));
}
- getLocationAuthorizationStatus(): Promise {
- return new Promise((resolve, reject) =>
- cordova.exec(resolve, reject, 'Diagnostic_Location', 'getLocationAuthorizationStatus'));
+ /**
+ * Returns the location authorization status for the application.
+ *
+ * @returns Authorization status.
+ */
+ getLocationAuthorizationStatus(): Promise {
+ return new Promise((resolve, reject) => {
+ if (cordova.platformId === 'ios') {
+ cordova.exec(resolve, reject, 'Diagnostic_Location', 'getLocationAuthorizationStatus');
+
+ return;
+ }
+
+ 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;
+ }
+
+ resolve(status);
+
+ return;
+ }).catch(reject);
+ });
}
- requestLocationAuthorization(): Promise {
- return new Promise((resolve, reject) =>
- cordova.exec(resolve, reject, 'Diagnostic_Location', 'requestLocationAuthorization'));
- }
-
- requestMicrophoneAuthorization(): Promise {
+ /**
+ * 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 {
return new Promise((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 {
+ return new Promise((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> {
+ return new Promise>((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 {
+ 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, 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 {
+ return new Promise((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;
diff --git a/src/core/services/geolocation.ts b/src/core/services/geolocation.ts
index 2f3df421b..5974f5de5 100644
--- a/src/core/services/geolocation.ts
+++ b/src/core/services/geolocation.ts
@@ -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.');