From 4a423f9cd3e600533eab135b22ae0e7a9d7bf8b6 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Fri, 8 Dec 2017 13:45:30 +0100 Subject: [PATCH] MOBILE-2253 emulator: Mock InAppBrowser --- .../emulator/classes/inappbrowserobject.ts | 253 ++++++++++++++++++ src/core/emulator/emulator.module.ts | 12 +- src/core/emulator/providers/helper.ts | 59 +--- src/core/emulator/providers/inappbrowser.ts | 48 ++++ .../emulator/providers/local-notifications.ts | 5 +- src/core/login/providers/helper.ts | 7 +- src/providers/app.ts | 54 ++++ 7 files changed, 372 insertions(+), 66 deletions(-) create mode 100644 src/core/emulator/classes/inappbrowserobject.ts create mode 100644 src/core/emulator/providers/inappbrowser.ts diff --git a/src/core/emulator/classes/inappbrowserobject.ts b/src/core/emulator/classes/inappbrowserobject.ts new file mode 100644 index 000000000..c680f6606 --- /dev/null +++ b/src/core/emulator/classes/inappbrowserobject.ts @@ -0,0 +1,253 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { CoreAppProvider } from '../../../providers/app'; +import { CoreFileProvider } from '../../../providers/file'; +import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; +import { Observable, Observer } from 'rxjs'; +import { InAppBrowserObject, InAppBrowserEvent } from '@ionic-native/in-app-browser'; + +/** + * Emulates the Cordova InAppBrowserObject in desktop apps. + */ +export class InAppBrowserObjectMock extends InAppBrowserObject { + protected window; + protected browserWindow; + protected screen; + protected isSSO: boolean; + protected isLinux: boolean; + + constructor(appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, private urlUtils: CoreUrlUtilsProvider, + private url: string, target?: string, options = '') { + super(url, target, options); + + if (!appProvider.isDesktop()) { + // This plugin is only supported in desktop. + return; + } + + this.browserWindow = require('electron').remote.BrowserWindow; + this.screen = require('electron').screen; + this.isSSO = !!(url && url.match(/\/launch\.php\?service=.+&passport=/)); + this.isLinux = appProvider.isLinux(); + + let width = 800, + height = 600, + display, + bwOptions: any = {}; + + if (screen) { + display = this.screen.getPrimaryDisplay(); + if (display && display.workArea) { + width = display.workArea.width || width; + height = display.workArea.height || height; + } + } + + // Create the BrowserWindow options based on the received options. + bwOptions.width = width; + bwOptions.height = height; + if (options.indexOf('hidden=yes') != -1) { + bwOptions.show = false; + } + if (options.indexOf('location=no') != -1) { + bwOptions.frame = false; + } + if (options.indexOf('fullscreen=yes') != -1) { + bwOptions.fullscreen = true; + } + + this.window = new this.browserWindow(bwOptions); + this.window.loadURL(url); + + if (this.isLinux && this.isSSO) { + // SSO in Linux. Simulate it's an iOS device so we can retrieve the launch URL. + // This is needed because custom URL scheme is not supported in Linux. + let userAgent = 'Mozilla/5.0 (iPad) AppleWebKit/603.3.8 (KHTML, like Gecko) Mobile/14G60'; + this.window.webContents.setUserAgent(userAgent); + } + } + + /** + * Close the window. + */ + close() : void { + this.window.close(); + } + + /** + * Execute a JS script. + * + * @param {any} details Details of the script to run, specifying either a file or code key. + * @return {Promise} Promise resolved when done. + */ + executeScript(details: any) : Promise { + return new Promise((resolve, reject) => { + if (details.code) { + this.window.webContents.executeJavaScript(details.code, false, resolve); + } else if (details.file) { + this.fileProvider.readFile(details.file).then((code) => { + this.window.webContents.executeJavaScript(code, false, resolve); + }).catch(reject); + } else { + reject('executeScript requires exactly one of code or file to be specified'); + } + }); + } + + /** + * Recursive function to get the launch URL from the contents of a BrowserWindow. + * + * @param {number} [retry=0] Retry number. + * @return {Promise} Promise resolved with the launch URL. + */ + protected getLaunchUrl(retry = 0) : Promise { + return new Promise((resolve, reject) => { + // Execute Javascript to retrieve the launch link. + let jsCode = 'var el = document.querySelector("#launchapp"); el && el.href;', + found = false; + + this.window.webContents.executeJavaScript(jsCode).then((launchUrl) => { + found = true; + resolve(launchUrl); + }); + + setTimeout(() => { + if (found) { + // URL found, stop. + } else if (retry > 5) { + // Waited enough, stop. + reject(); + } else { + this.getLaunchUrl(retry + 1).then(resolve, reject); + } + }, 300); + + }); + } + + /** + * Hide the window. + */ + hide() : void { + this.window.hide(); + } + + /** + * Insert CSS. + * + * @param {any} details Details of the CSS to insert, specifying either a file or code key. + * @return {Promise} Promise resolved when done. + */ + insertCSS(details: any) : Promise { + return new Promise((resolve, reject) => { + if (details.code) { + this.window.webContents.insertCSS(details.code); + resolve(); + } else if (details.file) { + this.fileProvider.readFile(details.file).then((code) => { + this.window.webContents.insertCSS(code); + resolve(); + }).catch(reject); + } else { + reject('insertCSS requires exactly one of code or file to be specified'); + } + }); + } + + /** + * Listen to events happening. + * + * @param {string} name Name of the event. + * @return {Observable} Observable that will listen to the event on subscribe, and will stop listening + * to the event on unsubscribe. + */ + on(name: string) : Observable { + // Create the observable. + return new Observable((observer: Observer) => { + // Helper functions to handle events. + let received = (event, url) => { + try { + event.url = url || this.window.getURL(); + event.type = name; + observer.next(event); + } catch(ex) {} + }, + finishLoad = (event) => { + // Check if user is back to launch page. + if (this.urlUtils.removeUrlParams(this.url) == this.urlUtils.removeUrlParams(this.window.getURL())) { + // The launch page was loaded. Search for the launch link. + this.getLaunchUrl().then((launchUrl) => { + if (launchUrl) { + // Launch URL retrieved, send it and stop listening. + received(event, launchUrl); + } + }); + } + }; + + switch (name) { + case 'loadstart': + this.window.webContents.on('did-start-loading', received); + + if (this.isLinux && this.isSSO) { + // Linux doesn't support custom URL Schemes. Check if launch page is loaded. + // listeners[callback].push(finishLoad); + this.window.webContents.on('did-finish-load', finishLoad); + } + break; + + case 'loadstop': + this.window.webContents.on('did-finish-load', received); + break; + + case 'loaderror': + this.window.webContents.on('did-fail-load', received); + break; + case 'exit': + this.window.on('close', received); + break; + } + + return () => { + // Unsubscribing. We need to remove the listeners. + switch (name) { + case 'loadstart': + this.window.webContents.removeListener('did-start-loading', received); + this.window.webContents.removeListener('did-finish-load', finishLoad); + break; + + case 'loadstop': + this.window.webContents.removeListener('did-finish-load', received); + break; + + case 'loaderror': + this.window.webContents.removeListener('did-fail-load', received); + break; + + case 'exit': + this.window.removeListener('close', received); + break; + } + }; + }); + } + + /** + * Show the window. + */ + show() : void { + this.window.show(); + } +} diff --git a/src/core/emulator/emulator.module.ts b/src/core/emulator/emulator.module.ts index ff665cdda..0e18b1e1f 100644 --- a/src/core/emulator/emulator.module.ts +++ b/src/core/emulator/emulator.module.ts @@ -19,6 +19,7 @@ import { Clipboard } from '@ionic-native/clipboard'; import { File } from '@ionic-native/file'; import { FileTransfer } from '@ionic-native/file-transfer'; import { Globalization } from '@ionic-native/globalization'; +import { InAppBrowser } from '@ionic-native/in-app-browser'; import { LocalNotifications } from '@ionic-native/local-notifications'; import { Network } from '@ionic-native/network'; import { Zip } from '@ionic-native/zip'; @@ -27,16 +28,17 @@ import { ClipboardMock } from './providers/clipboard'; import { FileMock } from './providers/file'; import { FileTransferMock } from './providers/file-transfer'; import { GlobalizationMock } from './providers/globalization'; +import { InAppBrowserMock } from './providers/inappbrowser'; import { LocalNotificationsMock } from './providers/local-notifications'; import { NetworkMock } from './providers/network'; import { ZipMock } from './providers/zip'; -import { InAppBrowser } from '@ionic-native/in-app-browser'; import { CoreEmulatorHelperProvider } from './providers/helper'; import { CoreAppProvider } from '../../providers/app'; import { CoreFileProvider } from '../../providers/file'; import { CoreTextUtilsProvider } from '../../providers/utils/text'; import { CoreMimetypeUtilsProvider } from '../../providers/utils/mimetype'; +import { CoreUrlUtilsProvider } from '../../providers/utils/url'; import { CoreUtilsProvider } from '../../providers/utils/utils'; import { CoreInitDelegate } from '../../providers/init'; @@ -77,6 +79,13 @@ import { CoreInitDelegate } from '../../providers/init'; return appProvider.isMobile() ? new Globalization() : new GlobalizationMock(appProvider); } }, + { + provide: InAppBrowser, + deps: [CoreAppProvider, CoreFileProvider, CoreUrlUtilsProvider], + useFactory: (appProvider: CoreAppProvider, fileProvider: CoreFileProvider, urlUtils: CoreUrlUtilsProvider) => { + return !appProvider.isDesktop() ? new InAppBrowser() : new InAppBrowserMock(appProvider, fileProvider, urlUtils); + } + }, { provide: LocalNotifications, deps: [CoreAppProvider, CoreUtilsProvider], @@ -101,7 +110,6 @@ import { CoreInitDelegate } from '../../providers/init'; return appProvider.isMobile() ? new Zip() : new ZipMock(file, mimeUtils); } }, - InAppBrowser ] }) export class CoreEmulatorModule { diff --git a/src/core/emulator/providers/helper.ts b/src/core/emulator/providers/helper.ts index 3c333b25d..abfd542fa 100644 --- a/src/core/emulator/providers/helper.ts +++ b/src/core/emulator/providers/helper.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreAppProvider } from '../../../providers/app'; import { CoreFileProvider } from '../../../providers/file'; import { CoreUtilsProvider } from '../../../providers/utils/utils'; import { File } from '@ionic-native/file'; @@ -31,61 +30,7 @@ export class CoreEmulatorHelperProvider implements CoreInitHandler { blocking = true; constructor(private file: File, private fileProvider: CoreFileProvider, private utils: CoreUtilsProvider, - initDelegate: CoreInitDelegate, private localNotif: LocalNotifications, private appProvider: CoreAppProvider) {} - - /** - * Check if the app is running in a Linux environment. - * - * @return {boolean} Whether it's running in a Linux environment. - */ - isLinux() : boolean { - if (!this.appProvider.isDesktop()) { - return false; - } - - try { - var os = require('os'); - return os.platform().indexOf('linux') === 0; - } catch(ex) { - return false; - } - } - - /** - * Check if the app is running in a Mac OS environment. - * - * @return {boolean} Whether it's running in a Mac OS environment. - */ - isMac() : boolean { - if (!this.appProvider.isDesktop()) { - return false; - } - - try { - var os = require('os'); - return os.platform().indexOf('darwin') === 0; - } catch(ex) { - return false; - } - } - - /** - * Check if the app is running in a Windows environment. - * - * @return {boolean} Whether it's running in a Windows environment. - */ - isWindows() : boolean { - if (!this.appProvider.isDesktop()) { - return false; - } - - try { - var os = require('os'); - return os.platform().indexOf('win') === 0; - } catch(ex) { - return false; - } - } + initDelegate: CoreInitDelegate, private localNotif: LocalNotifications) {} /** * Load the Mocks that need it. @@ -98,7 +43,7 @@ export class CoreEmulatorHelperProvider implements CoreInitHandler { promises.push((this.file).load().then((basePath: string) => { this.fileProvider.setHTMLBasePath(basePath); })); - promises.push((this.localNotif).load(this.isWindows())); + promises.push((this.localNotif).load()); (window).FileTransferError = FileTransferErrorMock; diff --git a/src/core/emulator/providers/inappbrowser.ts b/src/core/emulator/providers/inappbrowser.ts new file mode 100644 index 000000000..1aa83f800 --- /dev/null +++ b/src/core/emulator/providers/inappbrowser.ts @@ -0,0 +1,48 @@ +// (C) Copyright 2015 Martin Dougiamas +// +// 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 { InAppBrowser, InAppBrowserObject } from '@ionic-native/in-app-browser'; +import { CoreAppProvider } from '../../../providers/app'; +import { CoreFileProvider } from '../../../providers/file'; +import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; +import { InAppBrowserObjectMock } from '../classes/inappbrowserobject'; + +/** + * Emulates the Cordova InAppBrowser plugin in desktop apps. + */ +@Injectable() +export class InAppBrowserMock extends InAppBrowser { + + constructor(private appProvider: CoreAppProvider, private fileProvider: CoreFileProvider, + private urlUtils: CoreUrlUtilsProvider) { + super(); + } + + /** + * Opens a URL in a new InAppBrowser instance, the current browser instance, or the system browser. + * + * @param {string} url The URL to load. + * @param {string} [target] The target in which to load the URL, an optional parameter that defaults to _self. + * @param {string} [options] Options for the InAppBrowser. + * @return {InAppBrowserObject} The new instance. + */ + create(url: string, target?: string, options = '') : InAppBrowserObject { + if (!this.appProvider.isDesktop()) { + return super.create(url, target, options); + } + + return new InAppBrowserObjectMock(this.appProvider, this.fileProvider, this.urlUtils, url, target, options); + } +} diff --git a/src/core/emulator/providers/local-notifications.ts b/src/core/emulator/providers/local-notifications.ts index 209788430..35c1b6561 100644 --- a/src/core/emulator/providers/local-notifications.ts +++ b/src/core/emulator/providers/local-notifications.ts @@ -436,15 +436,14 @@ export class LocalNotificationsMock extends LocalNotifications { /** * Loads an initialize the API for desktop. * - * @param {boolean} isWindows Whether the app is running in a Windows environment. * @return {Promise} Promise resolved when done. */ - load(isWindows: boolean) : Promise { + load() : Promise { if (!this.appProvider.isDesktop()) { return Promise.resolve(); } - if (isWindows) { + if (this.appProvider.isWindows()) { try { this.winNotif = require('electron-windows-notifications'); } catch(ex) {} diff --git a/src/core/login/providers/helper.ts b/src/core/login/providers/helper.ts index 388d9e449..13e18440a 100644 --- a/src/core/login/providers/helper.ts +++ b/src/core/login/providers/helper.ts @@ -28,7 +28,6 @@ import { CoreUrlUtilsProvider } from '../../../providers/utils/url'; import { CoreUtilsProvider } from '../../../providers/utils/utils'; import { CoreConfigConstants } from '../../../configconstants'; import { CoreConstants } from '../../constants'; -import { CoreEmulatorHelperProvider } from '../../emulator/providers/helper'; import { Md5 } from 'ts-md5/dist/md5'; export interface CoreLoginSSOData { @@ -55,7 +54,7 @@ export class CoreLoginHelperProvider { private wsProvider: CoreWSProvider, private translate: TranslateService, private textUtils: CoreTextUtilsProvider, private eventsProvider: CoreEventsProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider,private configProvider: CoreConfigProvider, private platform: Platform, - private emulatorHelper: CoreEmulatorHelperProvider, private initDelegate: CoreInitDelegate) { + private initDelegate: CoreInitDelegate) { this.logger = logger.getInstance('CoreLoginHelper'); } @@ -591,7 +590,7 @@ export class CoreLoginHelperProvider { * @return {boolean} True if embedded browser, false othwerise. */ isSSOEmbeddedBrowser(code: number) : boolean { - if (this.appProvider.isDesktop() && this.emulatorHelper.isLinux()) { + if (this.appProvider.isLinux()) { // In Linux desktop apps, always use embedded browser. return true; } @@ -635,7 +634,7 @@ export class CoreLoginHelperProvider { loginUrl += '&oauthsso=' + params.id; - if (this.appProvider.isDesktop() && this.emulatorHelper.isLinux()) { + if (this.appProvider.isLinux()) { // In Linux desktop apps, always use embedded browser. this.utils.openInApp(loginUrl); } else { diff --git a/src/providers/app.ts b/src/providers/app.ts index 921781a5a..6a546eed9 100644 --- a/src/providers/app.ts +++ b/src/providers/app.ts @@ -115,6 +115,42 @@ export class CoreAppProvider { return this.isKeyboardShown; }; + /** + * Check if the app is running in a Linux environment. + * + * @return {boolean} Whether it's running in a Linux environment. + */ + isLinux() : boolean { + if (!this.isDesktop()) { + return false; + } + + try { + var os = require('os'); + return os.platform().indexOf('linux') === 0; + } catch(ex) { + return false; + } + } + + /** + * Check if the app is running in a Mac OS environment. + * + * @return {boolean} Whether it's running in a Mac OS environment. + */ + isMac() : boolean { + if (!this.isDesktop()) { + return false; + } + + try { + var os = require('os'); + return os.platform().indexOf('darwin') === 0; + } catch(ex) { + return false; + } + } + /** * Checks if the app is running in a mobile or tablet device (Cordova). * @@ -154,6 +190,24 @@ export class CoreAppProvider { return limited.indexOf(type) > -1; }; + /** + * Check if the app is running in a Windows environment. + * + * @return {boolean} Whether it's running in a Windows environment. + */ + isWindows() : boolean { + if (!this.isDesktop()) { + return false; + } + + try { + var os = require('os'); + return os.platform().indexOf('win') === 0; + } catch(ex) { + return false; + } + } + /** * Open the keyboard. */