Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
1129 lines
36 KiB
JavaScript
1129 lines
36 KiB
JavaScript
/* global ShrinkingUI, BaseModule, LazyLoader, WrapperFactory */
|
|
'use strict';
|
|
|
|
(function(exports) {
|
|
/**
|
|
* AppWindowManager manages the interaction of AppWindow instances.
|
|
*
|
|
* * Controll the open/close request of the living appWindow
|
|
* instances.
|
|
* * Deliver the resize/orientation lock/setVisible request
|
|
* from LayoutManager/OrientationManager/VisibilityManager to the
|
|
* active appWindow instance.
|
|
*
|
|
* @module AppWindowManager
|
|
*/
|
|
var AppWindowManager = function() {};
|
|
AppWindowManager.SERVICES = [
|
|
'stopRecording',
|
|
'kill'
|
|
];
|
|
AppWindowManager.STATES = [
|
|
'getApp',
|
|
'getAppInScope',
|
|
'getAppByURL',
|
|
'getApps',
|
|
'getUnpinnedWindows',
|
|
'slowTransition',
|
|
'getActiveApp',
|
|
'getActiveWindow',
|
|
'isBusyLoading'
|
|
];
|
|
AppWindowManager.EVENTS = [
|
|
'launchapp',
|
|
'appcreated',
|
|
'appterminated',
|
|
'appopened',
|
|
'apprequestopen',
|
|
'apprequestclose',
|
|
'shrinking-start',
|
|
'shrinking-stop',
|
|
'homescreenopened',
|
|
'reset-orientation',
|
|
'homescreencreated',
|
|
'homescreen-changed',
|
|
// Watch chrome event that order to close an app
|
|
'killapp',
|
|
// Watch for event to bring a currently-open app to the foreground.
|
|
'displayapp',
|
|
// Deal with application uninstall event
|
|
// if the application is being uninstalled,
|
|
// we ensure it stop running here.
|
|
'applicationuninstall',
|
|
'hidewindow',
|
|
'showwindow',
|
|
'hidewindowforscreenreader',
|
|
'showwindowforscreenreader',
|
|
'attentionopened',
|
|
'homegesture-enabled',
|
|
'homegesture-disabled',
|
|
'appwindow-orientationchange',
|
|
'sheets-gesture-begin',
|
|
'sheets-gesture-end',
|
|
// XXX: PermissionDialog is shared so we need AppWindowManager
|
|
// to focus the active app after it's closed.
|
|
'permissiondialoghide',
|
|
'sleepmenuhide', // We need to focus the active app on this event
|
|
'appopening',
|
|
'localized',
|
|
'launchtrusted',
|
|
'taskmanager-activated',
|
|
'hierarchytopmostwindowchanged',
|
|
'cardviewbeforeshow',
|
|
'cardviewclosed',
|
|
'cardviewshown'
|
|
];
|
|
AppWindowManager.SUB_MODULES = [
|
|
'PinPageSystemDialog',
|
|
'FtuLauncher',
|
|
'AppWindowFactory',
|
|
'LockScreenLauncher',
|
|
// HWM is here because AWM needs to manage homescreenWindow
|
|
// as well. If someday we have a animation manager maybe we
|
|
// don't need to put it here.
|
|
'HomescreenWindowManager',
|
|
'AppInstallManager', // blocked by integrations (gij & gip)
|
|
'WrapperFactory' // To resolve global reference - remove it
|
|
// once it's instantiable.
|
|
];
|
|
AppWindowManager.SETTINGS = [
|
|
'continuous-transition.enabled',
|
|
'nfc.enabled',
|
|
'app-suspending.enabled'
|
|
];
|
|
BaseModule.create(AppWindowManager, {
|
|
DEBUG: false,
|
|
name: 'AppWindowManager',
|
|
EVENT_PREFIX: 'appwindowmanager',
|
|
continuousTransition: false,
|
|
|
|
/**
|
|
* Indicates the system is busy doing something.
|
|
* Now it stands for the foreground app is not loaded yet.
|
|
*
|
|
* XXX: AppWindowManager should register a service
|
|
* for isBusyLoading query by
|
|
* Service.register('isBusyLoading', appWindowManager).
|
|
*/
|
|
isBusyLoading: function() {
|
|
var app = this._activeApp;
|
|
return app && !app.loaded;
|
|
},
|
|
|
|
/**
|
|
* Enable slow transition or not for debugging.
|
|
* Note: Turn on this would make app opening/closing durations become 3s.
|
|
* @type {Boolean}
|
|
* @memberOf AppWindowManager
|
|
*/
|
|
slowTransition: false,
|
|
|
|
isActive: function() {
|
|
return (!!this._activeApp &&
|
|
(!this.taskManager || !this.taskManager.isActive()));
|
|
},
|
|
|
|
setHierarchy: function(active) {
|
|
this._activeApp && this._activeApp.setVisibleForScreenReader(active);
|
|
},
|
|
|
|
setFocus: function(active) {
|
|
if (!this._activeApp) {
|
|
this.debug('No active app.');
|
|
return false;
|
|
}
|
|
if (active) {
|
|
// XXX: Use this.appWindowFactory later
|
|
if (!this.appWindowFactory.isLaunchingWindow() &&
|
|
!WrapperFactory.isLaunchingWindow()) {
|
|
this.focus();
|
|
}
|
|
} else {
|
|
this._activeApp.blur();
|
|
this._activeApp.setNFCFocus(false);
|
|
}
|
|
return true;
|
|
},
|
|
|
|
focus: function() {
|
|
if (!this._activeApp) {
|
|
return;
|
|
}
|
|
this.debug('focusing ' + this._activeApp.name);
|
|
this._activeApp.focus();
|
|
this._activeApp.setNFCFocus(true);
|
|
},
|
|
|
|
/**
|
|
* Test the app is already running.
|
|
* @param {AppConfig} app The configuration of app.
|
|
* @return {Boolean} The app is running or not.
|
|
*/
|
|
isRunning: function awm_isRunning(config) {
|
|
if (config.manifestURL && this.getApp(config.origin)) {
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* HierarchyManager will use this function to
|
|
* get the active window instance.
|
|
* @return {AppWindow|null} The active app window instance
|
|
*/
|
|
getActiveWindow: function() {
|
|
return this.getActiveApp();
|
|
},
|
|
|
|
/**
|
|
* Get active app. If active app is null, we'll return homescreen as
|
|
* default.
|
|
* @return {AppWindow} The app is active.
|
|
*/
|
|
getActiveApp: function awm_getActiveApp() {
|
|
return this._activeApp || this.service.query('getHomescreen');
|
|
},
|
|
|
|
/**
|
|
* Match app origin and get the first matching one.
|
|
* @param {String} origin The origin to be matched.
|
|
* @param {String} [manifestURL] The manifestURL to be matched.
|
|
* @return {AppWindow} The app window object matched.
|
|
*/
|
|
getApp: function awm_getApp(origin, manifestURL) {
|
|
for (var id in this._apps) {
|
|
var app = this._apps[id];
|
|
if (app.origin === origin &&
|
|
(!manifestURL || app.manifestURL === manifestURL) &&
|
|
(!app.isBrowser() || app.config.url === origin)) {
|
|
return app;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Match app scope and get the first matching one.
|
|
* @param {String} scope The scope to be matched.
|
|
* @return {AppWindow} The app window object matched.
|
|
*/
|
|
getAppInScope: function awm_getAppInScope(scope, origin, name) {
|
|
var keys = Object.keys(this._apps);
|
|
var appInScope;
|
|
keys.forEach(function(id) {
|
|
var app = this._apps[id];
|
|
var inScope = (scope && app.inScope(scope));
|
|
if (scope && !inScope) {
|
|
return;
|
|
}
|
|
|
|
if (inScope || app.matchesOriginAndName(origin, name)) {
|
|
var replace = (!appInScope || appInScope.launchTime < app.launchTime);
|
|
appInScope = replace ? app : appInScope;
|
|
}
|
|
}.bind(this));
|
|
|
|
return appInScope;
|
|
},
|
|
|
|
/**
|
|
* Find all not pinned appWindows
|
|
* @return Array {AppWindow} Array of appWindows
|
|
*/
|
|
getUnpinnedWindows: function awm_getUnpinnedWindows() {
|
|
var unpinnedWindows = [];
|
|
for (var id in this._apps) {
|
|
var app = this._apps[id];
|
|
if (app.isBrowser() && !app.appChrome.pinned) {
|
|
unpinnedWindows.push(app);
|
|
}
|
|
}
|
|
return unpinnedWindows;
|
|
},
|
|
|
|
/**
|
|
* Match app window that is currently at a specific url.
|
|
* @param {String} url The url to be matched.
|
|
* @return {AppWindow} The app window object matched.
|
|
*/
|
|
getAppByURL: function awm_getAppByURL(url) {
|
|
for (var id in this._apps) {
|
|
var app = this._apps[id];
|
|
if (app.config.url === url) {
|
|
return app;
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
|
|
/**
|
|
* Get all apps.
|
|
* @return {Object} The running app window references stored
|
|
* by its instanceID.
|
|
*/
|
|
getApps: function awm_getApps() {
|
|
return this._apps;
|
|
},
|
|
|
|
// reference to active appWindow instance.
|
|
_activeApp: null,
|
|
|
|
// the user is transitioning between apps through edge gestures
|
|
_sheetTransitioning: false,
|
|
|
|
// store all alive appWindow instances.
|
|
// note: the id is instanceID instead of origin here.
|
|
_apps: {},
|
|
|
|
// store all callback functions in order to unobserve them when uninit.
|
|
_settingsObserveHandler: null,
|
|
|
|
/**
|
|
* Handles system browser URL sharing via NFC. Starts listening for
|
|
* peerready events once NFC is enabled in settings
|
|
* @type {Object}
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
_nfcHandler: null,
|
|
|
|
/**
|
|
* Switch to a different app, when called with no parameters will switch to
|
|
* the homescreen
|
|
* @param {AppWindow} [newApp] The new app window instance.
|
|
* @param {String} [openAnimation] The open animation for opening app.
|
|
* @param {String} [closeAnimation] The close animation for closing app.
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
display: function awm_display(newApp, openAnimation, closeAnimation,
|
|
eventType) {
|
|
this._dumpAllWindows();
|
|
var appCurrent = this._activeApp;
|
|
var appNext = newApp || this.service.query('getHomescreen', true);
|
|
|
|
if (!appNext) {
|
|
this.debug('no next app.');
|
|
return;
|
|
}
|
|
|
|
// If the app has child app window, open it instead.
|
|
while (appNext.nextWindow) {
|
|
appNext = appNext.nextWindow;
|
|
}
|
|
|
|
this.debug(' current is ' + (appCurrent ? appCurrent.url : 'none') +
|
|
'; next is ' + (appNext ? appNext.url : 'none'));
|
|
|
|
if (appCurrent && appCurrent.instanceID == appNext.instanceID) {
|
|
if (!appCurrent.isHomescreen) {
|
|
// Do nothing.
|
|
this.debug('the app has been displayed.');
|
|
return;
|
|
} else if (appCurrent.manifestURL == appNext.manifestURL) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (document.mozFullScreen) {
|
|
document.mozCancelFullScreen();
|
|
}
|
|
|
|
this.screen.classList.remove('fullscreen-app');
|
|
|
|
var switching = appCurrent && !appCurrent.isHomescreen &&
|
|
!appNext.isHomescreen;
|
|
|
|
var that = this;
|
|
if (appCurrent && this.service.query('keyboardEnabled')) {
|
|
this.stopRecording();
|
|
|
|
// Ask keyboard to hide before we switch the app.
|
|
window.addEventListener('keyboardhidden', function onhiddenkeyboard() {
|
|
window.removeEventListener('keyboardhidden', onhiddenkeyboard);
|
|
that.switchApp(appCurrent, appNext, switching);
|
|
});
|
|
|
|
if (this.continuousTransition) {
|
|
// Do keyboard transition.
|
|
this.service.request('hideInputWindow');
|
|
} else {
|
|
// Hide keyboard immediately.
|
|
this.service.request('hideInputWindowImmediately');
|
|
}
|
|
} else if (this.rocketbar && this.rocketbar.active) {
|
|
// Wait for the rocketbar to close
|
|
window.addEventListener('rocketbar-overlayclosed', function onClose() {
|
|
window.removeEventListener('rocketbar-overlayclosed', onClose);
|
|
that.switchApp(appCurrent, appNext, switching);
|
|
});
|
|
} else {
|
|
this.stopRecording(function() {
|
|
this.switchApp(appCurrent, appNext, switching,
|
|
openAnimation, closeAnimation);
|
|
}.bind(this));
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Switch from the current app to the next app.
|
|
* @param {AppWindow} appCurrent Displayed appWindow instance.
|
|
* @param {AppWindow} appNext appWindow instance to be shown.
|
|
* @param {Boolean} [switching] Homescreen doesn't involve in the two apps.
|
|
* @param {String} [openAnimation] The open animation for opening app.
|
|
* @param {String} [closeAnimation] The close animation for closing app.
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
switchApp: function awm_switchApp(appCurrent, appNext, switching,
|
|
openAnimation, closeAnimation) {
|
|
|
|
this.debug('before ready check' + appCurrent + appNext);
|
|
this._updateActiveApp(appNext.instanceID);
|
|
|
|
appNext.ready(function() {
|
|
if (appNext.isDead() || this._sheetTransitioning) {
|
|
if (!appNext.isHomescreen) {
|
|
// The app was killed while we were opening it,
|
|
// or we switched via edge gesture to a new app
|
|
// before fully opening this.
|
|
this._updateActiveApp(appCurrent.instanceID);
|
|
return;
|
|
} else {
|
|
// Homescreen might be dead due to OOM, we should ensure its opening
|
|
// before updateActiveApp.
|
|
appNext = this.service.query('getHomescreen');
|
|
appNext.ensure(true);
|
|
}
|
|
}
|
|
|
|
// An app was opened while we were getting ready to
|
|
// transition (probaly from an edge gesture)
|
|
if (this._activeApp && appNext !== this._activeApp) {
|
|
this._activeApp.close('immediate');
|
|
}
|
|
|
|
appNext.reviveBrowser();
|
|
this.debug('ready to open/close' + switching);
|
|
if (switching) {
|
|
this.publish('appswitching', this, true);
|
|
}
|
|
this._updateActiveApp(appNext.instanceID);
|
|
|
|
var immediateTranstion = false;
|
|
if (appNext.rotatingDegree === 90 || appNext.rotatingDegree === 270) {
|
|
immediateTranstion = true;
|
|
this.debug('immediate due to perpendicular 1');
|
|
} else if (appCurrent) {
|
|
var degree = appCurrent.determineClosingRotationDegree();
|
|
if (degree === 90 || degree === 270) {
|
|
immediateTranstion = true;
|
|
this.debug('immediate due to perpendicular 2');
|
|
}
|
|
} else if (appNext.isHomescreen) {
|
|
// If there's no active app and next app is homescreen,
|
|
// open it right away.
|
|
this.debug('immediate due to home');
|
|
immediateTranstion = true;
|
|
}
|
|
|
|
if (appNext.resized &&
|
|
!this.service.query('match', appNext.width, appNext.height)) {
|
|
this.debug('immediate due to resized');
|
|
immediateTranstion = true;
|
|
}
|
|
|
|
if (appCurrent && appNext &&
|
|
appCurrent.isHomescreen && appNext.isHomescreen) {
|
|
immediateTranstion = true;
|
|
}
|
|
|
|
appNext.open(immediateTranstion ? 'immediate' :
|
|
((switching === true) ? 'invoked' : openAnimation));
|
|
if (appCurrent && appCurrent.instanceID !== appNext.instanceID) {
|
|
appCurrent.close(immediateTranstion ? 'immediate' :
|
|
((switching === true) ? 'invoking' : closeAnimation));
|
|
} else {
|
|
this.debug('No current running app!');
|
|
}
|
|
}.bind(this));
|
|
},
|
|
|
|
/**
|
|
* The init process from bootstrap to homescreen is opened:
|
|
* 
|
|
*
|
|
* 1. Applications is ready. (mozApps are parsed.)
|
|
* 2. Bootstrap tells HomescreenLauncher to init.
|
|
* 3. Homescreen is ready.
|
|
* 4. Bootstrap tells FTULauncher to fetch FTU(First Time Use app) info.
|
|
* 5. FTU app is skipped or done.
|
|
* 6. AppWindowManager open homescreen app via homescreenWindowManager.
|
|
*
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
_start: function awm_start() {
|
|
this.element = document.getElementById('windows');
|
|
this.screen = document.getElementById('screen');
|
|
this.activated = false;
|
|
if (this.slowTransition) {
|
|
this.element.classList.add('slow-transition');
|
|
} else {
|
|
this.element.classList.remove('slow-transition');
|
|
}
|
|
|
|
this.service.request('registerHierarchy', this);
|
|
// Backward comaptibility before 1169122 is resolved.
|
|
window.appWindowManager = this;
|
|
|
|
return this.loadWhenIdle([
|
|
'StackManager',
|
|
'SheetsTransition',
|
|
'EdgeSwipeDetector',
|
|
'TaskManager',
|
|
'Places',
|
|
'SuspendingAppPriorityManager',
|
|
'Updatable',
|
|
'UpdateManager',
|
|
'AppMigrator'
|
|
]);
|
|
},
|
|
|
|
_stop: function awm_stop() {
|
|
this.service.request('unregisterHierarchy', this);
|
|
},
|
|
|
|
'_handle_system-resize': function(evt) {
|
|
if (this._activeApp) {
|
|
this.debug(' Resizing ' + this._activeApp.name);
|
|
if (!this._activeApp.isTransitioning()) {
|
|
var p = this._activeApp.resize();
|
|
if (typeof evt.detail.waitUntil === 'function') {
|
|
evt.detail.waitUntil(p);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
},
|
|
|
|
_handle_home: function(evt) {
|
|
// When shrinkingUI is enabled, home should be locked.
|
|
if (this.shrinkingUI && this.shrinkingUI.respondToHierarchyEvent(evt)) {
|
|
return false;
|
|
} else if (this.ftuLauncher &&
|
|
!this.ftuLauncher.respondToHierarchyEvent(evt)) {
|
|
return false;
|
|
} else if (this.taskManager && this.taskManager.isActive()) {
|
|
return true;
|
|
} else {
|
|
this.display();
|
|
return false;
|
|
}
|
|
},
|
|
|
|
_handle_holdhome: function(evt) {
|
|
// When shrinkingUI is enabled, hold home should be locked.
|
|
if (this.shrinkingUI && this.shrinkingUI.respondToHierarchyEvent(evt)) {
|
|
return false;
|
|
} else {
|
|
return this.ftuLauncher &&
|
|
this.ftuLauncher.respondToHierarchyEvent(evt);
|
|
}
|
|
},
|
|
|
|
respondToHierarchyEvent: function(evt) {
|
|
if (this['_handle_' + evt.type]) {
|
|
return this['_handle_' + evt.type](evt);
|
|
}
|
|
return true;
|
|
},
|
|
|
|
_handle_hierarchytopmostwindowchanged: function() {
|
|
if (this.service.query('getTopMostUI') !== this) {
|
|
return;
|
|
}
|
|
this._activeApp && this._activeApp.getTopMostWindow()
|
|
.setNFCFocus(true);
|
|
},
|
|
|
|
'_handle_shrinking-start': function(evt) {
|
|
if (this.shrinkingUI && this.shrinkingUI.isActive()) {
|
|
return;
|
|
}
|
|
if (this.service.query('getTopMostUI') !== this) {
|
|
return;
|
|
}
|
|
if (!window.ShrinkingUI) {
|
|
LazyLoader.load(['shared/js/shrinking_ui.js']).then(() => {
|
|
this.launchShrinkingUI();
|
|
}).catch((err) => {
|
|
console.error(err);
|
|
});
|
|
} else {
|
|
this.launchShrinkingUI();
|
|
}
|
|
},
|
|
|
|
launchShrinkingUI: function() {
|
|
var bottomMost = this._activeApp.getBottomMostWindow();
|
|
this.shrinkingUI = new ShrinkingUI(bottomMost.element,
|
|
bottomMost.element.parentNode);
|
|
this.shrinkingUI.start();
|
|
this._activeApp && this._activeApp.broadcast('shrinkingstart');
|
|
},
|
|
|
|
'_handle_shrinking-stop': function(evt) {
|
|
if (this.shrinkingUI && this.shrinkingUI.isActive()) {
|
|
this._activeApp && this._activeApp.broadcast('shrinkingstop');
|
|
this.shrinkingUI.stop();
|
|
}
|
|
},
|
|
|
|
// We do this because task manager is lower than us by design.
|
|
'_handle_taskmanager-activated': function() {
|
|
this.activated = false;
|
|
this.publish('-deactivated');
|
|
},
|
|
|
|
'_handle_cardviewshown': function() {
|
|
this.broadcastMessage('cardviewshown');
|
|
},
|
|
|
|
'_handle_cardviewclosed': function() {
|
|
this.broadcastMessage('cardviewclosed');
|
|
},
|
|
|
|
// XXX: Remove this once permission dialog is moved into AppWindow.
|
|
'_handle_permissiondialoghide': function() {
|
|
this._activeApp && this._activeApp.broadcast('focus');
|
|
},
|
|
|
|
// When the sleep menu is hidden we need to re-focus the active app
|
|
// or key events won't be properly dispatched to it. See bug 1200351.
|
|
'_handle_sleepmenuhide': function() {
|
|
this._activeApp && this._activeApp.broadcast('focus');
|
|
},
|
|
|
|
'_handle_appwindow-orientationchange': function() {
|
|
this.broadcastMessage('orientationchange',
|
|
this.service.query('getTopMostUI') === this);
|
|
},
|
|
|
|
// Dispatch internal events for navigation usage.
|
|
// The active app's navigation needs to know homes gesture is
|
|
// toggled to hide itself.
|
|
'_handle_homegesture-disabled': function(evt) {
|
|
this.broadcastMessage(evt.type);
|
|
},
|
|
|
|
'_handle_homegesture-enabled': function(evt) {
|
|
this.broadcastMessage(evt.type);
|
|
},
|
|
|
|
_handle_appcreated: function(evt) {
|
|
var app = evt.detail;
|
|
this._apps[evt.detail.instanceID] = app;
|
|
},
|
|
|
|
_handle_appterminated: function(evt) {
|
|
var app = evt.detail; // jshint ignore:line
|
|
var instanceID = evt.detail.instanceID;
|
|
if (this._activeApp && app.instanceID === this._activeApp.instanceID) {
|
|
this._activeApp = null;
|
|
}
|
|
delete this._apps[instanceID];
|
|
},
|
|
|
|
'_handle_reset-orientation': function() {
|
|
this._activeApp && this._activeApp.setOrientation();
|
|
},
|
|
|
|
_handle_appopening: function(evt) {
|
|
this._updateActiveApp(evt.detail.instanceID);
|
|
},
|
|
_handle_appopened: function(evt) {
|
|
this._updateActiveApp(evt.detail.instanceID);
|
|
},
|
|
_handle_homescreenopened: function(evt) {
|
|
this._updateActiveApp(evt.detail.instanceID);
|
|
},
|
|
|
|
|
|
_handle_homescreencreated: function(evt) {
|
|
this._apps[evt.detail.instanceID] = evt.detail;
|
|
},
|
|
|
|
'_handle_homescreen-changed': function(evt) {
|
|
if (this.service.query('isFtuRunning')) {
|
|
return;
|
|
}
|
|
|
|
this.display();
|
|
},
|
|
|
|
_handle_killapp: function(evt) {
|
|
this.kill(evt.detail.origin);
|
|
},
|
|
|
|
_handle_displayapp: function(evt) {
|
|
this.display(evt.detail);
|
|
},
|
|
|
|
_handle_apprequestopen: function(evt) {
|
|
this.display(evt.detail);
|
|
},
|
|
|
|
_handle_apprequestclose: function(evt) {
|
|
if (evt.detail.isActive()) {
|
|
this.display();
|
|
}
|
|
},
|
|
|
|
// Deal with application uninstall event
|
|
// if the application is being uninstalled,
|
|
// we ensure it stop running here.
|
|
_handle_applicationuninstall: function(evt) {
|
|
this.kill(evt.detail.application.origin);
|
|
},
|
|
|
|
_handle_hidewindow: function(evt) {
|
|
this._activeApp && this._activeApp.broadcast('hidewindow', evt.detail);
|
|
},
|
|
|
|
_handle_hidewindowforscreenreader: function() {
|
|
this._activeApp.setVisibleForScreenReader(false);
|
|
},
|
|
|
|
_handle_showwindowforscreenreader: function() {
|
|
this._activeApp.setVisibleForScreenReader(true);
|
|
},
|
|
|
|
_handle_showwindow: function(evt) {
|
|
this.onShowWindow(evt.detail);
|
|
},
|
|
|
|
_handle_attentionopened: function(evt) {
|
|
// Instantly blur the frame in order to ensure hiding the keyboard
|
|
if (this._activeApp) {
|
|
if (!this._activeApp.isOOP()) {
|
|
// Bug 845661 - Attention screen does not appears when
|
|
// the url bar input is focused.
|
|
// Calling app.iframe.blur() on an in-process window
|
|
// seems to triggers heavy tasks that froze the main
|
|
// process for a while and seems to expose a gecko
|
|
// repaint issue.
|
|
// So since the only in-process frame is the browser app
|
|
// let's switch it's visibility as soon as possible when
|
|
// there is an attention window and delegate the
|
|
// responsibility to blur the possible focused elements
|
|
// itself.
|
|
this._activeApp.setVisible(false, true);
|
|
} else {
|
|
this._activeApp.blur();
|
|
}
|
|
}
|
|
},
|
|
|
|
_handle_launchapp: function(evt) {
|
|
var config = evt.detail;
|
|
this.debug('launching' + config.origin);
|
|
this.launch(config);
|
|
},
|
|
|
|
_handle_launchtrusted: function(evt) {
|
|
if (evt.detail.chromeId) {
|
|
this._launchTrustedWindow(evt);
|
|
}
|
|
},
|
|
|
|
_handle_cardviewbeforeshow: function(evt) {
|
|
this._activeApp && this._activeApp.getTopMostWindow().blur();
|
|
this.broadcastMessage('cardviewbeforeshow');
|
|
},
|
|
|
|
'_handle_sheets-gesture-begin': function() {
|
|
this._sheetTransitioning = true;
|
|
if (document.mozFullScreen) {
|
|
document.mozCancelFullScreen();
|
|
}
|
|
this._activeApp && this._activeApp.setVisibleForScreenReader(false);
|
|
this.broadcastMessage('sheetsgesturebegin');
|
|
},
|
|
|
|
'_handle_sheets-gesture-end': function() {
|
|
this._sheetTransitioning = false;
|
|
// All inactive app window instances need to be aware of this so they
|
|
// can hide the screenshot overlay. The check occurs in the AppWindow.
|
|
this._activeApp && this._activeApp.setVisibleForScreenReader(true);
|
|
this.broadcastMessage('sheetsgestureend');
|
|
},
|
|
|
|
_handle_localized: function() {
|
|
this.broadcastMessage('localized');
|
|
},
|
|
|
|
_handle_launchactivity: function(evt) {
|
|
if (evt.detail.isActivity && evt.detail.inline && this._activeApp) {
|
|
this._activeApp.getTopMostWindow().broadcast('launchactivity',
|
|
evt.detail);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
'_handle_inputfocus': function(evt) {
|
|
if (this._activeApp) {
|
|
this._activeApp.getTopMostWindow().broadcast('inputfocus', evt.detail);
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
'_handle_inputblur': function(evt) {
|
|
if (this._activeApp) {
|
|
this._activeApp.getTopMostWindow().broadcast('inputblur');
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
|
|
_launchTrustedWindow: function(evt) {
|
|
if (this._activeApp) {
|
|
this._activeApp.broadcast('launchtrusted', evt.detail);
|
|
}
|
|
},
|
|
|
|
_dumpAllWindows: function() {
|
|
if (!this.DEBUG) {
|
|
return;
|
|
}
|
|
console.log('=====DUMPING APP WINDOWS BEGINS=====');
|
|
for (var id in this._apps) {
|
|
var app = this._apps[id];
|
|
if (app.previousWindow) {
|
|
continue;
|
|
}
|
|
this._dumpWindow(app);
|
|
while (app.nextWindow) {
|
|
this._dumpWindow(app, '->child:');
|
|
app = app.nextWindow;
|
|
}
|
|
}
|
|
console.log('=====END OF DUMPING APP WINDOWS=====');
|
|
},
|
|
|
|
_dumpWindow: function(app, prefix) {
|
|
console.log((prefix ? prefix : '') + '[' + app.instanceID + ']' +
|
|
(app.name || app.title || 'ANONYMOUS') + ' (' + app.url + ')');
|
|
if (app.calleeWindow) {
|
|
console.log('==>activity:[' + app.instanceID + ']' +
|
|
(app.name || app.title || 'ANONYMOUS') + ' (' + app.url + ')');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* App Config
|
|
* @typedef {Object} AppConfig
|
|
* @property {String} [manifestURL] The manifestURL of the app
|
|
* @property {String} url The URL of the activity handling page
|
|
* @property {String} origin The origin of the app
|
|
* @property {Boolean} [stayBackground=false] This app is launched
|
|
* at background or not.
|
|
* Usually this means it's a
|
|
* request from system message.
|
|
* @property {Boolean} [changeURL=false] Need to change the URL
|
|
* of the running app or not. If it's
|
|
* true, we only change the URL when
|
|
* the app is not running at
|
|
* foreground.
|
|
*/
|
|
|
|
/**
|
|
* Instanciate app window by configuration
|
|
* @param {AppConfig} config The configuration of the app window.
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
launch: function awm_launch(config) {
|
|
if (config.changeURL && this.getApp(config.origin)) {
|
|
// XXX: Potential problems here:
|
|
// there may be more than one app window instances
|
|
// have the same origin running,
|
|
// and we may change the wrong one.
|
|
this.getApp(config.origin).modifyURLatBackground(config.url);
|
|
}
|
|
if (config.stayBackground) {
|
|
return;
|
|
}
|
|
|
|
// Cancel inline activities on webapps-launch to make sure we actually
|
|
// open on the app and not on an inline activity
|
|
// (Workaround for bug 1122205)
|
|
var app = this.getApp(config.origin);
|
|
if (config.evtType === 'webapps-launch') {
|
|
app.frontWindow && app.frontWindow.kill();
|
|
}
|
|
|
|
// Link the window before displaying it to avoid race condition.
|
|
if (config.isActivity && this._activeApp) {
|
|
this.linkWindowActivity(config);
|
|
}
|
|
this.display(app);
|
|
},
|
|
|
|
linkWindowActivity: function awm_linkWindowActivity(config) {
|
|
var caller;
|
|
var callee = this.getApp(config.origin);
|
|
caller = this._activeApp.getTopMostWindow();
|
|
if (caller.getBottomMostWindow() === callee) {
|
|
callee.frontWindow && callee.frontWindow.kill();
|
|
} else {
|
|
callee.callerWindow = caller;
|
|
caller.calleeWindow = callee;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Kill the running app window by origin
|
|
*
|
|
* AppWindow instances are responsible to kill theirselves whenever
|
|
* there's something wrong when the app/page is running.
|
|
* For example: OOM, crashed, window.close().
|
|
*
|
|
* When we need to kill a running instance,
|
|
* we call kill on the instance and let the instance to request 'close'
|
|
* to AppWindowManager or just destroy itself if it's at background.
|
|
*
|
|
* 
|
|
*
|
|
* @param {String} origin The origin of the running app window to be killed.
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
kill: function awm_kill(origin, manifestURL) {
|
|
for (var id in this._apps) {
|
|
if (this._apps[id].origin === origin &&
|
|
(!manifestURL || this._apps[id].manifestURL === manifestURL)) {
|
|
this._apps[id].kill();
|
|
}
|
|
}
|
|
},
|
|
|
|
_updateActiveApp: function awm__changeActiveApp(instanceID) {
|
|
var appHasChanged = (this._activeApp !== this._apps[instanceID]);
|
|
this.debug(appHasChanged, this.activated, this._activeApp);
|
|
var activated = false;
|
|
if (!this.activated && appHasChanged && !this._activeApp) {
|
|
activated = true;
|
|
} else if (!appHasChanged && this._activeApp && !this.activated) {
|
|
activated = true;
|
|
}
|
|
|
|
this._activeApp = this._apps[instanceID];
|
|
if (!this._activeApp) {
|
|
this.debug('no active app alive: ' + instanceID);
|
|
return;
|
|
}
|
|
|
|
this.screen.classList.toggle(
|
|
'fullscreen-app', this._activeApp.isFullScreen());
|
|
|
|
var fullScreenLayout = this._activeApp.isFullScreenLayout();
|
|
this.screen.classList.toggle('fullscreen-layout-app', fullScreenLayout);
|
|
|
|
// Resize when opened.
|
|
// Note: we will not trigger reflow if the final size
|
|
// is the same as its current value.
|
|
this._activeApp.resize();
|
|
if (appHasChanged) {
|
|
if (this.shrinkingUI && this.shrinkingUI.isActive()) {
|
|
this.shrinkingUI.stop();
|
|
}
|
|
this.publish('activeappchanged', this, true);
|
|
}
|
|
|
|
this.debug('=== Active app now is: ',
|
|
(this._activeApp.name || this._activeApp.origin), '===');
|
|
if (activated) {
|
|
this.publish('-activated');
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Broadcast message to all running app window instances
|
|
*
|
|
* Used while an event needs to be manipulated by
|
|
* all running instances. You should register the event handler
|
|
* in the appWindow as well.
|
|
*
|
|
* @example
|
|
* AppWindowManager.broadcastMessage('earthquake');
|
|
*
|
|
* AppWindow.prototype._handle__earthquake = function() {
|
|
* // Run on my own!
|
|
* };
|
|
*
|
|
* AppWindow.REGISTERED_EVENTS.push('_earthquake');
|
|
*
|
|
* @param {String} message The message name.
|
|
* @param {Object} [detail] The detail of the message.
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
broadcastMessage: function awm_broadcastMessage(message, detail) {
|
|
for (var id in this._apps) {
|
|
this._apps[id].broadcast(message, detail);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* The event 'showwindow' may come with details, which means there is
|
|
* some steps need to be done after we show or don't show the active app,
|
|
* or the homescreen window.
|
|
*
|
|
* @param {Object} [detail] The detail of the event.
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
onShowWindow: function awm_onShowWindow(detail) {
|
|
var activeApp = this._activeApp;
|
|
|
|
// Just move the code from the conditional branches below to
|
|
// a re-usable function. To avoid people get confused with other
|
|
// homescreen related methods, this should not be moved out to
|
|
// be a method of AWM.
|
|
var launchHomescreen = () => {
|
|
// jshint ignore:line
|
|
var home = this.service.query('getHomescreen');
|
|
if (home) {
|
|
if (home.isActive()) {
|
|
home.setVisible(true);
|
|
} else {
|
|
this.display();
|
|
}
|
|
}
|
|
};
|
|
detail = detail ? detail : {}; // Give an empty object if it's null.
|
|
|
|
// In this statement we can add more possible slots when it's required.
|
|
// The undefined variables would keep undefined, and the existing ones
|
|
// would hold the data from the detail, so we don't need to parse the
|
|
// detail object with switch cases.
|
|
var { activity, notificationId } = detail;
|
|
if (activity || notificationId) {
|
|
if (activeApp && !activeApp.isHomescreen) {
|
|
activeApp.setVisible(true);
|
|
if (activity) {
|
|
this.fireActivity(activity);
|
|
} else if (notificationId){
|
|
this.fireNotificationClicked(notificationId);
|
|
}
|
|
} else {
|
|
if (activity) {
|
|
this.fireActivity(activity);
|
|
} else if (notificationId){
|
|
launchHomescreen();
|
|
this.fireNotificationClicked(notificationId);
|
|
}
|
|
}
|
|
} else { // it don't have the detail we can handle.
|
|
if (activeApp && !activeApp.isHomescreen) {
|
|
activeApp.setVisible(true);
|
|
} else {
|
|
launchHomescreen();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* After show the window of activity or homescreen,
|
|
* fire the following activity.
|
|
*
|
|
* @param {Object} [activityContent]
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
fireActivity: function awm_fireActivity(activityContent) {
|
|
// Need to invoke activity
|
|
var a = new window.MozActivity(activityContent);
|
|
a.onerror = function ls_activityError() {
|
|
console.log('MozActivity: activity error.');
|
|
};
|
|
},
|
|
|
|
/**
|
|
* After show the window of activity or homescreen,
|
|
* fire the event of notification clicked.
|
|
*
|
|
* @param {String} [notificationId]
|
|
* @memberOf module:AppWindowManager
|
|
*/
|
|
fireNotificationClicked:
|
|
function awm_fireNotificationClicked(notificationId) {
|
|
var event = document.createEvent('CustomEvent');
|
|
event.initCustomEvent('mozContentNotificationEvent', true, true, {
|
|
type: 'desktop-notification-click',
|
|
id: notificationId
|
|
});
|
|
window.dispatchEvent(event);
|
|
|
|
window.dispatchEvent(new CustomEvent('notification-clicked', {
|
|
detail: {
|
|
id: notificationId
|
|
}
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Abuse the settings database to notify interested certified apps
|
|
* that the current foreground window is about to close. This is a
|
|
* hack implemented to fix bug 1051172 so that apps can be notified
|
|
* that they will be closing without having to wait for the
|
|
* visibilitychange event that does not arrive until after the app
|
|
* has been hidden.
|
|
*
|
|
* This function is called from display() above to handle switching
|
|
* from an app to the homescreen or to the task switcher. It is also
|
|
* called from stack_manager.js to handle edge gestures. I tried calling
|
|
* it from screen_manager.js to handle screen blanking and the sleep
|
|
* button, but the visibiltychange event arrived before the will hide
|
|
* notification did in that case, so it was not necessary.
|
|
*
|
|
* We ought to be able to remove this function and the code that
|
|
* calls it when bug 1034001 is fixed.
|
|
*
|
|
*/
|
|
stopRecording: function(callback) {
|
|
// If we are not currently recording anything, just call
|
|
// the callback synchronously
|
|
if (!this.service.query('isRecording')) {
|
|
if (callback) { callback(); }
|
|
return;
|
|
}
|
|
|
|
|
|
// Otherwise, if we are recording something, then send a
|
|
// "stop recording" signal via the settings db before
|
|
// calling the callback.
|
|
var setRequest = navigator.mozSettings.createLock().set({
|
|
'private.broadcast.stop_recording': true
|
|
});
|
|
setRequest.onerror = function() {
|
|
// If the set request failed for some reason, just call the callback
|
|
if (callback) { callback(); }
|
|
};
|
|
setRequest.onsuccess = function() {
|
|
// And meanwhile, call the callback
|
|
if (callback) { callback(); }
|
|
};
|
|
},
|
|
|
|
'_observe_continuous-transition.enabled': function(value) {
|
|
if (!value) {
|
|
return;
|
|
}
|
|
this.continuousTransition = !!value;
|
|
},
|
|
|
|
'_observe_app-suspending.enabled': function(value) {
|
|
// Kill all instances if they are suspended.
|
|
if (!value) {
|
|
this.broadcastMessage('kill_suspended');
|
|
}
|
|
}
|
|
});
|
|
}());
|