'use strict'; /* global applications, BrowserConfigHelper, AppWindow, Service */ /* jshint nonew: false */ (function(exports) { /** * AppWindowFactory handle the launch request from gecko and * wrap the config with properer parameters. * * If gecko is asking us to open a webapp, * AppWindowFactory would do the instantiation and let * AppWindowManager to do the following app opening control via * event launchapp. * * If gecko is asking us to open an inline activity page, * AppWindowFactory would wrap the configuration and sent it to * AppWindowFactory for it to do instantiation via event * launchactivity. * * ![app and activity launch flow](http://i.imgur.com/ZyMcgft.png) * * @class AppWindowFactory */ function AppWindowFactory() { this.preHandleEvent = this.preHandleEvent.bind(this); } AppWindowFactory.prototype = { name: 'AppWindowFactory', /** * Indicate whether this class is started or not. * @access private * @type {Boolean} * @memberof AppWindowFactory.prototype */ _started: false, /** * Register all event handlers. * @memberof AppWindowFactory.prototype */ start: function awf_start() { if (this._started) { return; } this._started = true; window.addEventListener('webapps-launch', this.preHandleEvent); window.addEventListener('webapps-close', this.preHandleEvent); window.addEventListener('open-app', this.preHandleEvent); window.addEventListener('openwindow', this.preHandleEvent); window.addEventListener('appopenwindow', this.preHandleEvent); window.addEventListener('applicationready', (function appReady(e) { window.removeEventListener('applicationready', appReady); this._handlePendingEvents(); }).bind(this)); Service.registerState('isLaunchingWindow', this); }, /** * Unregister all event handlers. * @memberof AppWindowFactory.prototype */ stop: function awf_stop() { if (!this._started) { return; } this._started = false; window.removeEventListener('webapps-launch', this.preHandleEvent); window.removeEventListener('webapps-close', this.preHandleEvent); window.removeEventListener('open-app', this.preHandleEvent); window.removeEventListener('openwindow', this.preHandleEvent); window.removeEventListener('appopenwindow', this.preHandleEvent); }, /** * Queue events until AppWindowFactory is ready to handle them. */ _queueEvents: [], _queuePendingEvent: function(evt) { this._queueEvents.push(evt); }, _handlePendingEvents: function() { this._queueEvents.forEach((function(evt) { this.handleEvent(evt); }).bind(this)); this._queueEvents = []; }, preHandleEvent: function(evt) { if (applications.ready) { this.handleEvent(evt); } else { this._queuePendingEvent(evt); } }, handleEvent: function awf_handleEvent(evt) { var detail = evt.detail; if (evt.type === '_opened' || evt.type === '_terminated') { if (this._launchingApp === detail) { this.forgetLastLaunchingWindow(); } return; } if (!detail.url && !detail.manifestURL) { return; } var config = new BrowserConfigHelper(detail); config.evtType = evt.type; switch (evt.type) { case 'openwindow': case 'appopenwindow': case 'webapps-launch': config.timestamp = detail.timestamp; // TODO: Look up current opened window list, // and then create a new instance here. this.launch(config); break; case 'open-app': // System Message Handler API is asking us to open the specific URL // that handles the pending system message. // We will launch it in background if it's not handling an activity. config.isSystemMessage = true; if (detail.isActivity) { config.isActivity = true; if (detail.target.disposition && detail.target.disposition == 'inline') { config.inline = true; } } config.changeURL = !detail.onlyShowApp; config.stayBackground = !detail.showApp; if (detail.extra && detail.extra.manifestURL) { config.parentApp = detail.extra.manifestURL; } // TODO: Create activity window instance // or background app window instance for system message here. this.launch(config); break; case 'webapps-close': this.publish('killapp', config); break; } }, /** * Browser Configuration * @typedef {Object} BrowserConfig * @property {String} origin the same as appURL. * @property {String} manifestURL the same as manifestURL. * @property {Object} manifest the parsed manifest object. * If the app is not an entry point app, * the manifest would be the reference of application * manifest stored in Applications module. But if the app * is an entry point app, we will do deep clone to * generate a new object and replace the properties of * entry point to proper position. * @property {String} name the name of the app, retrieved from manifest. * @property {Boolean} oop Indicate it's running out of process or in * process. */ /** * Launch an app window. * @param {BrowserConfig} config Generated by BrowserConfigHelper. * @memberof AppWindowFactory.prototype */ launch: function awf_launch(config) { if (config.url === window.location.href) { return; } if (config.isActivity && config.inline) { this.publish('launchactivity', config); return; } // The rocketbar currently handles the management of normal search app // launches. Requests for the 'newtab' page will continue to filter // through and publish the launchapp event. if (this._isSearch(config)) { if (config.url.indexOf('newtab.html') === -1) { return; } // In case of the Browser, oppen an already open // instance instead of creating a new one var openInstance = this._findBrowserInstance(config); if (openInstance) { openInstance.requestOpen(); return; } } var app = Service.query('getApp', config.origin, config.manifestURL); if (app) { if (config.evtType == 'appopenwindow') { app.browser.element.src = config.url; } app.reviveBrowser(); // Always relaunch background app locally this.publish('launchapp', config); } else { var launchApp = () => { // homescreenWindowManager already listens webapps-launch and // open-app. We don't need to check if the launched app is homescreen. this.forgetLastLaunchingWindow(); this.trackLauchingWindow(config); this.publish('launchapp', config); }; if (Service.query('MultiScreenController.enabled')) { Service.request('chooseDisplay', config).catch(launchApp); } else { launchApp(); } } }, trackLauchingWindow: function(config) { var app = new AppWindow(config); if (config.stayBackground) { return; } this._launchingApp = app; this._launchingApp.element.addEventListener('_opened', this); this._launchingApp.element.addEventListener('_terminated', this); }, forgetLastLaunchingWindow: function() { if (this._launchingApp && this._launchingApp.element) { this._launchingApp.element.removeEventListener('_opened', this); this._launchingApp.element.removeEventListener('_terminated', this); } this._launchingApp = null; }, isLaunchingWindow: function() { return !!this._launchingApp; }, /** * Returns an appWindow instace that matches * the criteria given by the config * @param {Object} detail The data passed when initializing the event. * @memberof AppWindowFactory.prototype */ _findBrowserInstance: function(config) { // Special case for the browser. Openning the last instance // of any unpinned window, if it exists. var activeApp = Service.query('AppWindowManager.getActiveApp'); if (!this._isSearch(config) || activeApp.isBrowser()) { return; } var unpinned = Service.query('AppWindowManager.getUnpinnedWindows'); if (unpinned.length) { return unpinned.sort(function(app1, app2) { return app2.launchTime - app1.launchTime; })[0]; } }, /** * Determines if the current app to be opened * is the Browser app * @param {Object} detail The data passed when initializing the event. * @memberof AppWindowFactory.prototype */ _isSearch: function(config) { return (config.manifest && config.manifest.role === 'search'); }, /** * Publish a CustomEvent. * @param {String} event The name of the event. * @param {Object} detail The data passed when initializing the event. * @memberof AppWindowFactory.prototype */ publish: function awf_publish(event, detail, scope) { scope = scope || window; var evt = document.createEvent('CustomEvent'); evt.initCustomEvent(event, true, false, detail); scope.dispatchEvent(evt); } }; exports.AppWindowFactory = AppWindowFactory; }(window));