From abf2867fc6ec020749341b0bc780b6396ecbb366 Mon Sep 17 00:00:00 2001 From: Alfonso Salces Date: Fri, 9 Feb 2024 10:11:51 +0100 Subject: [PATCH] MOBILE-4501 cordova-plugin-moodleapp: Add diagnostic plugin --- cordova-plugin-moodleapp/plugin.xml | 21 +++ cordova-plugin-moodleapp/src/ts/index.ts | 2 + .../src/ts/plugins/Diagnostic.ts | 131 ++++++++++++++++++ cordova-plugin-moodleapp/types/index.d.ts | 2 + package-lock.json | 39 ------ package.json | 7 - .../audio-recorder.component.ts | 15 +- src/core/features/native/native.module.ts | 3 - .../settings/pages/general/general.ts | 5 +- src/core/services/geolocation.ts | 50 +++++-- src/core/services/utils/iframe.ts | 5 +- src/core/singletons/index.ts | 2 - 12 files changed, 207 insertions(+), 75 deletions(-) create mode 100644 cordova-plugin-moodleapp/src/ts/plugins/Diagnostic.ts diff --git a/cordova-plugin-moodleapp/plugin.xml b/cordova-plugin-moodleapp/plugin.xml index 4a17c33c4..d8e5e6abc 100644 --- a/cordova-plugin-moodleapp/plugin.xml +++ b/cordova-plugin-moodleapp/plugin.xml @@ -15,9 +15,30 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/cordova-plugin-moodleapp/src/ts/index.ts b/cordova-plugin-moodleapp/src/ts/index.ts index 79542caa0..1b2ff1e8c 100644 --- a/cordova-plugin-moodleapp/src/ts/index.ts +++ b/cordova-plugin-moodleapp/src/ts/index.ts @@ -12,12 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { Diagnostic } from './plugins/Diagnostic'; import { InstallReferrer } from './plugins/InstallReferrer'; import { SecureStorage } from './plugins/SecureStorage'; const api: MoodleAppPlugins = { secureStorage: new SecureStorage(), installReferrer: new InstallReferrer(), + diagnostic: new Diagnostic(), }; // This is necessary to work around the default transpilation behavior, diff --git a/cordova-plugin-moodleapp/src/ts/plugins/Diagnostic.ts b/cordova-plugin-moodleapp/src/ts/plugins/Diagnostic.ts new file mode 100644 index 000000000..34c17739d --- /dev/null +++ b/cordova-plugin-moodleapp/src/ts/plugins/Diagnostic.ts @@ -0,0 +1,131 @@ +// (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. +// (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. +// (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. + +/** + * Checks whether device hardware features are enabled or available to the app, e.g. camera, GPS, wifi + */ +export class Diagnostic { + + /** + * Constants for requesting and reporting the various permission states. + */ + declare permissionStatus: typeof permissionStatus; + + /** + * ANDROID ONLY + * "Dangerous" permissions that need to be requested at run-time (Android 6.0/API 23 and above) + * See http://developer.android.com/guide/topics/security/permissions.html#perm-groups + * + */ + declare permission: typeof permission; + + constructor() { + this.permissionStatus = permissionStatus; + this.permission = permission; + } + + isLocationEnabled(): Promise { + return new Promise((resolve, reject) => cordova.exec(resolve, reject, 'Diagnostic', 'isLocationEnabled')); + } + + switchToLocationSettings(): Promise { + return new Promise((resolve, reject) => cordova.exec(resolve, reject, 'Diagnostic', 'switchToLocationSettings')); + } + + 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', 'getLocationAuthorizationStatus')); + } + + requestLocationAuthorization(): Promise { + return new Promise((resolve, reject) => cordova.exec(resolve, reject, 'Diagnostic', 'requestLocationAuthorization')); + } + + requestMicrophoneAuthorization(): Promise { + return new Promise((resolve, reject) => + cordova.exec(resolve, reject, 'Diagnostic', 'requestMicrophoneAuthorization')); + } + +} + +const permission = { + acceptHandover: 'ACCEPT_HANDOVER', + accessBackgroundLocation: 'ACCESS_BACKGROUND_LOCATION', + accessCoarseLocation: 'ACCESS_COARSE_LOCATION', + accessFineLocation: 'ACCESS_FINE_LOCATION', + accessMediaLocation: 'ACCESS_MEDIA_LOCATION', + bodySensors: 'BODY_SENSORS', + bodySensorsBackground: 'BODY_SENSORS_BACKGROUND', + getAccounts: 'GET_ACCOUNTS', + readExternalStorage: 'READ_EXTERNAL_STORAGE', + readMediaAudio: 'READ_MEDIA_AUDIO', + readMediaImages: 'READ_MEDIA_IMAGES', + readMediaVideo: 'READ_MEDIA_VIDEO', + readPhoneState: 'READ_PHONE_STATE', + readSms: 'READ_SMS', + receiveMms: 'RECEIVE_MMS', + receiveSms: 'RECEIVE_SMS', + receiveWapPush: 'RECEIVE_WAP_PUSH', + recordAudio: 'RECORD_AUDIO', + sendSms: 'SEND_SMS', + useSip: 'USE_SIP', + uwbRanging: 'UWB_RANGING', + 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', + grantedWhenInUse: 'authorized_when_in_use', + notRequested: 'not_determined' || 'NOT_REQUESTED', + deniedAlways: 'denied_always' || 'DENIED_ALWAYS', +} as const; diff --git a/cordova-plugin-moodleapp/types/index.d.ts b/cordova-plugin-moodleapp/types/index.d.ts index 9e7c3c6f2..18482e2d5 100644 --- a/cordova-plugin-moodleapp/types/index.d.ts +++ b/cordova-plugin-moodleapp/types/index.d.ts @@ -14,12 +14,14 @@ import { InstallReferrer } from '../src/ts/plugins/InstallReferrer'; import { SecureStorage as SecureStorageImpl } from '../src/ts/plugins/SecureStorage'; +import { Diagnostic } from '../src/ts/plugins/Diagnostic'; declare global { interface MoodleAppPlugins { secureStorage: SecureStorageImpl; installReferrer: InstallReferrer; + diagnostic: Diagnostic; } interface Cordova { diff --git a/package-lock.json b/package-lock.json index 41ee2d719..5566d6bc7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "@awesome-cordova-plugins/clipboard": "^6.6.0", "@awesome-cordova-plugins/core": "^6.6.0", "@awesome-cordova-plugins/device": "^6.6.0", - "@awesome-cordova-plugins/diagnostic": "^6.6.0", "@awesome-cordova-plugins/file": "^6.6.0", "@awesome-cordova-plugins/file-opener": "^6.6.0", "@awesome-cordova-plugins/geolocation": "^6.6.0", @@ -80,7 +79,6 @@ "cordova-plugin-wkuserscript": "^1.0.1", "cordova-plugin-wkwebview-cookies": "^1.0.1", "cordova-sqlite-storage": "^6.1.0", - "cordova.plugins.diagnostic": "^7.1.4", "core-js": "^3.9.1", "es6-promise-plugin": "^4.2.2", "ionicons": "^7.0.0", @@ -2573,22 +2571,6 @@ "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-11.0.3.tgz", "integrity": "sha512-kyuRQ40/NWQVhqGIHq78Ehu2Bf9Mlg0LhmSmis6ZFJK7z933FRfYi8tHe/k/0fB+PGfCf95rJC6TO7dopaFvAg==" }, - "node_modules/@awesome-cordova-plugins/diagnostic": { - "version": "6.6.0", - "license": "MIT", - "dependencies": { - "@types/cordova": "latest" - }, - "peerDependencies": { - "@awesome-cordova-plugins/core": "^6.0.1", - "rxjs": "^5.5.0 || ^6.5.0 || ^7.3.0" - } - }, - "node_modules/@awesome-cordova-plugins/diagnostic/node_modules/@types/cordova": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-11.0.3.tgz", - "integrity": "sha512-kyuRQ40/NWQVhqGIHq78Ehu2Bf9Mlg0LhmSmis6ZFJK7z933FRfYi8tHe/k/0fB+PGfCf95rJC6TO7dopaFvAg==" - }, "node_modules/@awesome-cordova-plugins/file": { "version": "6.6.0", "license": "MIT", @@ -11904,13 +11886,6 @@ "version": "2.0.20", "license": "MIT" }, - "node_modules/colors": { - "version": "1.4.0", - "license": "MIT", - "engines": { - "node": ">=0.1.90" - } - }, "node_modules/combined-stream": { "version": "1.0.8", "license": "MIT", @@ -13003,20 +12978,6 @@ "version": "4.1.0", "license": "MIT" }, - "node_modules/cordova.plugins.diagnostic": { - "version": "7.1.4", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "colors": "1.4.0", - "elementtree": "^0.1.6", - "minimist": "1.2.6" - } - }, - "node_modules/cordova.plugins.diagnostic/node_modules/minimist": { - "version": "1.2.6", - "license": "MIT" - }, "node_modules/cordova/node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", diff --git a/package.json b/package.json index 440f6af04..cea8eeef2 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,6 @@ "@awesome-cordova-plugins/clipboard": "^6.6.0", "@awesome-cordova-plugins/core": "^6.6.0", "@awesome-cordova-plugins/device": "^6.6.0", - "@awesome-cordova-plugins/diagnostic": "^6.6.0", "@awesome-cordova-plugins/file": "^6.6.0", "@awesome-cordova-plugins/file-opener": "^6.6.0", "@awesome-cordova-plugins/geolocation": "^6.6.0", @@ -114,7 +113,6 @@ "cordova-plugin-wkuserscript": "^1.0.1", "cordova-plugin-wkwebview-cookies": "^1.0.1", "cordova-sqlite-storage": "^6.1.0", - "cordova.plugins.diagnostic": "^7.1.4", "core-js": "^3.9.1", "es6-promise-plugin": "^4.2.2", "ionicons": "^7.0.0", @@ -236,11 +234,6 @@ "cordova-plugin-wkuserscript": {}, "cordova-plugin-wkwebview-cookies": {}, "cordova-sqlite-storage": {}, - "cordova.plugins.diagnostic": { - "ANDROID_SUPPORT_VERSION": "28.+", - "ANDROIDX_VERSION": "1.0.0", - "ANDROIDX_APPCOMPAT_VERSION": "1.6.1" - }, "nl.kingsquare.cordova.background-audio": {} } } diff --git a/src/core/features/fileuploader/components/audio-recorder/audio-recorder.component.ts b/src/core/features/fileuploader/components/audio-recorder/audio-recorder.component.ts index 6c8eba582..2c2a3187f 100644 --- a/src/core/features/fileuploader/components/audio-recorder/audio-recorder.component.ts +++ b/src/core/features/fileuploader/components/audio-recorder/audio-recorder.component.ts @@ -15,7 +15,7 @@ import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy } from '@angular/core'; import { CoreModalComponent } from '@classes/modal-component'; import { CorePlatform } from '@services/platform'; -import { Diagnostic, DomSanitizer, Translate } from '@singletons'; +import { DomSanitizer, Translate } from '@singletons'; import { BehaviorSubject, combineLatest, Observable, OperatorFunction } from 'rxjs'; import { Mp3MediaRecorder } from 'mp3-mediarecorder'; import { map, shareReplay, tap } from 'rxjs/operators'; @@ -26,6 +26,7 @@ import { CAPTURE_ERROR_NO_MEDIA_FILES, CoreCaptureError } from '@classes/errors/ import { CoreFileUploaderAudioRecording } from '@features/fileuploader/services/fileuploader'; import { CoreFile, CoreFileProvider } from '@services/file'; import { CorePath } from '@singletons/path'; +import { CoreNative } from '@features/native/services/native'; @Component({ selector: 'core-fileuploader-audio-recorder', @@ -204,17 +205,19 @@ export class CoreFileUploaderAudioRecorderComponent extends CoreModalComponent { - if (!CorePlatform.isMobile()) { + const diagnostic = await CoreNative.plugin('diagnostic')?.getInstance(); + + if (!CorePlatform.isMobile() || !diagnostic) { return; } - const status = await Diagnostic.requestMicrophoneAuthorization(); + const status = await diagnostic.requestMicrophoneAuthorization(); switch (status) { - case Diagnostic.permissionStatus.DENIED_ONCE: - case Diagnostic.permissionStatus.DENIED_ALWAYS: + case diagnostic.permissionStatus.deniedOnce: + case diagnostic.permissionStatus.deniedAlways: throw new Error(Translate.instant('core.fileuploader.microphonepermissiondenied')); - case Diagnostic.permissionStatus.RESTRICTED: + case diagnostic.permissionStatus.restricted: throw new Error(Translate.instant('core.fileuploader.microphonepermissionrestricted')); } } diff --git a/src/core/features/native/native.module.ts b/src/core/features/native/native.module.ts index 102e004f4..39ab66004 100644 --- a/src/core/features/native/native.module.ts +++ b/src/core/features/native/native.module.ts @@ -19,7 +19,6 @@ import { Camera } from '@awesome-cordova-plugins/camera/ngx'; import { Chooser } from '@features/native/plugins/chooser'; import { Clipboard } from '@awesome-cordova-plugins/clipboard/ngx'; import { Device } from '@awesome-cordova-plugins/device/ngx'; -import { Diagnostic } from '@awesome-cordova-plugins/diagnostic/ngx'; import { File } from '@awesome-cordova-plugins/file/ngx'; import { FileOpener } from '@awesome-cordova-plugins/file-opener/ngx'; import { Geolocation } from '@awesome-cordova-plugins/geolocation/ngx'; @@ -43,7 +42,6 @@ export const CORE_NATIVE_SERVICES = [ Chooser, Clipboard, Device, - Diagnostic, File, FileOpener, Geolocation, @@ -68,7 +66,6 @@ export const CORE_NATIVE_SERVICES = [ Camera, Clipboard, Device, - Diagnostic, File, FileOpener, Geolocation, diff --git a/src/core/features/settings/pages/general/general.ts b/src/core/features/settings/pages/general/general.ts index 638b088ef..176a04772 100644 --- a/src/core/features/settings/pages/general/general.ts +++ b/src/core/features/settings/pages/general/general.ts @@ -20,13 +20,14 @@ import { CoreLang } from '@services/lang'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreSettingsHelper, CoreColorScheme, CoreZoomLevel } from '../../services/settings-helper'; import { CoreIframeUtils } from '@services/utils/iframe'; -import { Diagnostic, Translate } from '@singletons'; +import { Translate } from '@singletons'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { AlertButton } from '@ionic/angular'; import { CoreNavigator } from '@services/navigator'; import { CorePlatform } from '@services/platform'; import { CoreAnalytics } from '@services/analytics'; +import { CoreNative } from '@features/native/services/native'; /** * Page that displays the general settings. @@ -263,7 +264,7 @@ export class CoreSettingsGeneralPage { ev.stopPropagation(); ev.preventDefault(); - Diagnostic.switchToSettings(); + CoreNative.plugin('diagnostic')?.switchToSettings(); } } diff --git a/src/core/services/geolocation.ts b/src/core/services/geolocation.ts index c696550f3..2f3df421b 100644 --- a/src/core/services/geolocation.ts +++ b/src/core/services/geolocation.ts @@ -17,12 +17,13 @@ import { Coordinates } from '@awesome-cordova-plugins/geolocation'; import { CoreApp } from '@services/app'; import { CoreAnyError, CoreError } from '@classes/errors/error'; -import { Geolocation, Diagnostic, makeSingleton } from '@singletons'; +import { Geolocation, makeSingleton } from '@singletons'; import { CoreUtils } from './utils/utils'; import { CorePlatform } from './platform'; import { CoreSilentError } from '@classes/errors/silenterror'; import { CoreSubscriptions } from '@singletons/subscriptions'; import { CoreLogger } from '@singletons/logger'; +import { CoreNative } from '@features/native/services/native'; @Injectable({ providedIn: 'root' }) export class CoreGeolocationProvider { @@ -79,7 +80,13 @@ export class CoreGeolocationProvider { * @throws {CoreGeolocationError} */ async enableLocation(): Promise { - let locationEnabled = await Diagnostic.isLocationEnabled(); + const diagnostic = CoreNative.plugin('diagnostic'); + + if (!diagnostic) { + return; + } + + let locationEnabled = await diagnostic.isLocationEnabled(); if (locationEnabled) { // Location is enabled. @@ -87,10 +94,10 @@ export class CoreGeolocationProvider { } if (!CorePlatform.isIOS()) { - Diagnostic.switchToLocationSettings(); + diagnostic.switchToLocationSettings(); await CoreApp.waitForResume(30000); - locationEnabled = await Diagnostic.isLocationEnabled(); + locationEnabled = await diagnostic.isLocationEnabled(); } if (!locationEnabled) { @@ -105,26 +112,34 @@ export class CoreGeolocationProvider { * @throws {CoreGeolocationError} */ protected async doAuthorizeLocation(failOnDeniedOnce: boolean = false): Promise { - const authorizationStatus = await Diagnostic.getLocationAuthorizationStatus(); + const diagnostic = await CoreNative.plugin('diagnostic')?.getInstance(); + + if (!diagnostic) { + return; + } + + const authorizationStatus = await diagnostic.getLocationAuthorizationStatus(); this.logger.log(`Authorize location: status ${authorizationStatus}`); switch (authorizationStatus) { - case Diagnostic.permissionStatus.DENIED_ONCE: + case diagnostic.permissionStatus.deniedOnce: if (failOnDeniedOnce) { throw new CoreGeolocationError(CoreGeolocationErrorReason.PERMISSION_DENIED); } + + case diagnostic.permissionStatus.granted: + case diagnostic.permissionStatus.grantedWhenInUse: + // Location is authorized. + return; + // Fall through. - case Diagnostic.permissionStatus.NOT_REQUESTED: + case diagnostic.permissionStatus.notRequested: this.logger.log('Request location authorization.'); await this.requestLocationAuthorization(); this.logger.log('Location authorization granted.'); await CoreApp.waitForResume(500); await this.doAuthorizeLocation(true); - return; - case Diagnostic.permissionStatus.GRANTED: - case Diagnostic.permissionStatus.GRANTED_WHEN_IN_USE: - // Location is authorized. return; default: throw new CoreGeolocationError(CoreGeolocationErrorReason.PERMISSION_DENIED); @@ -151,7 +166,13 @@ export class CoreGeolocationProvider { * @returns If location can be requested. */ async canRequest(): Promise { - return CoreUtils.promiseWorks(Diagnostic.getLocationAuthorizationStatus()); + const diagnostic = CoreNative.plugin('diagnostic'); + + if (diagnostic) { + return CoreUtils.promiseWorks(diagnostic.getLocationAuthorizationStatus()); + } + + return false; } /** @@ -159,7 +180,7 @@ export class CoreGeolocationProvider { */ protected async requestLocationAuthorization(): Promise { if (!CorePlatform.isIOS()) { - await Diagnostic.requestLocationAuthorization(); + await CoreNative.plugin('diagnostic')?.requestLocationAuthorization(); return; } @@ -168,7 +189,8 @@ export class CoreGeolocationProvider { return new Promise((resolve, reject) => { // Don't display an error if app is sent to the background, just finish the process. const unsubscribe = CoreSubscriptions.once(CorePlatform.pause, () => reject(new CoreSilentError())); - Diagnostic.requestLocationAuthorization().then(() => resolve(), reject).finally(() => unsubscribe()); + CoreNative.plugin('diagnostic')?.requestLocationAuthorization() + .then(() => resolve(), reject).finally(() => unsubscribe()); }); } diff --git a/src/core/services/utils/iframe.ts b/src/core/services/utils/iframe.ts index 0410a2989..6837b2625 100644 --- a/src/core/services/utils/iframe.ts +++ b/src/core/services/utils/iframe.ts @@ -24,7 +24,7 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; -import { makeSingleton, NgZone, Translate, Diagnostic } from '@singletons'; +import { makeSingleton, NgZone, Translate } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreUrl } from '@singletons/url'; import { CoreWindow } from '@singletons/window'; @@ -36,6 +36,7 @@ import { FrameElement } from '@classes/element-controllers/FrameElementControlle import { CoreMimetypeUtils } from './mimetype'; import { CoreFilepool } from '@services/filepool'; import { CoreSite } from '@classes/sites/site'; +import { CoreNative } from '@features/native/services/native'; type CoreFrameElement = FrameElement & { window?: Window; @@ -627,7 +628,7 @@ export class CoreIframeUtilsProvider { { text: Translate.instant('core.opensettings'), handler: (): void => { - Diagnostic.switchToSettings(); + CoreNative.plugin('diagnostic')?.switchToSettings(); }, }, ], diff --git a/src/core/singletons/index.ts b/src/core/singletons/index.ts index 1bef7dd7d..eaba8d516 100644 --- a/src/core/singletons/index.ts +++ b/src/core/singletons/index.ts @@ -40,7 +40,6 @@ import { import { Badge as BadgeService } from '@awesome-cordova-plugins/badge/ngx'; import { Camera as CameraService } from '@awesome-cordova-plugins/camera/ngx'; import { Clipboard as ClipboardService } from '@awesome-cordova-plugins/clipboard/ngx'; -import { Diagnostic as DiagnosticService } from '@awesome-cordova-plugins/diagnostic/ngx'; import { Device as DeviceService } from '@awesome-cordova-plugins/device/ngx'; import { File as FileService } from '@awesome-cordova-plugins/file/ngx'; import { FileOpener as FileOpenerService } from '@awesome-cordova-plugins/file-opener/ngx'; @@ -171,7 +170,6 @@ export function makeSingleton( // Convert ionic-native services to singleton. export const Badge = makeSingleton(BadgeService); export const Clipboard = makeSingleton(ClipboardService); -export const Diagnostic = makeSingleton(DiagnosticService); export const File = makeSingleton(FileService); export const FileOpener = makeSingleton(FileOpenerService); export const Geolocation = makeSingleton(GeolocationService);