'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.
*
* 
*
* @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));