diff --git a/src/app/app.scss b/src/app/app.scss
index b384030c3..064b7bfdb 100644
--- a/src/app/app.scss
+++ b/src/app/app.scss
@@ -1275,3 +1275,17 @@ ion-app.app-root {
}
}
}
+
+// QR scan. The scanner is at the background of the app, we need to hide the elements that overlay it.
+.core-scanning-qr {
+ ion-app.app-root {
+ background-color: transparent;
+
+ .ion-page {
+ background-color: transparent;
+ }
+ ion-content, ion-backdrop, ion-modal:not(.core-modal-fullscreen), core-ion-tabs {
+ display: none;
+ }
+ }
+}
diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json
index 7f19738e8..193426e84 100644
--- a/src/assets/lang/en.json
+++ b/src/assets/lang/en.json
@@ -1868,6 +1868,7 @@
"core.previous": "Previous",
"core.proceed": "Proceed",
"core.pulltorefresh": "Pull to refresh",
+ "core.qrscanner": "QR scanner",
"core.question.answer": "Answer",
"core.question.answersaved": "Answer saved",
"core.question.cannotdeterminestatus": "Cannot determine status",
@@ -1910,6 +1911,7 @@
"core.retry": "Retry",
"core.save": "Save",
"core.savechanges": "Save changes",
+ "core.scanqr": "Scan QR code",
"core.search": "Search",
"core.searching": "Searching",
"core.searchresults": "Search results",
diff --git a/src/core/mainmenu/pages/more/more.html b/src/core/mainmenu/pages/more/more.html
index d2d3908d1..4b2949e7a 100644
--- a/src/core/mainmenu/pages/more/more.html
+++ b/src/core/mainmenu/pages/more/more.html
@@ -31,6 +31,10 @@
{{item.label}}
+
+
+ {{ 'core.scanqr' | translate }}
+
{{ 'core.mainmenu.website' | translate }}
diff --git a/src/core/mainmenu/pages/more/more.ts b/src/core/mainmenu/pages/more/more.ts
index 864402eef..ebcab3684 100644
--- a/src/core/mainmenu/pages/more/more.ts
+++ b/src/core/mainmenu/pages/more/more.ts
@@ -16,9 +16,13 @@ import { Component, OnDestroy } from '@angular/core';
import { IonicPage, NavController } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
+import { CoreTextUtilsProvider } from '@providers/utils/text';
+import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreMainMenuDelegate, CoreMainMenuHandlerData } from '../../providers/delegate';
import { CoreMainMenuProvider, CoreMainMenuCustomItem } from '../../providers/mainmenu';
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
+import { TranslateService } from '@ngx-translate/core';
/**
* Page that displays the list of main menu options that aren't in the tabs.
@@ -35,6 +39,7 @@ export class CoreMainMenuMorePage implements OnDestroy {
siteInfo: any;
siteName: string;
logoutLabel: string;
+ showScanQR: boolean;
showWeb: boolean;
showHelp: boolean;
docsUrl: string;
@@ -45,14 +50,22 @@ export class CoreMainMenuMorePage implements OnDestroy {
protected langObserver;
protected updateSiteObserver;
- constructor(private menuDelegate: CoreMainMenuDelegate, private sitesProvider: CoreSitesProvider,
- private navCtrl: NavController, private mainMenuProvider: CoreMainMenuProvider,
- eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider) {
+ constructor(private menuDelegate: CoreMainMenuDelegate,
+ private sitesProvider: CoreSitesProvider,
+ private navCtrl: NavController,
+ private mainMenuProvider: CoreMainMenuProvider,
+ eventsProvider: CoreEventsProvider,
+ private loginHelper: CoreLoginHelperProvider,
+ private utils: CoreUtilsProvider,
+ private linkHelper: CoreContentLinksHelperProvider,
+ private textUtils: CoreTextUtilsProvider,
+ private translate: TranslateService) {
this.langObserver = eventsProvider.on(CoreEventsProvider.LANGUAGE_CHANGED, this.loadSiteInfo.bind(this));
this.updateSiteObserver = eventsProvider.on(CoreEventsProvider.SITE_UPDATED, this.loadSiteInfo.bind(this),
sitesProvider.getCurrentSiteId());
this.loadSiteInfo();
+ this.showScanQR = this.utils.canScanQR();
}
/**
@@ -155,6 +168,30 @@ export class CoreMainMenuMorePage implements OnDestroy {
this.navCtrl.push('CoreSitePreferencesPage');
}
+ /**
+ * Scan and treat a QR code.
+ */
+ scanQR(): void {
+ // Scan for a QR code.
+ this.utils.scanQR().then((text) => {
+ if (text) {
+ // Check if it's a URL. We basically check it has a protocol and doesn't include any space.
+ if (/^[^:]{2,}:\/\/[^ ]+$/i.test(text)) {
+ // Check if the app can handle the URL.
+ this.linkHelper.handleLink(text, undefined, this.navCtrl, true, true).then((treated) => {
+ if (!treated) {
+ // Can't handle it, open it in browser.
+ this.sitesProvider.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(text);
+ }
+ });
+ } else {
+ // It's not a URL, open it in a modal so the user can see it and copy it.
+ this.textUtils.expandText(this.translate.instant('core.qrscanner'), text);
+ }
+ }
+ });
+ }
+
/**
* Logout the user.
*/
diff --git a/src/core/viewer/pages/qr-scanner/qr-scanner.html b/src/core/viewer/pages/qr-scanner/qr-scanner.html
new file mode 100644
index 000000000..dac2e266c
--- /dev/null
+++ b/src/core/viewer/pages/qr-scanner/qr-scanner.html
@@ -0,0 +1,13 @@
+
+
+ {{ title }}
+
+
+
+
+
+
+
+
diff --git a/src/core/viewer/pages/qr-scanner/qr-scanner.module.ts b/src/core/viewer/pages/qr-scanner/qr-scanner.module.ts
new file mode 100644
index 000000000..db7f83fa7
--- /dev/null
+++ b/src/core/viewer/pages/qr-scanner/qr-scanner.module.ts
@@ -0,0 +1,31 @@
+// (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 { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreViewerQRScannerPage } from './qr-scanner';
+import { CoreDirectivesModule } from '@directives/directives.module';
+
+@NgModule({
+ declarations: [
+ CoreViewerQRScannerPage
+ ],
+ imports: [
+ CoreDirectivesModule,
+ IonicPageModule.forChild(CoreViewerQRScannerPage),
+ TranslateModule.forChild()
+ ]
+})
+export class CoreViewerQRScannerPageModule {}
diff --git a/src/core/viewer/pages/qr-scanner/qr-scanner.ts b/src/core/viewer/pages/qr-scanner/qr-scanner.ts
new file mode 100644
index 000000000..49a2060e7
--- /dev/null
+++ b/src/core/viewer/pages/qr-scanner/qr-scanner.ts
@@ -0,0 +1,79 @@
+// (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 { Component } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { IonicPage, ViewController, NavParams } from 'ionic-angular';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+
+/**
+ * Page to scan a QR code.
+ */
+@IonicPage({ segment: 'core-viewer-qr-scanner' })
+@Component({
+ selector: 'page-core-viewer-qr-scanner',
+ templateUrl: 'qr-scanner.html',
+})
+export class CoreViewerQRScannerPage {
+ title: string; // Page title.
+
+ constructor(params: NavParams,
+ translate: TranslateService,
+ protected viewCtrl: ViewController,
+ protected domUtils: CoreDomUtilsProvider,
+ protected utils: CoreUtilsProvider) {
+
+ this.title = params.get('title') || translate.instant('core.scanqr');
+
+ this.utils.startScanQR().then((text) => {
+ // Text captured, return it.
+ text = typeof text == 'string' ? text.trim() : '';
+
+ this.closeModal(text);
+ }).catch((error) => {
+ if (!error.coreCanceled) {
+ // Show error and stop scanning.
+ this.domUtils.showErrorModalDefault(error, 'An error occurred.');
+ this.utils.stopScanQR();
+ }
+
+ this.closeModal();
+ });
+ }
+
+ /**
+ * Cancel scanning.
+ */
+ cancel(): void {
+ this.utils.stopScanQR();
+ }
+
+ /**
+ * Close modal.
+ *
+ * @param text The text to return (if any).
+ */
+ closeModal(text?: string): void {
+ this.viewCtrl.dismiss(text);
+ }
+
+ /**
+ * View will leave.
+ */
+ ionViewWillLeave(): void {
+ // If this code is reached and scan hasn't been stopped yet it means the user clicked the back button, cancel.
+ this.utils.stopScanQR();
+ }
+}
diff --git a/src/lang/en.json b/src/lang/en.json
index 7764553bb..cc4d99e42 100644
--- a/src/lang/en.json
+++ b/src/lang/en.json
@@ -209,6 +209,7 @@
"previous": "Previous",
"proceed": "Proceed",
"pulltorefresh": "Pull to refresh",
+ "qrscanner": "QR scanner",
"quotausage": "You have currently used {{$a.used}} of your {{$a.total}} limit.",
"redirectingtosite": "You will be redirected to the site.",
"refresh": "Refresh",
@@ -223,6 +224,7 @@
"retry": "Retry",
"save": "Save",
"savechanges": "Save changes",
+ "scanqr": "Scan QR code",
"search": "Search",
"searching": "Searching",
"searchresults": "Search results",
diff --git a/src/providers/utils/utils.ts b/src/providers/utils/utils.ts
index b3781a706..49d562d18 100644
--- a/src/providers/utils/utils.ts
+++ b/src/providers/utils/utils.ts
@@ -13,11 +13,12 @@
// limitations under the License.
import { Injectable, NgZone } from '@angular/core';
-import { Platform } from 'ionic-angular';
+import { Platform, ModalController } from 'ionic-angular';
import { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser';
import { Clipboard } from '@ionic-native/clipboard';
import { FileOpener } from '@ionic-native/file-opener';
import { WebIntent } from '@ionic-native/web-intent';
+import { QRScanner } from '@ionic-native/qr-scanner';
import { CoreAppProvider } from '../app';
import { CoreDomUtilsProvider } from './dom';
import { CoreMimetypeUtilsProvider } from './mimetype';
@@ -28,6 +29,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreLangProvider } from '../lang';
import { CoreWSProvider, CoreWSError } from '../ws';
import { CoreFile } from '../file';
+import { Subscription } from 'rxjs';
import { makeSingleton } from '@singletons/core.singletons';
/**
@@ -63,12 +65,25 @@ export class CoreUtilsProvider {
protected logger;
protected iabInstance: InAppBrowserObject;
protected uniqueIds: {[name: string]: number} = {};
+ protected qrScanData: {deferred: PromiseDefer, observable: Subscription};
- constructor(private iab: InAppBrowser, private appProvider: CoreAppProvider, private clipboard: Clipboard,
- private domUtils: CoreDomUtilsProvider, logger: CoreLoggerProvider, private translate: TranslateService,
- private platform: Platform, private langProvider: CoreLangProvider, private eventsProvider: CoreEventsProvider,
- private fileOpener: FileOpener, private mimetypeUtils: CoreMimetypeUtilsProvider, private webIntent: WebIntent,
- private wsProvider: CoreWSProvider, private zone: NgZone, private textUtils: CoreTextUtilsProvider) {
+ constructor(protected iab: InAppBrowser,
+ protected appProvider: CoreAppProvider,
+ protected clipboard: Clipboard,
+ protected domUtils: CoreDomUtilsProvider,
+ logger: CoreLoggerProvider,
+ protected translate: TranslateService,
+ protected platform: Platform,
+ protected langProvider: CoreLangProvider,
+ protected eventsProvider: CoreEventsProvider,
+ protected fileOpener: FileOpener,
+ protected mimetypeUtils: CoreMimetypeUtilsProvider,
+ protected webIntent: WebIntent,
+ protected wsProvider: CoreWSProvider,
+ protected zone: NgZone,
+ protected textUtils: CoreTextUtilsProvider,
+ protected modalCtrl: ModalController,
+ protected qrScanner: QRScanner) {
this.logger = logger.getInstance('CoreUtilsProvider');
}
@@ -1420,6 +1435,117 @@ export class CoreUtilsProvider {
return debounced;
}
+
+ /**
+ * Check whether the app can scan QR codes.
+ *
+ * @return Whether the app can scan QR codes.
+ */
+ canScanQR(): boolean {
+ return this.appProvider.isMobile();
+ }
+
+ /**
+ * Open a modal to scan a QR code.
+ *
+ * @param title Title of the modal. Defaults to "QR reader".
+ * @return Promise resolved with the captured text or undefined if cancelled or error.
+ */
+ scanQR(title?: string): Promise {
+ return new Promise((resolve, reject): void => {
+ const modal = this.modalCtrl.create('CoreViewerQRScannerPage', {
+ title: title
+ }, { cssClass: 'core-modal-fullscreen'});
+
+ modal.present();
+
+ modal.onDidDismiss((data) => {
+ resolve(data);
+ });
+ });
+ }
+
+ /**
+ * Start scanning for a QR code.
+ *
+ * @return Promise resolved with the QR string, rejected if error or cancelled.
+ */
+ startScanQR(): Promise {
+ if (!this.appProvider.isMobile()) {
+ return Promise.reject('QRScanner isn\'t available in desktop apps.');
+ }
+
+ // Ask the user for permission to use the camera.
+ // The scan method also does this, but since it returns an Observable we wouldn't be able to detect if the user denied.
+ return this.qrScanner.prepare().then((status) => {
+
+ if (!status.authorized) {
+ // No access to the camera, reject. In android this shouldn't happen, denying access passes through catch.
+ return Promise.reject('The user denied camera access.');
+ }
+
+ if (this.qrScanData && this.qrScanData.deferred) {
+ // Already scanning.
+ return this.qrScanData.deferred.promise;
+ }
+
+ // Start scanning.
+ this.qrScanData = {
+ deferred: this.promiseDefer(),
+ observable: this.qrScanner.scan().subscribe((text) => {
+
+ // Text received, stop scanning and return the text.
+ this.stopScanQR(text, false);
+ })
+ };
+
+ // Show the camera.
+ return this.qrScanner.show().then(() => {
+ document.body.classList.add('core-scanning-qr');
+
+ return this.qrScanData.deferred.promise;
+ }, (err) => {
+ this.stopScanQR(err, true);
+
+ return Promise.reject(err);
+ });
+
+ }).catch((err) => {
+ err.message = err.message || err._message;
+
+ return Promise.reject(err);
+ });
+ }
+
+ /**
+ * Stop scanning for QR code. If no param is provided, the app will consider the user cancelled.
+ *
+ * @param data If success, the text of the QR code. If error, the error object or message. Undefined for cancelled.
+ * @param error True if the data belongs to an error, false otherwise.
+ */
+ stopScanQR(data?: any, error?: boolean): void {
+
+ if (!this.qrScanData) {
+ // Not scanning.
+ return;
+ }
+
+ // Hide camera preview.
+ document.body.classList.remove('core-scanning-qr');
+ this.qrScanner.hide();
+
+ this.qrScanData.observable.unsubscribe(); // Stop scanning.
+
+ if (error) {
+ this.qrScanData.deferred.reject(data);
+ } else if (typeof data != 'undefined') {
+ this.qrScanData.deferred.resolve(data);
+ } else {
+ this.qrScanData.deferred.reject({coreCanceled: true});
+ }
+
+ delete this.qrScanData;
+ }
}
export class CoreUtils extends makeSingleton(CoreUtilsProvider) {}