diff --git a/src/addon/mod/data/fields/latlong/component/latlong.ts b/src/addon/mod/data/fields/latlong/component/latlong.ts index 6beb14285..f34e13102 100644 --- a/src/addon/mod/data/fields/latlong/component/latlong.ts +++ b/src/addon/mod/data/fields/latlong/component/latlong.ts @@ -14,15 +14,10 @@ import { Component } from '@angular/core'; import { FormBuilder } from '@angular/forms'; import { DomSanitizer, SafeUrl } from '@angular/platform-browser'; -import { Platform } from 'ionic-angular'; -import { Geolocation } from '@ionic-native/geolocation'; import { AddonModDataFieldPluginComponent } from '../../../classes/field-plugin-component'; import { CoreApp, CoreAppProvider } from '@providers/app'; +import { CoreGeolocation, CoreGeolocationError, CoreGeolocationErrorReason } from '@providers/geolocation'; import { CoreDomUtilsProvider } from '@providers/utils/dom'; -import { Diagnostic } from '@singletons/core.singletons'; -import { CoreError } from '@classes/error'; - -class AccessLocationError extends CoreError {} /** * Component to render data latlong field. @@ -37,13 +32,11 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo east: number; showGeolocation: boolean; - constructor(protected fb: FormBuilder, - protected platform: Platform, - protected geolocation: Geolocation, + constructor( + protected fb: FormBuilder, protected domUtils: CoreDomUtilsProvider, protected sanitizer: DomSanitizer, - appProvider: CoreAppProvider - ) { + appProvider: CoreAppProvider) { super(fb); this.showGeolocation = !appProvider.isDesktop(); @@ -126,107 +119,25 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo const modal = this.domUtils.showModalLoading('addon.mod_data.gettinglocation', true); try { - await this.updateLocation(); + const coordinates = await CoreGeolocation.instance.getCoordinates(); + + this.form.controls['f_' + this.field.id + '_0'].setValue(coordinates.latitude); + this.form.controls['f_' + this.field.id + '_1'].setValue(coordinates.longitude); } catch (error) { - this.showErrorModal(error); + this.showLocationErrorModal(error); } modal.dismiss(); } /** - * Update component location. - */ - protected async updateLocation(): Promise { - await this.authorizeLocation(); - await this.enableLocation(); - - const result = await this.geolocation.getCurrentPosition({ - enableHighAccuracy: true, - timeout: 30000, - }); - - this.form.controls['f_' + this.field.id + '_0'].setValue(result.coords.latitude); - this.form.controls['f_' + this.field.id + '_1'].setValue(result.coords.longitude); - } - - /** - * Make sure that using device location has been authorize and ask for permission if it hasn't. + * Show the appropriate error modal for the given error getting the location. * - * @param failOnDeniedOnce Throw an exception if the permission has been denied once. + * @param error Location error. */ - protected async authorizeLocation(failOnDeniedOnce: boolean = false): Promise { - const authorizationStatus = await Diagnostic.instance.getLocationAuthorizationStatus(); - - switch (authorizationStatus) { - // This constant is hard-coded because it is not declared in @ionic-native/diagnostic v4. - case 'DENIED_ONCE': - if (failOnDeniedOnce) { - throw new AccessLocationError('addon.mod_data.locationpermissiondenied'); - } - // Fall through. - case Diagnostic.instance.permissionStatus.NOT_REQUESTED: - await Diagnostic.instance.requestLocationAuthorization(); - await CoreApp.instance.waitForResume(500); - await this.authorizeLocation(true); - - return; - case Diagnostic.instance.permissionStatus.GRANTED: - case Diagnostic.instance.permissionStatus.GRANTED_WHEN_IN_USE: - // Location is authorized. - return; - case Diagnostic.instance.permissionStatus.DENIED: - default: - throw new AccessLocationError('addon.mod_data.locationpermissiondenied'); - } - } - - /** - * Make sure that location is enabled and switch to settings if it hasn't. - */ - protected async enableLocation(): Promise { - let locationEnabled = await Diagnostic.instance.isLocationEnabled(); - - if (locationEnabled) { - // Location is enabled. - return; - } - - if (!CoreApp.instance.isIOS()) { - await Diagnostic.instance.switchToLocationSettings(); - await CoreApp.instance.waitForResume(30000); - - locationEnabled = await Diagnostic.instance.isLocationEnabled(); - } - - if (!locationEnabled) { - throw new AccessLocationError('addon.mod_data.locationnotenabled'); - } - } - - /** - * Check whether an error was caused by a PERMISSION_DENIED. - * - * @param error Error. - */ - protected isPermissionDeniedError(error?: any): boolean { - return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED; - } - - /** - * Show the appropriate error modal for the given error. - * - * @param error Error. - */ - protected showErrorModal(error: any): void { - if (error instanceof AccessLocationError) { - this.domUtils.showErrorModal(error.message, true); - - return; - } - - if (this.isPermissionDeniedError(error)) { - this.domUtils.showErrorModal('addon.mod_data.locationpermissiondenied', true); + protected showLocationErrorModal(error: any): void { + if (error instanceof CoreGeolocationError) { + this.domUtils.showErrorModal(this.getGeolocationErrorMessage(error), true); return; } @@ -234,4 +145,19 @@ export class AddonModDataFieldLatlongComponent extends AddonModDataFieldPluginCo this.domUtils.showErrorModalDefault(error, 'Error getting location'); } + /** + * Get error message from a geolocation error. + * + * @param error Geolocation error. + */ + protected getGeolocationErrorMessage(error: CoreGeolocationError): string { + // tslint:disable-next-line: switch-default + switch (error.reason) { + case CoreGeolocationErrorReason.PermissionDenied: + return 'addon.mod_data.locationpermissiondenied'; + case CoreGeolocationErrorReason.LocationNotEnabled: + return 'addon.mod_data.locationnotenabled'; + } + } + } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 911f2d755..cb49b0c71 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -26,6 +26,7 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; import { TranslateHttpLoader } from '@ngx-translate/http-loader'; import { Diagnostic } from '@ionic-native/diagnostic'; +import { Geolocation } from '@ionic-native/geolocation'; import { ScreenOrientation } from '@ionic-native/screen-orientation'; import { MoodleMobileApp } from './app.component'; @@ -60,6 +61,7 @@ import { CorePluginFileDelegate } from '@providers/plugin-file-delegate'; import { CoreSyncProvider } from '@providers/sync'; import { CoreFileHelperProvider } from '@providers/file-helper'; import { CoreCustomURLSchemesProvider } from '@providers/urlschemes'; +import { CoreGeolocationProvider } from '@providers/geolocation'; // Handlers. import { CoreSiteInfoCronHandler } from '@providers/handlers/site-info-cron-handler'; @@ -195,7 +197,8 @@ export const CORE_PROVIDERS: any[] = [ CorePluginFileDelegate, CoreSyncProvider, CoreFileHelperProvider, - CoreCustomURLSchemesProvider + CoreCustomURLSchemesProvider, + CoreGeolocationProvider, ]; export const WP_PROVIDER: any = null; @@ -343,6 +346,7 @@ export const WP_PROVIDER: any = null; CoreSyncProvider, CoreFileHelperProvider, CoreCustomURLSchemesProvider, + CoreGeolocationProvider, CoreSiteInfoCronHandler, { provide: HTTP_INTERCEPTORS, @@ -350,6 +354,7 @@ export const WP_PROVIDER: any = null; multi: true, }, Diagnostic, + Geolocation, ScreenOrientation, {provide: COMPILER_OPTIONS, useValue: {}, multi: true}, {provide: JitCompilerFactory, useClass: JitCompilerFactory, deps: [COMPILER_OPTIONS]}, diff --git a/src/core/compile/providers/compile.ts b/src/core/compile/providers/compile.ts index e62e8ba70..5a34199b7 100644 --- a/src/core/compile/providers/compile.ts +++ b/src/core/compile/providers/compile.ts @@ -67,6 +67,7 @@ import { CoreContentLinksModuleGradeHandler } from '@core/contentlinks/classes/m import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler'; import { CoreCourseActivityPrefetchHandlerBase } from '@core/course/classes/activity-prefetch-handler'; import { CoreCourseResourcePrefetchHandlerBase } from '@core/course/classes/resource-prefetch-handler'; +import { CoreGeolocationError, CoreGeolocationErrorReason } from '@providers/geolocation'; // Import all core modules that define components, directives and pipes. import { CoreComponentsModule } from '@components/components.module'; @@ -294,6 +295,8 @@ export class CoreCompileProvider { instance['CoreSitePluginsQuizAccessRuleComponent'] = CoreSitePluginsQuizAccessRuleComponent; instance['CoreSitePluginsAssignFeedbackComponent'] = CoreSitePluginsAssignFeedbackComponent; instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent; + instance['CoreGeolocationError'] = CoreGeolocationError; + instance['CoreGeolocationErrorReason'] = CoreGeolocationErrorReason; } /** diff --git a/src/providers/geolocation.ts b/src/providers/geolocation.ts new file mode 100644 index 000000000..138664645 --- /dev/null +++ b/src/providers/geolocation.ts @@ -0,0 +1,143 @@ +// (C) Copyright 2015 Moodle Pty Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable } from '@angular/core'; +import { Coordinates } from '@ionic-native/geolocation'; +import { CoreApp } from '@providers/app'; +import { Geolocation, Diagnostic, makeSingleton } from '@singletons/core.singletons'; +import { CoreError } from '@classes/error'; + +export enum CoreGeolocationErrorReason { + PermissionDenied = 'permission-denied', + LocationNotEnabled = 'location-not-enabled', +} + +export class CoreGeolocationError extends CoreError { + + readonly reason: CoreGeolocationErrorReason; + + constructor(reason: CoreGeolocationErrorReason) { + super(`GeolocationError: ${reason}`); + + this.reason = reason; + } + +} + +@Injectable() +export class CoreGeolocationProvider { + + /** + * Get current user coordinates. + * + * @throws {CoreGeolocationError} + */ + async getCoordinates(): Promise { + try { + await this.authorizeLocation(); + await this.enableLocation(); + + const result = await Geolocation.instance.getCurrentPosition({ + enableHighAccuracy: true, + timeout: 30000, + }); + + return result.coords; + } catch (error) { + if (this.isCordovaPermissionDeniedError(error)) { + throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied); + } + + throw error; + } + } + + /** + * Make sure that using device location has been authorized and ask for permission if it hasn't. + * + * @throws {CoreGeolocationError} + */ + async authorizeLocation(): Promise { + await this.doAuthorizeLocation(); + } + + /** + * Make sure that location is enabled and open settings to enable it if necessary. + * + * @throws {CoreGeolocationError} + */ + async enableLocation(): Promise { + let locationEnabled = await Diagnostic.instance.isLocationEnabled(); + + if (locationEnabled) { + // Location is enabled. + return; + } + + if (!CoreApp.instance.isIOS()) { + await Diagnostic.instance.switchToLocationSettings(); + await CoreApp.instance.waitForResume(30000); + + locationEnabled = await Diagnostic.instance.isLocationEnabled(); + } + + if (!locationEnabled) { + throw new CoreGeolocationError(CoreGeolocationErrorReason.LocationNotEnabled); + } + } + + /** + * Recursive implementation of authorizeLocation method, protected to avoid exposing the failOnDeniedOnce parameter. + * + * @param failOnDeniedOnce Throw an exception if the permission has been denied once. + * @throws {CoreGeolocationError} + */ + protected async doAuthorizeLocation(failOnDeniedOnce: boolean = false): Promise { + const authorizationStatus = await Diagnostic.instance.getLocationAuthorizationStatus(); + + switch (authorizationStatus) { + // This constant is hard-coded because it is not declared in @ionic-native/diagnostic v4. + case 'DENIED_ONCE': + if (failOnDeniedOnce) { + throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied); + } + // Fall through. + case Diagnostic.instance.permissionStatus.NOT_REQUESTED: + await Diagnostic.instance.requestLocationAuthorization(); + await CoreApp.instance.waitForResume(500); + await this.doAuthorizeLocation(true); + + return; + case Diagnostic.instance.permissionStatus.GRANTED: + case Diagnostic.instance.permissionStatus.GRANTED_WHEN_IN_USE: + // Location is authorized. + return; + case Diagnostic.instance.permissionStatus.DENIED: + default: + throw new CoreGeolocationError(CoreGeolocationErrorReason.PermissionDenied); + } + } + + /** + * Check whether an error was caused by a PERMISSION_DENIED from the cordova plugin. + * + * @param error Error. + */ + protected isCordovaPermissionDeniedError(error?: any): boolean { + return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED; + } + +} + +export class CoreGeolocation extends makeSingleton(CoreGeolocationProvider) {} diff --git a/src/singletons/core.singletons.ts b/src/singletons/core.singletons.ts index ad230e5ca..349afea24 100644 --- a/src/singletons/core.singletons.ts +++ b/src/singletons/core.singletons.ts @@ -16,6 +16,7 @@ import { AlertController, App } from 'ionic-angular'; import { Injector } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; import { HttpClient } from '@angular/common/http'; +import { Geolocation as GeolocationService } from '@ionic-native/geolocation'; import { Diagnostic as DiagnosticService } from '@ionic-native/diagnostic'; import { CoreSingletonsFactory, CoreInjectionToken, CoreSingletonClass } from '@classes/singletons-factory'; @@ -49,4 +50,6 @@ export class Ionic extends makeSingleton(App) {} export class Diagnostic extends makeSingleton(DiagnosticService) {} +export class Geolocation extends makeSingleton(GeolocationService) {} + export class Http extends makeSingleton(HttpClient) {}