Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2635 lines
78 KiB
JavaScript
2635 lines
78 KiB
JavaScript
/* global LazyLoader */
|
|
/* global AppChrome */
|
|
/* global AudioChannelController */
|
|
/* global BaseModule */
|
|
/* global BrowserFrame */
|
|
/* global ManifestHelper */
|
|
/* global WebManifestHelper */
|
|
/* global ScreenLayout */
|
|
/* global Service */
|
|
/* global DUMP */
|
|
/* global IconsHelper */
|
|
/* global LazyLoader */
|
|
'use strict';
|
|
|
|
(function(exports) {
|
|
const ICON_SIZE = 32;
|
|
|
|
// Turn on this flag to debug all windows.
|
|
var DEBUG = false;
|
|
// Turn on this flag to print all trace in debugging function.
|
|
var TRACE = false;
|
|
var _id = 0;
|
|
|
|
var themeGroups = [
|
|
'theme-communications', 'theme-media',
|
|
'theme-productivity', 'theme-settings'
|
|
];
|
|
|
|
/**
|
|
* AppWindow creates, contains, manages a
|
|
* [mozbrowser](https://developer.mozilla.org/en-US/docs/WebAPI/Browser)
|
|
* iframe. AppWindow is directly managed by AppWindowManager,
|
|
* by call resize(), open(), close() on AppWindow.
|
|
*
|
|
* Basically AppWindow would manipulate all mozbrowser events
|
|
* fired from the mozbrowser iframe by itself and show relevant UI.
|
|
*
|
|
* AppWindow is also the parent class of ActivityWindow and HomescreenWindow.
|
|
* Mostly they do the same thing but is different at some points
|
|
* like the way transitioning.
|
|
*
|
|
* About creating an AppWindow,
|
|
* you need to provide at least the web app/page URL.
|
|
* If you have also provided the manifest,
|
|
* then you would get an AppWindow object which is a web app.
|
|
* Otherwise you would get an AppWindow which is in 'Wrapper' type.
|
|
* The only one different thing between web app and web page is
|
|
* just the manifestURL.
|
|
*
|
|
* ##### Life cycle state machine of an appWindow instance
|
|
* 
|
|
*
|
|
* @example
|
|
* var app = new AppWindow({
|
|
* url: 'http://uitest.gaiamobile.org:8080/index.html',
|
|
* manifestURL: 'http://uitest.gaiamobile.org:8080/manifest.webapp'
|
|
* });
|
|
* app.open();
|
|
*
|
|
* @class AppWindow
|
|
* @mixes BrowserMixin into AppWindow.prototype
|
|
*/
|
|
var AppWindow = function AppWindow(configuration) {
|
|
this.reConfig(configuration);
|
|
this.render();
|
|
/**
|
|
* This is fired when the app window is instantiated.
|
|
* @event AppWindow#appcreated
|
|
*/
|
|
this.publish('created');
|
|
|
|
if (DEBUG || this._DEBUG) {
|
|
this.constructor[this.instanceID] = this;
|
|
}
|
|
this.isCrashed = false;
|
|
this.launchTime = Date.now();
|
|
this.metachangeDetails = [];
|
|
this.meta = {};
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* Change this if new window has its own styles.
|
|
*
|
|
* @type string
|
|
* @memberof AppWindow
|
|
*/
|
|
AppWindow.prototype.CLASS_LIST = 'appWindow';
|
|
AppWindow.prototype._DEBUG = false;
|
|
|
|
/**
|
|
* This is telling us who is the hierarchy manager to manage this window.
|
|
* * HierarchyManager ->
|
|
* * AttentionWindowManager -> AttentionWindow, CallscreenWindow
|
|
* (-> its popup/activity)
|
|
* * SecureWindowManager -> SecureWindow (-> its popup/activity)
|
|
* * LockScreenWindowManager -> LockScreenWindow (-> its popup/activity)
|
|
* * GlobalOverlayWindowManager -> GlobalOverlayWindow
|
|
* (-> its popup/activity)
|
|
* * Rocketbar -> SearchWindow (-> its popup/activity)
|
|
* * AppWindowManager -> AppWindow, HomescreenWindow (-> its popup/activity)
|
|
* @type {String}
|
|
*/
|
|
AppWindow.prototype.HIERARCHY_MANAGER = 'AppWindowManager';
|
|
|
|
/**
|
|
* Generate instanceID of this instance.
|
|
*/
|
|
AppWindow.prototype.generateID = function() {
|
|
if (!this.instanceID) {
|
|
this.instanceID = this.CLASS_NAME + '_' + _id;
|
|
_id++;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Generate all configurations we need.
|
|
* @param {Object} configuration Initial configuration object
|
|
* Includes manifestURL, manifest, url, origin, name.
|
|
*/
|
|
AppWindow.prototype.reConfig = function aw_reConfig(configuration) {
|
|
// Some modules are querying appWindow.manifestURL or appWindow.origin
|
|
// so we inject all configurations into appWindow first.
|
|
for (var key in configuration) {
|
|
this[key] = configuration[key];
|
|
}
|
|
|
|
this.browser_config = configuration;
|
|
// Store initial configuration in this.config
|
|
this.config = configuration;
|
|
|
|
if (this.manifest) {
|
|
this.name = new ManifestHelper(this.manifest).displayName;
|
|
} else if (this.config.url) {
|
|
this.name = new URL(this.config.url).hostname;
|
|
}
|
|
|
|
// Get icon splash
|
|
this.getIconForSplash();
|
|
|
|
this.generateID();
|
|
/**
|
|
* The instanceID of the root window of this window.
|
|
* @type {String}
|
|
*/
|
|
this.groupID = this.getRootWindow().instanceID;
|
|
if (this.previousWindow) {
|
|
this.previousWindow.setNextWindow(this);
|
|
} else if (this.rearWindow) {
|
|
this.rearWindow.setFrontWindow(this);
|
|
}
|
|
|
|
// W3C web app manifest "display" property takes precedence
|
|
if (this.manifest && this.manifest.display) {
|
|
switch(this.manifest.display) {
|
|
case 'fullscreen':
|
|
this._fullScreen = true;
|
|
this.config.chrome = {
|
|
scrollable: false,
|
|
maximized: false,
|
|
};
|
|
return; // Early return
|
|
case 'standalone':
|
|
this.config.chrome = {
|
|
scrollable: false,
|
|
maximized: false,
|
|
};
|
|
return;
|
|
case 'minimal-ui':
|
|
case 'browser':
|
|
this.config.chrome = {
|
|
navigation: true, //AppChrome checks for this
|
|
scrollable: true,
|
|
maximized: true
|
|
};
|
|
return;
|
|
default:
|
|
console.error('Invalid display property in web app manifest.');
|
|
}
|
|
}
|
|
|
|
// Fall back to mozApp manifest chrome and fullscreen properties
|
|
this.config.chrome = (this.manifest && this.manifest.chrome) ?
|
|
this.manifest.chrome :
|
|
this.config.chrome;
|
|
|
|
if (!this.config.chrome) {
|
|
this.config.chrome = {
|
|
scrollable: this.isBrowser(),
|
|
maximized: this.isBrowser()
|
|
};
|
|
} else if (this.config.chrome.navigation) {
|
|
this.config.chrome.scrollable = !this.isFullScreen();
|
|
// This is for backward compatibility with application that
|
|
// requests the |navigation| flag in their manifest.
|
|
this.config.chrome.maximized = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Represents the current page visibility state
|
|
*/
|
|
AppWindow.prototype._visible = true;
|
|
|
|
/**
|
|
* The rotating degree of current frame.
|
|
*/
|
|
AppWindow.prototype.rotatingDegree = 0;
|
|
|
|
AppWindow.prototype._dump = function aw__dump() {
|
|
console.log('======================');
|
|
try {
|
|
throw new Error('e');
|
|
} catch (e) {
|
|
this.debug(e.stack);
|
|
}
|
|
console.log('======================');
|
|
};
|
|
|
|
/**
|
|
* Set active should be only applied to top most window.
|
|
*/
|
|
AppWindow.prototype.setActive = function(enable) {
|
|
this.getTopMostWindow()._setActive(enable);
|
|
};
|
|
|
|
AppWindow.prototype.inScope = function(scope) {
|
|
if (!this.isBrowser() || !scope) {
|
|
return false;
|
|
}
|
|
// within-scope per http://www.w3.org/TR/appmanifest/#dfn-within-scope
|
|
// except we also support paths
|
|
var target = this.config.url;
|
|
var fullScopeUrl = new URL(scope, target).href;
|
|
return fullScopeUrl && target.startsWith(fullScopeUrl);
|
|
};
|
|
|
|
AppWindow.prototype.matchesOriginAndName = function(origin, name) {
|
|
if (!this.isBrowser()) {
|
|
return false;
|
|
}
|
|
|
|
var target = this.config.url;
|
|
return new URL(target).origin === origin && this.name === name;
|
|
};
|
|
|
|
/**
|
|
* In order to prevent flashing of unpainted frame
|
|
* during switching from one to another,
|
|
* many event listener & callbacks are employed.
|
|
*
|
|
* When 'cardviewbeforeshow' is received, the screenshotOverlay is shown.
|
|
*
|
|
* When 'cardviewclosed' is received, the screenshotOverlay is hidden.
|
|
*
|
|
* When 'sheetdisplayed' is received, the screenshotOverlay is shown.
|
|
*
|
|
* When 'sheetsgestureend' is received the screenshotOverlay is hidden.
|
|
*
|
|
* However, setVisible *never* controls the visibility of the
|
|
* screenshotOverlay. The visibility of it is entirely event driven or can
|
|
* be triggered manually be calling _showScreenshotOverlay and
|
|
* _hideScreenshotOverlay.
|
|
*/
|
|
AppWindow.prototype.setVisible =
|
|
function aw_setVisible(visible, doNotPropagate) {
|
|
this.setVisibleForScreenReader(visible);
|
|
if (!doNotPropagate && this.frontWindow && this.frontWindow.isActive()) {
|
|
this.frontWindow.setVisible(visible);
|
|
return;
|
|
}
|
|
|
|
if (this._visible === visible) {
|
|
return;
|
|
}
|
|
this._visible = visible;
|
|
|
|
this.debug('set visibility -> ', visible);
|
|
this._setActive(visible);
|
|
if (visible) {
|
|
// If this window is not the lockscreen, and the screen is locked,
|
|
// we need to aria-hide the window.
|
|
this._showFrame();
|
|
|
|
// Handle any pending scroll-area-changed (this is deferred when an
|
|
// app isn't visible).
|
|
if (this.appChrome) {
|
|
this.appChrome.handleScrollAreaChanged();
|
|
}
|
|
} else {
|
|
this._hideFrame();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Set screen reader visibility.
|
|
* @type {Boolean} A flag indicating if it should be visible to the screen
|
|
* reader.
|
|
*/
|
|
AppWindow.prototype.setVisibleForScreenReader =
|
|
function aw_setVisibleForScreenReader(visible) {
|
|
if (!this.element) {
|
|
return;
|
|
}
|
|
|
|
// We need to let the screenreader reach the frontWindow if it's visible
|
|
if (this.frontWindow && this.frontWindow.isVisible()) {
|
|
visible = true;
|
|
}
|
|
|
|
this.element.setAttribute('aria-hidden', !visible);
|
|
this._setVisibleForScreenReader(visible);
|
|
};
|
|
|
|
/**
|
|
* _showFrame turn on the frame visibility.
|
|
* So this shouldn't be invoked by others directly.
|
|
*/
|
|
AppWindow.prototype._showFrame = function aw__showFrame() {
|
|
this.debug('before showing frame');
|
|
this.reviveBrowser();
|
|
|
|
// If we're already showing, do nothing!
|
|
if (!this.element.classList.contains('inactive')) {
|
|
return;
|
|
}
|
|
|
|
this.element.classList.remove('inactive');
|
|
this._setVisible(true);
|
|
|
|
if (this.isHomescreen) {
|
|
return;
|
|
}
|
|
|
|
// Getting a new screenshot to force compositing before
|
|
// removing the screenshot overlay if it exists.
|
|
if (this.screenshotOverlay &&
|
|
this.screenshotOverlay.classList.contains('visible')) {
|
|
this.tryWaitForFullRepaint(this._hideScreenshotOverlay.bind(this));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* _hideFrame will turn off the frame visibility.
|
|
* So this shouldn't be invoked by others directly.
|
|
*/
|
|
AppWindow.prototype._hideFrame = function aw__hideFrame() {
|
|
this.debug('before hiding frame');
|
|
|
|
// If we're already hidden, we have nothing to do!
|
|
if (!this.browser || this.element.classList.contains('inactive')) {
|
|
return;
|
|
}
|
|
|
|
this._setVisible(false);
|
|
this.element.classList.add('inactive');
|
|
};
|
|
|
|
/**
|
|
* An appWindow is active means:
|
|
* 1. Going to be opened.
|
|
* 2. Already opened.
|
|
* 3. Not going to be closed.
|
|
*
|
|
* Note: The element has active class unless it's at closed state.
|
|
* But a closing instance is not recognized as active.
|
|
*
|
|
* @return {Boolean} The instance is active or not.
|
|
*/
|
|
AppWindow.prototype.isActive = function aw_isActive() {
|
|
if (!this.element) {
|
|
return false;
|
|
}
|
|
|
|
if (this.element.classList.contains('will-become-active')) {
|
|
return true;
|
|
}
|
|
|
|
if (this.element.classList.contains('will-become-inactive')) {
|
|
return false;
|
|
}
|
|
|
|
if (this.transitionController) {
|
|
return (this.transitionController._transitionState == 'opened' ||
|
|
this.transitionController._transitionState == 'opening');
|
|
} else {
|
|
// Before the transition controller is inited
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* An appWindow should resize:
|
|
* 1. If it's active
|
|
* 2. If it was just queued for close.
|
|
*
|
|
* @return {Boolean} The instance should be resized.
|
|
*/
|
|
AppWindow.prototype.shouldResize = function aw_shouldResize() {
|
|
if (this.element.classList.contains('will-become-inactive')) {
|
|
return true;
|
|
}
|
|
|
|
return this.isActive();
|
|
};
|
|
|
|
AppWindow.prototype.isSheetTransitioning =
|
|
function aw_isSheetTransitioning() {
|
|
return this.element.classList.contains('inside-edges');
|
|
};
|
|
|
|
/**
|
|
* TODO: Integrate swipe transition.
|
|
*
|
|
* @return {Boolean} The instance is transitioning or not.
|
|
*/
|
|
AppWindow.prototype.isTransitioning = function aw_isTransitioning() {
|
|
if (this.transitionController) {
|
|
return (this.transitionController._transitionState == 'opening' ||
|
|
this.transitionController._transitionState == 'closing');
|
|
} else {
|
|
// Fallback
|
|
return (this.element.classList.contains('transition-opening') ||
|
|
this.element.classList.contains('transition-closing'));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* If the instance is crashed but not resumed yet,
|
|
* it would be in suspended state.
|
|
*
|
|
* The state would be turned off once we render the browser again.
|
|
* @type {Boolean}
|
|
*/
|
|
AppWindow.prototype.suspended = false;
|
|
|
|
/**
|
|
* Re-render the browser element with the same config.
|
|
*/
|
|
AppWindow.prototype.reviveBrowser = function() {
|
|
if (this.browser) {
|
|
return;
|
|
}
|
|
this.isCrashed = false;
|
|
this.debug(' ...revived!');
|
|
this.browser = new BrowserFrame(this.browser_config);
|
|
this.browserContainer.appendChild(this.browser.element);
|
|
this.iframe = this.browser.element;
|
|
this.launchTime = Date.now();
|
|
this.suspended = false;
|
|
this.element.classList.remove('suspended');
|
|
// Launch as background by default.
|
|
this.browser.element.classList.add('hidden');
|
|
this._setVisible(false);
|
|
this.publish('resumed');
|
|
};
|
|
|
|
/**
|
|
* Remove the browser element and clear the states of the browser.
|
|
* @fires AppWindow#suspended
|
|
*/
|
|
AppWindow.prototype.destroyBrowser = function() {
|
|
if (!this.browser) {
|
|
return;
|
|
}
|
|
this.inError = false;
|
|
this.loading = false;
|
|
this.loaded = false;
|
|
this.metachangeDetails = [];
|
|
this.suspended = true;
|
|
this.element && this.element.classList.add('suspended');
|
|
this.browserContainer.removeChild(this.browser.element);
|
|
this.browser = null;
|
|
this.iframe = null;
|
|
this._sslState = '';
|
|
this.publish('suspended');
|
|
};
|
|
|
|
/**
|
|
* Kill an instance.
|
|
*
|
|
* 1. If this instance has any living activity window as callee,
|
|
* all of the activity window callee would be killed.
|
|
* 2. If this instance is active, it would request to close.
|
|
*
|
|
* @fires AppWindow#appterminated
|
|
*/
|
|
AppWindow.prototype.kill = function aw_kill(evt) {
|
|
if (this._killed) {
|
|
return;
|
|
}
|
|
|
|
if (!this.isHomescreen) {
|
|
this._killed = true;
|
|
}
|
|
|
|
if (DEBUG || this._DEBUG) {
|
|
this.constructor[this.instanceID] = null;
|
|
}
|
|
|
|
// Remove callee <-> caller reference before we remove the window.
|
|
if (this.callerWindow) {
|
|
this.callerWindow.calleeWindow = null;
|
|
this.callerWindow = null;
|
|
}
|
|
if (this.calleeWindow) {
|
|
this.calleeWindow.callerWindow = null;
|
|
this.calleeWindow = null;
|
|
}
|
|
|
|
// Kill any front window.
|
|
if (this.frontWindow) {
|
|
this.frontWindow.kill();
|
|
this.frontWindow = null;
|
|
}
|
|
|
|
// Kill any next window.
|
|
if (this.nextWindow) {
|
|
this.nextWindow.kill();
|
|
this.nextWindow = null;
|
|
}
|
|
|
|
// Kill any attention window.
|
|
if (this.attentionWindow) {
|
|
this.attentionWindow.kill();
|
|
this.attentionWindow = null;
|
|
}
|
|
|
|
// If the app is the currently displayed app, switch to the homescreen
|
|
if (this.isActive() && this.getBottomMostWindow().isActive() &&
|
|
!this.isHomescreen) {
|
|
|
|
var onClosed = function() {
|
|
this.element.removeEventListener('_closed', onClosed);
|
|
this.destroy();
|
|
}.bind(this);
|
|
|
|
this.element.addEventListener('_closed', onClosed);
|
|
|
|
if (this.previousWindow) {
|
|
this.previousWindow.getBottomMostWindow().open('in-from-left');
|
|
this.close('out-to-right');
|
|
} else {
|
|
if (this.transitionController) {
|
|
// In normal case,
|
|
// the window manager will call this.close() to response
|
|
// the requestClose(), and when the timeout here is reached,
|
|
// it will not close again because transition controller
|
|
// is taking care of that.
|
|
setTimeout(this.close.bind(this, 'immediate'),
|
|
this.transitionController.CLOSING_TRANSITION_TIMEOUT);
|
|
}
|
|
this.requestClose();
|
|
}
|
|
} else {
|
|
this.destroy();
|
|
}
|
|
|
|
/**
|
|
* Fired when the instance is terminated.
|
|
* @event AppWindow#appterminated
|
|
*/
|
|
this.publish('terminated');
|
|
};
|
|
|
|
/**
|
|
* An appWindow is dead if somebody requested it to be killed.
|
|
*
|
|
* @return {Boolean} The instance is dead or not.
|
|
*/
|
|
AppWindow.prototype.isDead = function aw_isDead() {
|
|
return (this._killed || !this.element);
|
|
};
|
|
|
|
/**
|
|
* Destroy the instance.
|
|
* @fires AppWindow#appdestroyed
|
|
*/
|
|
AppWindow.prototype.destroy = function aw_destroy() {
|
|
/**
|
|
* Fired before the instance id destroyed.
|
|
* @event AppWindow#appwilldestroy
|
|
*/
|
|
// Remove previous -> next reference.
|
|
if (this.previousWindow) {
|
|
this.previousWindow.unsetNextWindow();
|
|
this.previousWindow = null;
|
|
}
|
|
|
|
// Remove rear -> front reference.
|
|
if (this.rearWindow) {
|
|
this.rearWindow.unsetFrontWindow();
|
|
this.rearWindow = null;
|
|
}
|
|
|
|
// Remove attention reference.
|
|
if (this.parentWindow) {
|
|
this.parentWindow.unsetAttentionWindow();
|
|
this.parentWindow = null;
|
|
}
|
|
|
|
this.publish('willdestroy');
|
|
this.uninstallSubComponents();
|
|
if (this.element) {
|
|
if (this.element.parentNode) {
|
|
this.element.parentNode.removeChild(this.element);
|
|
}
|
|
this.element = null;
|
|
}
|
|
|
|
/**
|
|
* Fired after the instance id destroyed.
|
|
* @event AppWindow#appdestroyed
|
|
*/
|
|
this.publish('destroyed');
|
|
};
|
|
|
|
AppWindow.prototype.containerElement = document.getElementById('windows');
|
|
AppWindow.prototype.containerId = 'windows';
|
|
|
|
AppWindow.prototype.view = function aw_view() {
|
|
return `<div class="${this.CLASS_LIST}" id="${this.instanceID}"
|
|
transition-state="closed">
|
|
<div class="identification-overlay">
|
|
<div>
|
|
<div class="icon"></div>
|
|
<span class="title" dir="auto"></span>
|
|
</div>
|
|
</div>
|
|
<div class="fade-overlay"></div>
|
|
<div class="touch-blocker"></div>
|
|
<div class="browser-container">
|
|
<div class="screenshot-overlay"></div>
|
|
</div>
|
|
</div>`;
|
|
};
|
|
|
|
/**
|
|
* Render the mozbrowser iframe and some overlays.
|
|
* @inner
|
|
*/
|
|
AppWindow.prototype._render = function aw__render() {
|
|
if (this.element) {
|
|
return;
|
|
}
|
|
/**
|
|
* Fired before this element is appended to the DOM tree.
|
|
* @event AppWindow#appwillrender
|
|
*/
|
|
this.publish('willrender');
|
|
|
|
var range = document.createRange();
|
|
var fragment = range.createContextualFragment(this.view());
|
|
|
|
// window.open would offer the iframe so we don't need to generate.
|
|
if (this.iframe) {
|
|
this.browser = {
|
|
element: this.iframe
|
|
};
|
|
} else {
|
|
this.browser = new BrowserFrame(this.browser_config);
|
|
}
|
|
this.element = fragment.getElementById(this.instanceID);
|
|
|
|
// For gaiauitest usage.
|
|
this.element.dataset.manifestName = this.manifest ? this.manifest.name : '';
|
|
|
|
// XXX: Remove following two lines once mozbrowser element is moved
|
|
// into appWindow.
|
|
this.frame = this.element;
|
|
this.iframe = this.browser.element;
|
|
this.iframe.dataset.frameType = 'window';
|
|
this.iframe.dataset.frameOrigin = this.origin;
|
|
this.iframe.dataset.url = this.config.url;
|
|
|
|
if (this.isFullScreen()) {
|
|
this.element.classList.add('fullscreen-app');
|
|
}
|
|
|
|
if (this.isBrowser()) {
|
|
this.element.classList.add('browser');
|
|
}
|
|
|
|
if (this.isPrivateBrowser()) {
|
|
this.element.classList.add('private');
|
|
}
|
|
|
|
this.browserContainer = this.element.querySelector('.browser-container');
|
|
this.browserContainer.appendChild(this.browser.element);
|
|
|
|
if (!this.containerElement && this.containerId) {
|
|
this.containerElement = document.getElementById(this.containerId);
|
|
}
|
|
this.containerElement.appendChild(fragment);
|
|
|
|
this.screenshotOverlay = this.element.querySelector('.screenshot-overlay');
|
|
this.fadeOverlay = this.element.querySelector('.fade-overlay');
|
|
|
|
var overlay = '.identification-overlay';
|
|
this.identificationOverlay = this.element.querySelector(overlay);
|
|
var icon = '.identification-overlay .icon';
|
|
this.identificationIcon = this.element.querySelector(icon);
|
|
var title = '.identification-overlay .title';
|
|
this.identificationTitle = this.element.querySelector(title);
|
|
|
|
// Launched as background: set visibility and overlay screenshot.
|
|
// homescreen is launched at background under FTU/lockscreen too.
|
|
if (this.config.stayBackground || this.isHomescreen) {
|
|
this.setVisible(false);
|
|
}
|
|
|
|
/**
|
|
* Fired after the app window element is appended to the DOM tree.
|
|
* @event AppWindow#apprendered
|
|
*/
|
|
this.publish('rendered');
|
|
this._rendered = true;
|
|
|
|
setTimeout(this.setFrameBackground.bind(this));
|
|
};
|
|
|
|
AppWindow.prototype.render = function aw_render() {
|
|
this._render();
|
|
this._registerEvents();
|
|
this.installSubComponents();
|
|
// Pre determine the rotation degree.
|
|
this.determineRotationDegree();
|
|
};
|
|
|
|
/**
|
|
* Check an appWindow is a regular browsing window, not tied to a particular
|
|
* application.
|
|
*
|
|
* @return {Boolean} is the current instance a browsing window.
|
|
*/
|
|
AppWindow.prototype.isBrowser = function aw_isbrowser() {
|
|
return !this.manifestURL;
|
|
};
|
|
|
|
/**
|
|
* Checks if an appWindow is a private window.
|
|
*
|
|
* @return {Boolean} is the current instance a private window.
|
|
*/
|
|
AppWindow.prototype.isPrivateBrowser = function aw_isprivate() {
|
|
return !!(this.browser_config && this.browser_config.isPrivate);
|
|
};
|
|
|
|
/**
|
|
* Check an appWindow contains a certified application
|
|
*
|
|
* @return {Boolean} is the current instance a certified application.
|
|
*/
|
|
AppWindow.prototype.isCertified = function aw_iscertified() {
|
|
return this.config.manifest && 'certified' === this.config.manifest.type;
|
|
};
|
|
|
|
AppWindow.prototype.isSearch = function() {
|
|
return this.manifest && this.manifest.role === 'search';
|
|
};
|
|
|
|
/**
|
|
* Try to navigate the current frame to a given url. If the current instance
|
|
* is not a browsing window, we reconfigure it to be a browser window, and
|
|
* give it a clean browser frame.
|
|
* @param {String} url The url to navigate to
|
|
*/
|
|
AppWindow.prototype.navigate = function aw_isbrowser(url) {
|
|
|
|
// Bug 1071882 - We currently only support navigating browser windows and
|
|
// apps with a role="search", which we use for the browser landing page.
|
|
var appIsSearch = this.isSearch();
|
|
if (!this.isBrowser() && !appIsSearch) {
|
|
console.warn('Tried to navigate an illegal app window.');
|
|
return;
|
|
}
|
|
|
|
// Kill any front window.
|
|
if (this.frontWindow) {
|
|
this.frontWindow.kill();
|
|
this.frontWindow = null;
|
|
}
|
|
|
|
if (!this.isBrowser()) {
|
|
// Handle navigating from an app -> browser.
|
|
this.manifestURL = null;
|
|
this.manifest = null;
|
|
this.name = null;
|
|
this.element.classList.add('browser');
|
|
|
|
// Reset the browser
|
|
this.reConfig({
|
|
url: url,
|
|
title: url,
|
|
oop: true,
|
|
isPrivate: this.isPrivate
|
|
});
|
|
this.appChrome && this.appChrome.reConfig();
|
|
this.browserContainer.removeChild(this.browser.element);
|
|
this.browser = new BrowserFrame(this.browser_config);
|
|
this.browserContainer.appendChild(this.browser.element);
|
|
this.iframe = this.browser.element;
|
|
this.launchTime = Date.now();
|
|
// Register audio channels for new browser frame.
|
|
this._unregisterAudioChannels();
|
|
this._registerAudioChannels();
|
|
}
|
|
this.browser.element.src = url;
|
|
};
|
|
|
|
/**
|
|
* The event is necessary for edge gesture swiper.
|
|
* It means the app window is swiped in.
|
|
*
|
|
* @access private
|
|
* @event AppWindow~_swipein
|
|
*/
|
|
|
|
/**
|
|
* The event is necessary for edge gesture swiper.
|
|
* It means the app window is swiped out.
|
|
*
|
|
* @access private
|
|
* @event AppWindow~_swipeout
|
|
*/
|
|
AppWindow.REGISTERED_EVENTS =
|
|
['mozbrowserclose', 'mozbrowsererror', 'mozbrowservisibilitychange',
|
|
'mozbrowserloadend', 'mozbrowseractivitydone', 'mozbrowserloadstart',
|
|
'mozbrowsertitlechange', 'mozbrowserlocationchange',
|
|
'mozbrowsermetachange', 'mozbrowsericonchange',
|
|
'mozbrowsersecuritychange', 'mozbrowsermanifestchange',
|
|
'_localized', '_swipein', '_swipeout', '_kill_suspended',
|
|
'_orientationchange', '_focus', '_blur', '_hidewindow', '_sheetdisplayed',
|
|
'_sheetsgestureend', '_cardviewbeforeshow', '_cardviewclosed',
|
|
'_cardviewshown', '_closed', '_shrinkingstart', '_shrinkingstop'];
|
|
|
|
AppWindow.SUB_COMPONENTS = {
|
|
'transitionController': 'AppTransitionController',
|
|
'modalDialog': 'AppModalDialog',
|
|
'valueSelector': 'ValueSelector',
|
|
'authDialog': 'AppAuthenticationDialog',
|
|
'childWindowFactory': 'ChildWindowFactory',
|
|
'statusbar': 'AppStatusbar',
|
|
'textSelectionDialog': 'AppTextSelectionDialog'
|
|
};
|
|
|
|
AppWindow.SUB_MODULES = {
|
|
'contextmenu': 'BrowserContextMenu'
|
|
};
|
|
|
|
AppWindow.prototype.openAnimation = 'enlarge';
|
|
AppWindow.prototype.closeAnimation = 'reduce';
|
|
|
|
/**
|
|
* Install sub components belong to this window instance.
|
|
* The necessary components are based on AppWindow.SUB_COMPONENTS,
|
|
* now they are AppModalDialog/AppAuthenticationDialog.
|
|
*
|
|
* @example
|
|
* // Implement a new dialog
|
|
* var myDialog = function myDialog(app) {
|
|
* };
|
|
*
|
|
* var appWindow = new AppWindow();
|
|
* AppWindow.SUB_COMPONENTS.push(myDialog);
|
|
*
|
|
* app.installSubComponents();
|
|
*/
|
|
AppWindow.prototype.installSubComponents =
|
|
function aw_installSubComponents() {
|
|
this.debug('installing sub components...');
|
|
for (var componentName in this.constructor.SUB_COMPONENTS) {
|
|
var constructor =
|
|
window[this.constructor.SUB_COMPONENTS[componentName]];
|
|
if (constructor) {
|
|
this[componentName] =
|
|
new constructor(this);
|
|
this[componentName].start && this[componentName].start();
|
|
}
|
|
}
|
|
|
|
for (var propertyName in this.constructor.SUB_MODULES) {
|
|
var moduleName = this.constructor.SUB_MODULES[propertyName];
|
|
if (moduleName) {
|
|
this[propertyName] = BaseModule.instantiate(moduleName, this);
|
|
this[propertyName].start();
|
|
}
|
|
}
|
|
|
|
// Need to wait for mozbrowserloadstart to get allowedAudioChannels.
|
|
this.browser.element.addEventListener(
|
|
'mozbrowserloadstart',
|
|
function onloadstart() {
|
|
this.browser.element.removeEventListener(
|
|
'mozbrowserloadstart', onloadstart
|
|
);
|
|
this._registerAudioChannels();
|
|
}.bind(this)
|
|
);
|
|
|
|
if (this.isInputMethod) {
|
|
return;
|
|
}
|
|
|
|
if (this.manifest) {
|
|
var that = this;
|
|
that.element.addEventListener('_opened', function onOpened() {
|
|
that.element.removeEventListener('_opened', onOpened);
|
|
that.appChrome = new AppChrome(that);
|
|
|
|
// Some signals that chrome needs to respond to can occur before
|
|
// chrome has loaded - in those cases, manually call the handlers.
|
|
if (that.inError) {
|
|
that.appChrome.handleEvent({type: 'mozbrowsererror'});
|
|
}
|
|
if (that.loading) {
|
|
that.appChrome.handleEvent({type: 'mozbrowserloadstart'});
|
|
that.appChrome.handleEvent({type: '_loading'});
|
|
}
|
|
if (that.metachangeDetails && that.metachangeDetails.length) {
|
|
that.metachangeDetails.forEach(function(detail) {
|
|
that.appChrome.handleEvent({type: 'mozbrowsermetachange',
|
|
detail: detail});
|
|
});
|
|
that.metachangeDetails = [];
|
|
}
|
|
});
|
|
} else {
|
|
this.appChrome = new AppChrome(this);
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype.uninstallSubComponents =
|
|
function aw_uninstallSubComponents() {
|
|
for (var componentName in this.constructor.SUB_COMPONENTS) {
|
|
if (this[componentName] && this[componentName].destroy) {
|
|
this[componentName].destroy();
|
|
}
|
|
this[componentName] = null;
|
|
}
|
|
|
|
for (var propertyName in this.constructor.SUB_MODULES) {
|
|
if (this[propertyName] && this[propertyName].stop) {
|
|
this[propertyName].stop();
|
|
}
|
|
this[propertyName] = null;
|
|
}
|
|
|
|
this._unregisterAudioChannels();
|
|
|
|
if (this.appChrome) {
|
|
this.appChrome.destroy();
|
|
this.appChrome = null;
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._registerAudioChannels =
|
|
function aw_registerAudioChannels() {
|
|
this.audioChannels = new Map();
|
|
var audioChannels = this.browser.element.allowedAudioChannels;
|
|
audioChannels && audioChannels.forEach((audioChannel) => {
|
|
this.audioChannels.set(
|
|
audioChannel.name,
|
|
new AudioChannelController(this, audioChannel)
|
|
);
|
|
this.debug('Registered ' + audioChannel.name + ' audio channel');
|
|
});
|
|
};
|
|
|
|
AppWindow.prototype._unregisterAudioChannels =
|
|
function aw_unregisterAudioChannels() {
|
|
if (this.audioChannels) {
|
|
this.audioChannels.forEach(function(audioChannel) {
|
|
audioChannel.destroy();
|
|
});
|
|
this.audioChannels = null;
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle__localized = function aw__handle__localized() {
|
|
if (!this.manifest) {
|
|
return;
|
|
}
|
|
this.name = new ManifestHelper(this.manifest).displayName;
|
|
|
|
if (this.identificationTitle) {
|
|
this.identificationTitle.textContent = this.name;
|
|
}
|
|
|
|
// For uitest.
|
|
this.element.dataset.localizedName = this.name;
|
|
this.publish('namechanged');
|
|
};
|
|
|
|
AppWindow.prototype._handle__orientationchange = function(evt) {
|
|
if (this.isActive()) {
|
|
this.frontWindow &&
|
|
this.frontWindow.broadcast('orientationchange');
|
|
|
|
if (!this.isHomescreen) {
|
|
this._resize(evt.detail);
|
|
return;
|
|
// XXX: Preventing orientaiton of homescreen app is changed by background
|
|
// app. It's a workaround for bug 1089951.
|
|
// It should be remove once bug 1043102 is done.
|
|
} else if (Service.query('getTopMostWindow') &&
|
|
Service.query('getTopMostWindow') === this) {
|
|
this.lockOrientation();
|
|
}
|
|
}
|
|
|
|
var width = Service.query('LayoutManager.width');
|
|
var height = Service.query('getHeightFor', this);
|
|
this.element.style.width = width + 'px';
|
|
this.element.style.height = height + 'px';
|
|
|
|
// The homescreen doesn't have an identification overlay
|
|
if (this.isHomescreen) {
|
|
return;
|
|
}
|
|
|
|
// If the screenshot doesn't match the new orientation hide it
|
|
if (this.width != width) {
|
|
this.screenshotOverlay.style.visibility = 'hidden';
|
|
} else {
|
|
this.screenshotOverlay.style.visibility = '';
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowservisibilitychange =
|
|
function aw__handle_mozbrowservisibilitychange(evt) {
|
|
var type = evt.detail.visible ? 'foreground' : 'background';
|
|
this.publish(type);
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowseractivitydone =
|
|
function aw__handle_mozbrowseractivitydone(evt) {
|
|
// In case we're not ActivityWindow but we're launched
|
|
// as window disposition activity.
|
|
if (this.isActive() && this.callerWindow) {
|
|
var caller = this.callerWindow;
|
|
var callerBottom = caller.getBottomMostWindow();
|
|
var calleeBottom = this.getBottomMostWindow();
|
|
caller.calleeWindow = null;
|
|
this.callerWindow = null;
|
|
// No transition when the callee is caller
|
|
if (callerBottom !== calleeBottom) {
|
|
callerBottom.open('in-from-left');
|
|
calleeBottom.close('out-to-right');
|
|
}
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowserclose =
|
|
function aw__handle_mozbrowserclose(evt) {
|
|
this.kill();
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowsererror =
|
|
function aw__handle_mozbrowsererror(evt) {
|
|
if (evt.detail.type !== 'fatal') {
|
|
this.inError = true;
|
|
return;
|
|
}
|
|
// Send event instead of call crash reporter directly.
|
|
this.publish('crashed');
|
|
|
|
// Only appWindow needs to be revived.
|
|
if (Service.query('suspendingAppWindow') &&
|
|
this.CLASS_NAME === 'AppWindow' && !this.isActive()) {
|
|
this.debug(' ..sleep! I will come back.');
|
|
this.destroyBrowser();
|
|
if (this.frontWindow) {
|
|
this.frontWindow.kill();
|
|
}
|
|
} else {
|
|
this.isCrashed = true;
|
|
this.kill(evt);
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowserloadstart =
|
|
function aw__handle_mozbrowserloadstart(evt) {
|
|
this.loading = true;
|
|
this.inError = false;
|
|
this._changeState('loading', true);
|
|
this.nameChanged = false;
|
|
this.publish('loading');
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowsertitlechange =
|
|
function aw__handle_handle_mozbrowsertitlechange(evt) {
|
|
this.title = evt.detail;
|
|
this.publish('titlechange');
|
|
|
|
// Do not set the identification title if we're browsing a URL privately
|
|
if (this.identificationTitle && !this.manifest &&
|
|
(!this.isPrivateBrowser() || this.config.url.startsWith('app:'))) {
|
|
this.identificationTitle.textContent = this.title;
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowserloadend =
|
|
function aw__handle_mozbrowserloadend(evt) {
|
|
if (!this.loaded) {
|
|
// Perf test needs.
|
|
this.publish('loadtime', {
|
|
time: parseInt(Date.now() - this.launchTime),
|
|
timestamp: this.timestamp,
|
|
type: 'c',
|
|
src: this.config.url
|
|
});
|
|
}
|
|
this.loading = false;
|
|
this.loaded = true;
|
|
this.element.classList.add('render');
|
|
// Bug 1043408 - Marionette tests relies on the render class of the
|
|
// iframe parent in order to starts. So let's replicate the 'render'
|
|
// class on browser-container until the proper patch on the external
|
|
// repo.
|
|
this.browserContainer.classList.add('render');
|
|
// Force removing background image.
|
|
this.element.style.backgroundImage = 'none';
|
|
this._changeState('loading', false);
|
|
this.publish('loaded');
|
|
var backgroundColor = evt.detail.backgroundColor;
|
|
this.debug('bgcolor= ', backgroundColor);
|
|
/* When rotating the screen, the child may take some time to reflow.
|
|
* If the child takes longer than layers.orientation.sync.timeout
|
|
* to respond, gecko will go ahead and draw anyways. This code
|
|
* uses a simple heuristic to guess the least distracting color
|
|
* we should draw in the blank space. */
|
|
|
|
/* Only allow opaque colors */
|
|
// TODOEVME - this kept throwing errors when homescreen launched,
|
|
// bgcolor was null
|
|
if (backgroundColor && backgroundColor.indexOf('rgb(') != -1 &&
|
|
!this.isHomescreen) {
|
|
this.debug('setting background color..');
|
|
this.browser.element.style.backgroundColor = backgroundColor;
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowserlocationchange =
|
|
function aw__handle_mozbrowserlocationchange(evt) {
|
|
// Gecko 52 includes navigation flags alongside the URL.
|
|
var url = typeof evt.detail === 'string' ? evt.detail : evt.detail.url;
|
|
this.browser.element.dataset.url = url;
|
|
if (this.config.url !== url) {
|
|
this.title = url;
|
|
this.favicons = {};
|
|
this.webManifestURL = null;
|
|
this.webManifest = null;
|
|
this.meta = {};
|
|
this.config.url = url;
|
|
if (!this.manifest && !this.nameChanged) {
|
|
this.name = new URL(url).hostname;
|
|
}
|
|
}
|
|
this.publish('locationchange');
|
|
};
|
|
|
|
|
|
AppWindow.prototype._handle_mozbrowsericonchange =
|
|
function aw__handle_mozbrowsericonchange(evt) {
|
|
var href = evt.detail.href;
|
|
var sizes = evt.detail.sizes;
|
|
|
|
if (!('favicons' in this)) {
|
|
this.favicons = {};
|
|
}
|
|
|
|
if (!(href in this.favicons)) {
|
|
this.favicons[href] = {sizes: []};
|
|
}
|
|
|
|
if (sizes && this.favicons[href].sizes.indexOf(sizes) === -1) {
|
|
this.favicons[href].sizes.push(sizes);
|
|
}
|
|
|
|
if (this.identificationIcon && !this.isPrivateBrowser()) {
|
|
this.identificationIcon.style.backgroundImage =
|
|
'url("' + evt.detail.href + '")';
|
|
}
|
|
this.publish('iconchange');
|
|
};
|
|
|
|
|
|
// We should create a helper in order to share network
|
|
// related methods.
|
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1230082
|
|
AppWindow.prototype.getBlobFromURL = function getBlobFromURL(url) {
|
|
return new Promise((resolve, reject) => {
|
|
var xhr = new XMLHttpRequest({
|
|
mozAnon: true,
|
|
mozSystem: true
|
|
});
|
|
|
|
xhr.open('GET', url, true);
|
|
xhr.responseType = 'blob';
|
|
xhr.send();
|
|
|
|
xhr.onload = () => {
|
|
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
|
|
var blob = xhr.response;
|
|
resolve(blob);
|
|
|
|
return;
|
|
}
|
|
|
|
reject(new Error(
|
|
`Got HTTP status ${xhr.status} trying to load ${url}.`));
|
|
};
|
|
|
|
xhr.onerror = xhr.ontimeout = () => {
|
|
reject(new Error(`Error while getting ${url}.`));
|
|
};
|
|
});
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowsermetachange =
|
|
function aw__handle_mozbrowsermetachange(evt) {
|
|
|
|
var detail = evt.detail;
|
|
this.meta = this.meta || {};
|
|
|
|
if (!this.appChrome) {
|
|
this.metachangeDetails = this.metachangeDetails || [];
|
|
this.metachangeDetails.push(detail);
|
|
}
|
|
|
|
switch (detail.name) {
|
|
case 'theme-group':
|
|
if (!detail.type) {
|
|
return;
|
|
}
|
|
|
|
if (detail.type === 'removed') {
|
|
themeGroups.forEach((group) => {
|
|
this.element.classList.remove(group);
|
|
});
|
|
} else {
|
|
var group = detail.content;
|
|
if (themeGroups.indexOf(group) !== -1) {
|
|
this.element.classList.add(group);
|
|
}
|
|
}
|
|
|
|
break;
|
|
|
|
case 'og:image':
|
|
this.getBlobFromURL(new URL(detail.content, this.config.url).href).then((blob) => {
|
|
this.meta[detail.name] = blob;
|
|
this.publish('metachange');
|
|
}, error => console.warn('Optional page preview unavailable: ' + error.message));
|
|
break;
|
|
|
|
case 'og:site_name':
|
|
this.meta[detail.name] = detail.content;
|
|
if (this.nameChanged) {
|
|
break;
|
|
}
|
|
/* falls through */
|
|
case 'application-name':
|
|
// Apps have a compulsory name field in their manifest
|
|
// which takes precedence.
|
|
var name = detail.content || '';
|
|
var emptyString = !(name.trim());
|
|
if (this.manifestURL || this.webManifestURL || emptyString) {
|
|
return;
|
|
}
|
|
this.name = name;
|
|
this.nameChanged = true;
|
|
this.publish('namechanged');
|
|
break;
|
|
|
|
default:
|
|
this.meta[detail.name] = detail.content;
|
|
this.publish('metachange');
|
|
break;
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle_mozbrowsermanifestchange =
|
|
function aw__handle_mozbrowsermanifestchange(evt) {
|
|
if (!evt.detail.href) {
|
|
return;
|
|
}
|
|
this.webManifestURL = evt.detail.href;
|
|
this._updateManifest(this.webManifestURL);
|
|
};
|
|
|
|
AppWindow.prototype._updateManifest =
|
|
function aw_updateManifest(manifestURL) {
|
|
return LazyLoader.load('/shared/js/web_manifest_helper.js').then(() => {
|
|
return WebManifestHelper.getManifest(manifestURL);
|
|
}).then((webManifest) => {
|
|
this.webManifest = webManifest;
|
|
if (webManifest.short_name || webManifest.name) {
|
|
this.name = webManifest.short_name || webManifest.name;
|
|
this.publish('namechanged');
|
|
}
|
|
}).catch(() => {
|
|
console.error('Failed to get web manifest from ' + manifestURL);
|
|
});
|
|
};
|
|
|
|
AppWindow.prototype._registerEvents = function aw__registerEvents() {
|
|
if (this.element === null) {
|
|
this._dump();
|
|
return;
|
|
}
|
|
this.constructor.REGISTERED_EVENTS.forEach(function iterator(evt) {
|
|
this.debug('adding ' + evt + ' event handler ...');
|
|
this.element.addEventListener(evt, this);
|
|
}, this);
|
|
};
|
|
|
|
/**
|
|
* General event handler interface.
|
|
* Child classes shouldn't change this.
|
|
* @param {DOMEvent} evt The event.
|
|
*/
|
|
AppWindow.prototype.handleEvent = function aw_handleEvent(evt) {
|
|
// We are rendering inline activities + popup inside this element too,
|
|
// so we need to prevent ourselves to be affected
|
|
// by the mozbrowser events of the callee.
|
|
|
|
// WebAPI testing is using mozbrowserloadend event to know
|
|
// the first app is loaded so we cannot stop the propagation here.
|
|
// but we don't want subsequent mozbrowser events to bubble to the
|
|
// used-to-be-rear-window
|
|
if (this.rearWindow && evt.type.startsWith('mozbrowser')) {
|
|
evt.stopPropagation();
|
|
}
|
|
this.debug(' Handling ' + evt.type + ' event...');
|
|
if (this['_handle_' + evt.type]) {
|
|
this['_handle_' + evt.type](evt);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* A temp variable to store current screenshot blob.
|
|
* We should store the blob and create objectURL
|
|
* once we need to display the image,
|
|
* and revoke right away after we finish rendering the image.
|
|
*/
|
|
AppWindow.prototype._screenshotBlob = undefined;
|
|
|
|
AppWindow.prototype.CLASS_NAME = 'AppWindow';
|
|
|
|
AppWindow.prototype.debug = function aw_debug(msg) {
|
|
if (DEBUG || this._DEBUG) {
|
|
console.log('[' + this.CLASS_NAME + ']' +
|
|
'[' + (this.name || this.origin) + ']' +
|
|
'[' + this.instanceID + ']' +
|
|
'[' + Service.currentTime() + '] ' +
|
|
Array.slice(arguments).concat());
|
|
|
|
if (TRACE) {
|
|
console.trace();
|
|
}
|
|
} else if (window.DUMP) {
|
|
DUMP('[' + this.CLASS_NAME + ']' +
|
|
'[' + (this.name || this.origin) + ']' +
|
|
'[' + this.instanceID + ']' +
|
|
'[' + Service.currentTime() + '] ' +
|
|
Array.slice(arguments).concat());
|
|
}
|
|
};
|
|
|
|
// Force debug output.
|
|
AppWindow.prototype.forceDebug = function aw_debug(msg) {
|
|
console.log('[Dump:' + this.CLASS_NAME + ']' +
|
|
'[' + (this.name || this.origin) + ']' +
|
|
'[' + Service.currentTime() + ']' +
|
|
Array.slice(arguments).concat());
|
|
};
|
|
|
|
AppWindow.prototype.show = function aw_show() {
|
|
if (!this.element || !this.element.classList.contains('hidden')) {
|
|
return;
|
|
}
|
|
this.element.classList.remove('hidden');
|
|
this.publish('shown');
|
|
};
|
|
|
|
AppWindow.prototype.hide = function aw_hide() {
|
|
if (!this.element || this.element.classList.contains('hidden')) {
|
|
return;
|
|
}
|
|
this.debug('hidden the entire app window.');
|
|
this.element.classList.add('hidden');
|
|
this.publish('hidden');
|
|
};
|
|
|
|
AppWindow.prototype.queueShow = function aw_queueShow() {
|
|
this.element.classList.add('will-become-active');
|
|
// bug 1033921: notify current app changed
|
|
this.publish('will-become-active');
|
|
};
|
|
|
|
AppWindow.prototype.cancelQueuedShow = function aw_cancelQueuedShow() {
|
|
this.element.classList.remove('will-become-active');
|
|
};
|
|
|
|
AppWindow.prototype.queueHide = function aw_queueHide() {
|
|
this.element.classList.add('will-become-inactive');
|
|
this.publish('will-become-inactive');
|
|
};
|
|
|
|
/**
|
|
* Wait for a full repaint of the mozbrowser iframe.
|
|
*/
|
|
AppWindow.prototype.tryWaitForFullRepaint = function onTWFRepaint(callback) {
|
|
if (!callback) {
|
|
return;
|
|
}
|
|
|
|
if (this.isHomescreen) {
|
|
setTimeout(callback);
|
|
return;
|
|
}
|
|
|
|
this.debug('trying wait for full repaint by screenshot enforcing..');
|
|
|
|
this.getScreenshot(function() {
|
|
setTimeout(callback);
|
|
}, 1, 1, 400);
|
|
};
|
|
|
|
/**
|
|
* Request a screenshot ObjectURL temporarily.
|
|
* The image would be discarded after 200ms or the revoke callback
|
|
* is invoked.
|
|
*/
|
|
AppWindow.prototype.requestScreenshotURL =
|
|
function aw__requestScreenshotURL() {
|
|
if (this.frontWindow) {
|
|
return this.frontWindow.requestScreenshotURL();
|
|
}
|
|
if (!this._screenshotBlob) {
|
|
this.debug('requestScreenshotURL, no _screenshotBlob');
|
|
return null;
|
|
}
|
|
var screenshotURL = URL.createObjectURL(this._screenshotBlob);
|
|
setTimeout(function onTimeout() {
|
|
if (screenshotURL) {
|
|
URL.revokeObjectURL(screenshotURL);
|
|
screenshotURL = null;
|
|
}
|
|
}, 200);
|
|
|
|
return screenshotURL;
|
|
};
|
|
|
|
/**
|
|
* Show screenshot overlay and hide the iframe.
|
|
*/
|
|
AppWindow.prototype._showScreenshotOverlay =
|
|
function aw__showScreenshotOverlay() {
|
|
if (this.frontWindow && this.frontWindow.isActive()) {
|
|
return this.frontWindow._showScreenshotOverlay();
|
|
}
|
|
if (!this.screenshotOverlay ||
|
|
this.screenshotOverlay.classList.contains('visible')) {
|
|
return Promise.resolve();
|
|
}
|
|
if (this.identificationOverlay) {
|
|
this.element.classList.add('overlay');
|
|
}
|
|
|
|
this.screenshotOverlay.classList.add('visible');
|
|
|
|
// will be null if there is no blob
|
|
var screenshotURL = this.requestScreenshotURL();
|
|
|
|
if (!screenshotURL) {
|
|
this.element.classList.add('no-screenshot');
|
|
this.screenshotOverlay.style.backgroundImage = 'none';
|
|
return Promise.resolve();
|
|
}
|
|
|
|
// return promise to make sure the image is ready
|
|
var promise = new Promise((resolve) => {
|
|
var image = document.createElement('img');
|
|
image.onload = function() {
|
|
resolve();
|
|
}.bind(this);
|
|
image.src = screenshotURL;
|
|
});
|
|
this.screenshotOverlay.style.backgroundImage =
|
|
'url(' + screenshotURL + ')';
|
|
this.element.classList.remove('no-screenshot');
|
|
return promise;
|
|
};
|
|
|
|
/**
|
|
* Check if current visibility state is screenshot or not,
|
|
* to hide the screenshot overlay.
|
|
*/
|
|
AppWindow.prototype._hideScreenshotOverlay =
|
|
function aw__hideScreenshotOverlay() {
|
|
if (this.frontWindow && this.frontWindow.isActive()) {
|
|
this.frontWindow._hideScreenshotOverlay();
|
|
}
|
|
if (!this.screenshotOverlay ||
|
|
!this.screenshotOverlay.classList.contains('visible')) {
|
|
return;
|
|
}
|
|
|
|
this.screenshotOverlay.classList.remove('visible');
|
|
this.screenshotOverlay.style.backgroundImage = '';
|
|
this.element.classList.remove('no-screenshot');
|
|
|
|
if (this.identificationOverlay) {
|
|
var element = this.element;
|
|
// A white flash can occur when removing the screenshot
|
|
// so we trigger this transition after a tick to hide it.
|
|
setTimeout(function nextTick() {
|
|
element.classList.remove('overlay');
|
|
});
|
|
}
|
|
};
|
|
|
|
// Get cached screenshot Blob if there is one.
|
|
// Note: the caller should revoke the created ObjectURL once it's finishing.
|
|
AppWindow.prototype.getCachedScreenshotBlob =
|
|
function aw_getCachedScreenshotBlob() {
|
|
return this._screenshotBlob;
|
|
};
|
|
|
|
// Save and update screenshot Blob.
|
|
AppWindow.prototype.renewCachedScreenshotBlob =
|
|
function aw_renewScreenshot(screenshotBlob) {
|
|
this._screenshotBlob = screenshotBlob;
|
|
};
|
|
|
|
/**
|
|
* Event prefix presents the object type
|
|
* when publishing an event from the element.
|
|
* Always 'app' for now.
|
|
*
|
|
* @type {String}
|
|
*/
|
|
AppWindow.prototype.eventPrefix = 'app';
|
|
|
|
/**
|
|
* Publish an event.
|
|
*
|
|
* @param {String} event Event name, without object type prefix.
|
|
* @param {Object} detail Parameters in JSON format.
|
|
*/
|
|
AppWindow.prototype.publish = function(event, detail) {
|
|
// Dispatch internal event before external events.
|
|
this.broadcast(event, detail);
|
|
var evt = new CustomEvent(this.eventPrefix + event,
|
|
{
|
|
bubbles: true,
|
|
detail: detail || this
|
|
});
|
|
|
|
this.debug('publishing external event: ' + event +
|
|
(detail ? JSON.stringify(detail) : ''));
|
|
|
|
// Publish external event.
|
|
if (this.rearWindow && this.element) {
|
|
this.element.dispatchEvent(evt);
|
|
} else {
|
|
window.dispatchEvent(evt);
|
|
}
|
|
|
|
// The other module could have all kind of window events
|
|
window.dispatchEvent(new CustomEvent('window' + event, {
|
|
bubbles: true,
|
|
detail: this
|
|
}));
|
|
};
|
|
|
|
AppWindow.prototype.broadcast = function aw_broadcast(event, detail) {
|
|
// Broadcast internal event.
|
|
if (this.element) {
|
|
var internalEvent = new CustomEvent('_' + event,
|
|
{
|
|
bubbles: false,
|
|
detail: detail || this
|
|
});
|
|
|
|
this.debug('publishing internal event: ' + event);
|
|
this.element.dispatchEvent(internalEvent);
|
|
}
|
|
};
|
|
|
|
var OrientationRotationArray = [
|
|
'portrait-primary', 'portrait-secondary', 'portrait',
|
|
'landscape-primary', 'landscape-secondary', 'landscape', 'default'];
|
|
|
|
function findRotationDegree(orientation1, orientation2) {
|
|
var OrientationRotationTable = {
|
|
'portrait-primary': [0, 180, 0, 90,
|
|
270, 90, Service.query('isDefaultPortrait') ? 0 : 90],
|
|
'landscape-primary': [270, 90, 270, 0,
|
|
180, 0, Service.query('isDefaultPortrait') ? 270 : 0],
|
|
'portrait-secondary': [180, 0, 180, 270,
|
|
90, 270, Service.query('isDefaultPortrait') ? 180 : 270],
|
|
'landscape-secondary': [90, 270, 90, 180,
|
|
0, 180, Service.query('isDefaultPortrait') ? 180 : 90]
|
|
};
|
|
return OrientationRotationTable[orientation1][orientation2];
|
|
}
|
|
|
|
AppWindow.prototype.determineRotationDegree =
|
|
function aw__determineRotationDegree() {
|
|
if (!this.manifest) {
|
|
return 0;
|
|
}
|
|
|
|
var appOrientation = this.manifest.orientation;
|
|
var orientation = this.determineOrientation(appOrientation);
|
|
var degree = findRotationDegree(
|
|
Service.query('defaultOrientation') || 'portrait-primary',
|
|
OrientationRotationArray.indexOf(orientation));
|
|
this.rotatingDegree = degree;
|
|
if (degree == 90 || degree == 270) {
|
|
this.element.classList.add('perpendicular');
|
|
}
|
|
return degree;
|
|
};
|
|
|
|
AppWindow.prototype.determineClosingRotationDegree =
|
|
function aw__determineClosingRotationDegree() {
|
|
if (!this.manifest) {
|
|
return 0;
|
|
}
|
|
|
|
// XXX: Assume homescreen's orientation is just device default.
|
|
var homeOrientation = Service.query('defaultOrientation');
|
|
var currentOrientation = Service.query('fetchCurrentOrientation');
|
|
this.debug(currentOrientation);
|
|
var degree = findRotationDegree(homeOrientation,
|
|
OrientationRotationArray.indexOf(currentOrientation));
|
|
return Math.abs(360 - degree) % 360;
|
|
};
|
|
|
|
/**
|
|
* Detect whether this is a full screen app by its manifest.
|
|
* @return {Boolean} We're a fullscreen app or not.
|
|
*/
|
|
AppWindow.prototype.isFullScreen = function aw_isFullScreen() {
|
|
if (typeof(this._fullScreen) !== 'undefined') {
|
|
return this._fullScreen;
|
|
}
|
|
// Fullscreen
|
|
this._fullScreen = !!(this.manifest &&
|
|
('fullscreen' in this.manifest ? this.manifest.fullscreen : false)) ||
|
|
this.isFullScreenLayout();
|
|
return this._fullScreen;
|
|
};
|
|
|
|
/**
|
|
* Detect whether this app is resized 100% width and height by its manifest.
|
|
* @return {Boolean} We're a fullscreen_layout app or not.
|
|
*/
|
|
AppWindow.prototype.isFullScreenLayout = function aw_isFullScreenLayout() {
|
|
if (typeof(this._fullScreenLayout) !== 'undefined') {
|
|
return this._fullScreenLayout;
|
|
}
|
|
// Fullscreen
|
|
this._fullScreenLayout = !!(this.manifest &&
|
|
('fullscreen_layout' in this.manifest ? this.manifest.fullscreen_layout :
|
|
false));
|
|
return this._fullScreenLayout;
|
|
};
|
|
|
|
AppWindow.prototype._defaultOrientation = null;
|
|
|
|
AppWindow.prototype.determineOrientation =
|
|
function aw_determineOrientation(orientation) {
|
|
if (this._defaultOrientation) {
|
|
return this._defaultOrientation;
|
|
} else if (!orientation) {
|
|
this._defaultOrientation = 'default';
|
|
return this._defaultOrientation;
|
|
}
|
|
|
|
if (!Array.isArray(orientation)) {
|
|
orientation = [orientation];
|
|
}
|
|
|
|
this._defaultOrientation = orientation[0];
|
|
|
|
return this._defaultOrientation;
|
|
};
|
|
|
|
AppWindow.prototype._resize = function aw__resize(ignoreKeyboard) {
|
|
var height, width;
|
|
this.debug('force RESIZE...');
|
|
if (!ignoreKeyboard && Service.query('keyboardEnabled')) {
|
|
/**
|
|
* The event is dispatched on the app window only when keyboard is up.
|
|
*
|
|
* @access private
|
|
* @event AppWindow~_withkeyboard
|
|
*/
|
|
this.broadcast('withkeyboard');
|
|
} else {
|
|
/**
|
|
* The event is dispatched on the app window only when keyboard is hidden.
|
|
*
|
|
* @access private
|
|
* @event AppWindow~_withoutkeyboard
|
|
*/
|
|
this.broadcast('withoutkeyboard');
|
|
}
|
|
height = Service.query('getHeightFor', this, ignoreKeyboard) ||
|
|
window.innerHeight;
|
|
|
|
// If we have sidebar in the future, change layoutManager then.
|
|
width = Service.query('LayoutManager.width') || window.innerWidth;
|
|
|
|
if (parseInt(this.element.style.width, 10) === (width | 0) &&
|
|
parseInt(this.element.style.height, 10) === (height | 0)) {
|
|
return;
|
|
}
|
|
|
|
this.width = width;
|
|
this.height = height;
|
|
|
|
this.element.style.width = width + 'px';
|
|
this.element.style.height = height + 'px';
|
|
|
|
this.reviveBrowser();
|
|
|
|
this.resized = true;
|
|
if (this.screenshotOverlay) {
|
|
this.screenshotOverlay.style.visibility = '';
|
|
}
|
|
|
|
if (this.modalDialog && this.modalDialog.isVisible()) {
|
|
this.modalDialog.updateMaxHeight();
|
|
}
|
|
|
|
/**
|
|
* Fired when the app is resized.
|
|
*
|
|
* @event AppWindow#appresize
|
|
*/
|
|
this.publish('resize');
|
|
this.debug('W:', width, 'H:', height);
|
|
|
|
return this.waitForNextPaint();
|
|
};
|
|
|
|
/**
|
|
* Set the size of the app's iframe to match the size of the screen.
|
|
* We have to call this on resize events (which happen when the
|
|
* phone orientation is changed). And also when an app is launched
|
|
* and each time an app is brought to the front, since the
|
|
* orientation could have changed since it was last displayed
|
|
*
|
|
* An appWindow instance only resizes if it's active.
|
|
* Since we already pre-config the size in window.css,
|
|
* if an app is launched by system message we don't need to resize
|
|
* it.
|
|
*
|
|
* 
|
|
*/
|
|
AppWindow.prototype.resize = function aw_resize() {
|
|
if (this.isDead()) {
|
|
return;
|
|
}
|
|
this.debug('request RESIZE...active? ', this.isActive());
|
|
var bottom = this.getBottomMostWindow();
|
|
if (!bottom.shouldResize() || this.isTransitioning()) {
|
|
return Promise.resolve();
|
|
}
|
|
if (this.frontWindow) {
|
|
return Promise.all(
|
|
[this._resize(), this.frontWindow.resize()]);
|
|
} else {
|
|
// resize myself if no child.
|
|
this.debug(' will resize... ');
|
|
return this._resize();
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype.getTopMostWindow = function() {
|
|
var win = this;
|
|
while (win.frontWindow) {
|
|
win = win.frontWindow;
|
|
}
|
|
|
|
return win;
|
|
};
|
|
|
|
AppWindow.prototype.getBottomMostWindow = function() {
|
|
var win = this;
|
|
|
|
while (win.rearWindow) {
|
|
win = win.rearWindow;
|
|
}
|
|
|
|
return win;
|
|
};
|
|
|
|
/**
|
|
* Lock the orientation for this app anyway.
|
|
*/
|
|
AppWindow.prototype.lockOrientation = function(forceOrientation) {
|
|
var manifest = this.manifest || this.config.manifest;
|
|
var orientation = forceOrientation ||
|
|
manifest && manifest.orientation ||
|
|
Service.query('globalOrientation');
|
|
if (orientation) {
|
|
var rv = screen.mozLockOrientation(orientation);
|
|
|
|
if (rv === false) {
|
|
console.warn('screen.mozLockOrientation() returned false for',
|
|
this.origin, 'orientation', orientation);
|
|
} else {
|
|
this.debug(' locking screen orientation to ' + orientation);
|
|
}
|
|
} else { // If no orientation was requested, then let it rotate
|
|
screen.mozUnlockOrientation();
|
|
this.debug(' Unlocking screen orientation..');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Lock or unlock orientation for this app.
|
|
* Note: if we have front window, the request would be delivered
|
|
* to the front active window.
|
|
*/
|
|
AppWindow.prototype.setOrientation =
|
|
function aw_setOrientation() {
|
|
if (!this.getBottomMostWindow().isActive()) {
|
|
return;
|
|
}
|
|
if (this.frontWindow && this.frontWindow.isActive()) {
|
|
this.frontWindow.setOrientation();
|
|
} else {
|
|
this.lockOrientation();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* fade out to window to black.
|
|
*/
|
|
AppWindow.prototype.fadeOut = function aw__fadeout() {
|
|
if (!this.isActive() && this.element) {
|
|
this.element.classList.add('fadeout');
|
|
this.debug(' fade out >>>> ');
|
|
}
|
|
};
|
|
|
|
/**
|
|
* fade in the window from black.
|
|
*/
|
|
AppWindow.prototype.fadeIn = function aw__fadein() {
|
|
if (this.isActive()) {
|
|
this.element.classList.remove('fadeout');
|
|
this.debug(' fade in <<<<< ');
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype.setCalleeWindow =
|
|
function aw_setCalleeWindow(callee) {
|
|
this.calleeWindow = callee;
|
|
callee.callerWindow = this;
|
|
};
|
|
|
|
AppWindow.prototype.unsetCalleeWindow =
|
|
function aw_unsetCalleeWindow() {
|
|
if (this.calleeWindow.callerWindow) {
|
|
this.calleeWindow.callerWindow = null;
|
|
}
|
|
this.calleeWindow = null;
|
|
};
|
|
|
|
AppWindow.prototype.unsetAttentionWindow =
|
|
function aw_unsetAttentionWindow() {
|
|
this.attentionWindow = null;
|
|
};
|
|
|
|
/**
|
|
* Modify an attribute on this.element
|
|
* @param {String} type State type.
|
|
* @param {String} state State name.
|
|
*/
|
|
AppWindow.prototype._changeState = function aw__changeState(type, state) {
|
|
if (this.element) {
|
|
this.element.setAttribute(type + '-state', state.toString());
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Mixin the appWindow prototype with {mixin} object.
|
|
* @param {Object} mixin The object to be mixed.
|
|
*/
|
|
AppWindow.addMixin = function AW_addMixin(mixin) {
|
|
for (var prop in mixin) {
|
|
if (mixin.hasOwnProperty(prop)) {
|
|
// Put event handler function into an array,
|
|
// if the name of the propery is '_on'.
|
|
if (!this.prototype.hasOwnProperty(prop)) {
|
|
this.prototype[prop] = mixin[prop];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype.preloadSplash = function aw_preloadSplash() {
|
|
if (this._splash || this.config.icon) {
|
|
if (this.config.icon) {
|
|
this._splash = this.config.icon;
|
|
} else {
|
|
// origin might contain a pathname too, so need to parse it to find the
|
|
// "real origin"
|
|
var url = (this.config.origin || location.origin).split('/');
|
|
var origin = url[0] + '//' + url[2];
|
|
this._splash = origin + this._splash;
|
|
}
|
|
// Start to load the image in background to avoid flickering if possible.
|
|
var img = new Image();
|
|
img.src = this._splash;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The preferred CSS size of the icon used for cold launch splash for phones.
|
|
*/
|
|
AppWindow.prototype.SPLASH_ICON_SIZE_TINY = 60;
|
|
|
|
/**
|
|
* The preferred CSS size of the icon used for cold launch splash for
|
|
* other devices.
|
|
*/
|
|
AppWindow.prototype.SPLASH_ICON_SIZE_NOT_TINY = 90;
|
|
|
|
AppWindow.prototype.getIconForSplash =
|
|
function aw_getIconForSplash(manifest) {
|
|
if (this.isPrivate) {
|
|
var privateIconPath = '/style/icons/pb_icon.png';
|
|
this._splash = privateIconPath;
|
|
this.preloadSplash();
|
|
return privateIconPath;
|
|
}
|
|
|
|
var icons = this.manifest ?
|
|
('icons' in this.manifest ? this.manifest.icons : null) : null;
|
|
if (!icons) {
|
|
return null;
|
|
}
|
|
|
|
var targetedPixelSize = 2 * (ScreenLayout.getCurrentLayout('tiny') ?
|
|
this.SPLASH_ICON_SIZE_TINY : this.SPLASH_ICON_SIZE_NOT_TINY) *
|
|
Math.ceil(window.devicePixelRatio || 1);
|
|
|
|
var preferredSize = Number.MAX_VALUE;
|
|
var max = 0;
|
|
|
|
for (var size in icons) {
|
|
size = parseInt(size, 10);
|
|
if (size > max) {
|
|
max = size;
|
|
}
|
|
|
|
if (size >= targetedPixelSize && size < preferredSize) {
|
|
preferredSize = size;
|
|
}
|
|
}
|
|
// If there is an icon matching the preferred size, we return the result,
|
|
// if there isn't, we will return the maximum available size.
|
|
if (preferredSize === Number.MAX_VALUE) {
|
|
preferredSize = max;
|
|
}
|
|
|
|
this._splash = icons[preferredSize];
|
|
this.preloadSplash();
|
|
return icons[preferredSize];
|
|
};
|
|
|
|
/**
|
|
* Set splash as our inital background.
|
|
* If the content is loaded then just skip.
|
|
*/
|
|
AppWindow.prototype.setFrameBackground =
|
|
function aw_setFrameBackground() {
|
|
if (!this.isHomescreen &&
|
|
!this.loaded && !this.splashed && this.element) {
|
|
|
|
if (this._splash) {
|
|
this.splashed = true;
|
|
this.element.style.backgroundImage = 'url("' + this._splash + '")';
|
|
|
|
var iconCSSSize = 2 * (ScreenLayout.getCurrentLayout('tiny') ?
|
|
this.SPLASH_ICON_SIZE_TINY : this.SPLASH_ICON_SIZE_NOT_TINY);
|
|
this.element.style.backgroundSize =
|
|
iconCSSSize + 'px ' + iconCSSSize + 'px';
|
|
|
|
if (this.identificationIcon) {
|
|
this.identificationIcon.style.backgroundImage =
|
|
'url("' + this._splash + '")';
|
|
}
|
|
}
|
|
|
|
if (this.identificationTitle) {
|
|
this.identificationTitle.textContent = this.name;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* The app window is requesting to be opened.
|
|
*
|
|
* @fires AppWindow#apprequestopen
|
|
*/
|
|
AppWindow.prototype.requestOpen = function aw_requestOpen() {
|
|
/**
|
|
* Fired once we'd like to be opened.
|
|
* The one who makes decision to call open() would be AppWindowManager.
|
|
*
|
|
* @event AppWindow#apprequestopen
|
|
*/
|
|
this.publish('requestopen');
|
|
};
|
|
|
|
/**
|
|
* The app window is requesting to be closed.
|
|
*
|
|
* @fires AppWindow#apprequestclose
|
|
*/
|
|
AppWindow.prototype.requestClose = function aw_requestClose() {
|
|
/**
|
|
* Fired once we'd like to be closed.
|
|
* The one who makes decision to call close() would be AppWindowManager.
|
|
*
|
|
* @event AppWindow#apprequestclose
|
|
*/
|
|
this.publish('requestclose');
|
|
};
|
|
|
|
/**
|
|
* Change URL only when we're at background.
|
|
* @param {String} url URL.
|
|
*/
|
|
AppWindow.prototype.modifyURLatBackground = function aw_changeURL(url) {
|
|
// If the app is in foreground, it's too risky to change it's
|
|
// URL. We'll ignore this request.
|
|
if (!this.isActive()) {
|
|
var iframe = this.browser.element;
|
|
// If the app is opened and it is loaded to the correct page,
|
|
// then th=ere is nothing to do.
|
|
if (iframe.src !== url) {
|
|
// Rewrite the URL of the app frame to the requested URL.
|
|
// XXX: We could ended opening URls not for the app frame
|
|
// in the app frame. But we don't care.
|
|
iframe.src = url;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Do resize and/or ensure repaint before opening the app.
|
|
* @param {Function} callback Callback when app is ready to be opened.
|
|
*/
|
|
AppWindow.prototype.ready = function aw_ready(callback) {
|
|
if (!this.element) {
|
|
return;
|
|
}
|
|
|
|
this.debug('requesting to open');
|
|
|
|
if (!this.loaded ||
|
|
(this.screenshotOverlay &&
|
|
this.screenshotOverlay.classList.contains('visible'))) {
|
|
this.debug('loaded yet');
|
|
setTimeout(callback);
|
|
return;
|
|
}
|
|
|
|
var invoked = false;
|
|
this.waitForNextPaint(function() {
|
|
if (invoked) {
|
|
return;
|
|
}
|
|
invoked = true;
|
|
setTimeout(callback);
|
|
});
|
|
if (this.isHomescreen) {
|
|
return;
|
|
}
|
|
this.tryWaitForFullRepaint(function() {
|
|
if (invoked) {
|
|
return;
|
|
}
|
|
invoked = true;
|
|
setTimeout(callback);
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Open the window; the detail is done in appTransitionController.
|
|
* @param {String} animation The animation class name.
|
|
*/
|
|
AppWindow.prototype.open = function aw_open(animation) {
|
|
// Request "open" to our internal transition controller.
|
|
if (this.transitionController) {
|
|
this.transitionController.requireOpen(animation);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Close the window; the detail is done in appTransitionController.
|
|
* @param {String} animation The animation class name.
|
|
*/
|
|
AppWindow.prototype.close = function aw_close(animation) {
|
|
// Request "close" to our internal transition controller.
|
|
if (this.transitionController) {
|
|
this.transitionController.requireClose(animation);
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle__swipein = function aw_swipein() {
|
|
if (this.isCrashed) {
|
|
if (this.transitionController) {
|
|
this.transitionController.clearTransitionClasses();
|
|
}
|
|
return;
|
|
}
|
|
// Revive the browser element if it's got killed in background.
|
|
this.reviveBrowser();
|
|
// Request "open" to our internal transition controller.
|
|
if (this.transitionController) {
|
|
this.transitionController.switchTransitionState('opened');
|
|
this.publish('opening');
|
|
this.publish('opened');
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle__swipeout = function aw_swipeout() {
|
|
// Request "close" to our internal transition controller.
|
|
if (this.transitionController) {
|
|
this.transitionController.switchTransitionState('closed');
|
|
this.publish('closing');
|
|
this.publish('closed');
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle__sheetdisplayed = function aw_sheetdisplayed() {
|
|
// If we're the active we shouldn't do anything at this point. We'll get
|
|
// our frame hidden on swipeout.
|
|
if (this.isActive()) {
|
|
this.debug('no screenshot for active app during sheetdisplayed');
|
|
return;
|
|
}
|
|
|
|
// For inactive apps we'll already have a screenshot blob ready for use.
|
|
this.debug('showing screenshot during sheetdisplayed');
|
|
this._showScreenshotOverlay();
|
|
};
|
|
|
|
AppWindow.prototype._handle__sheetsgestureend = function aw_sgend() {
|
|
this.debug('hiding screenshot on sheetsgestureend');
|
|
this._hideScreenshotOverlay();
|
|
};
|
|
|
|
AppWindow.prototype._handle__cardviewbeforeshow = function aw_cvbeforeshow() {
|
|
if (this.isActive()) {
|
|
// Hopefully update the screenshot in time for the cards view to show
|
|
this.debug('requesting, then showing screenshot for cardsview.');
|
|
this.getScreenshot(this._showScreenshotOverlay.bind(this));
|
|
} else {
|
|
this.debug('showing screenshot for cardsview.');
|
|
this._showScreenshotOverlay();
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle__cardviewshown = function aw_cvshown() {
|
|
this._showScreenshotOverlay();
|
|
if (this.element && this.element.classList.contains('no-screenshot') &&
|
|
this._screenshotBlob) {
|
|
this.element.classList.remove('no-screenshot');
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle__cardviewclosed = function aw_cvclosed() {
|
|
this.debug('hiding screenshot after cardsview closed.');
|
|
this._hideScreenshotOverlay();
|
|
};
|
|
|
|
AppWindow.prototype._handle__closed = function aw_closed() {
|
|
if (!this.loaded ||
|
|
this.getBottomMostWindow().isHomescreen) {
|
|
// We will eventually get screenshot when being requested from
|
|
// task manager.
|
|
return;
|
|
}
|
|
|
|
// Update screenshot blob here to avoid slowing down closing transitions.
|
|
this.getScreenshot();
|
|
};
|
|
|
|
AppWindow.prototype._handle__kill_suspended = function aw() {
|
|
if (this.suspended) {
|
|
this.kill();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Handles shrinkingstart event broadcasted from ShrinkingUI. Gets the current
|
|
* screenshot, once it's available shows screenshot overlay and hides itself.
|
|
* @memberOf AppWindow.prototype
|
|
*/
|
|
AppWindow.prototype._handle__shrinkingstart = function aw_shrinkingstart() {
|
|
this.broadcast('blur');
|
|
this.getScreenshot(() => {
|
|
this._showScreenshotOverlay().then(() => {
|
|
this.setVisible(false);
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Handles shrinkingstop event broadcasted from ShrinkingUI.
|
|
* Shows itself automatically hiding screenshot overlay.
|
|
* @memberOf AppWindow.prototype
|
|
*/
|
|
AppWindow.prototype._handle__shrinkingstop = function aw_shrinkingstop() {
|
|
this.setVisible(true);
|
|
this.broadcast('focus');
|
|
};
|
|
|
|
/**
|
|
* Link child window to this app window instance.
|
|
* If there's already one, kill it at first.
|
|
* @param {ChildWindow} childWindow The child window instance.
|
|
*/
|
|
AppWindow.prototype.setNextWindow = function aw_setNextWindow(nextWindow) {
|
|
if (this.nextWindow) {
|
|
console.warn('There is already alive child window, killing...',
|
|
this.nextWindow.instanceID);
|
|
this.nextWindow.kill();
|
|
}
|
|
this.nextWindow = nextWindow;
|
|
};
|
|
|
|
AppWindow.prototype.unsetNextWindow = function aw_unsetNextWindow() {
|
|
if (this.nextWindow) {
|
|
this.nextWindow = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Build bottom/top window relationship.
|
|
* If there's already one, kill it at first.
|
|
* @param {AppWindow} fronWindow The front window instance.
|
|
*/
|
|
AppWindow.prototype.setFrontWindow = function aw_setFrontWindow(frontWindow) {
|
|
if (this.frontWindow) {
|
|
console.warn('There is already alive child window, killing...',
|
|
this.frontWindow.instanceID);
|
|
this.frontWindow.kill();
|
|
}
|
|
this.frontWindow = frontWindow;
|
|
};
|
|
|
|
AppWindow.prototype.unsetFrontWindow = function aw_unsetFrontWindow() {
|
|
if (this.frontWindow) {
|
|
this.frontWindow = null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the previous window reference of current active one.
|
|
* @return {AppWindow} The previous one in the app sheet chain.
|
|
*/
|
|
AppWindow.prototype.getPrev = function() {
|
|
var current = this.getActiveWindow();
|
|
if (current) {
|
|
return current.previousWindow;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the next window reference of current active one.
|
|
* @return {AppWindow} The next one in the app sheet chain.
|
|
*/
|
|
AppWindow.prototype.getNext = function() {
|
|
var current = this.getActiveWindow();
|
|
if (current) {
|
|
return current.nextWindow;
|
|
} else {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the active window reference in the app sheet chain.
|
|
* If there's no active window, return null.
|
|
* @return {AppWindow} The active window in the app sheet chain.
|
|
*/
|
|
AppWindow.prototype.getActiveWindow = function() {
|
|
var app = this;
|
|
while (app) {
|
|
if (app.isActive()) {
|
|
return app;
|
|
}
|
|
app = app.nextWindow;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Get the root window reference.
|
|
* If there's no parent, return ourself.
|
|
* @return {AppWindow} The first one in the app sheet chain.
|
|
*/
|
|
AppWindow.prototype.getRootWindow = function() {
|
|
var app = this;
|
|
while (app.previousWindow) {
|
|
app = app.previousWindow;
|
|
}
|
|
return app;
|
|
};
|
|
|
|
/**
|
|
* Get the leaf window reference.
|
|
* If there's no child, return ourself.
|
|
* @return {AppWindow} The last one in the app sheet chain.
|
|
*/
|
|
AppWindow.prototype.getLeafWindow = function() {
|
|
var app = this;
|
|
while (app.nextWindow) {
|
|
app = app.nextWindow;
|
|
}
|
|
return app;
|
|
};
|
|
|
|
AppWindow.prototype.getFrameForScreenshot = function() {
|
|
var top = this.getTopMostWindow();
|
|
return top.browser ? top.browser.element : null;
|
|
};
|
|
|
|
/**
|
|
* Indicate we are in viewport or not.
|
|
* @return {Boolean} We are in viewport or outside viewport.
|
|
*/
|
|
AppWindow.prototype.isVisible = function() {
|
|
var bottomMostWindow = this.getBottomMostWindow();
|
|
return bottomMostWindow.isActive() && this.isActive();
|
|
};
|
|
|
|
/**
|
|
* Make adjustments to display inside the task manager
|
|
*/
|
|
AppWindow.prototype.enterTaskManager = function aw_enterTaskManager() {
|
|
this._dirtyStyleProperties = {};
|
|
if (this.element && this.transitionController) {
|
|
this.element.classList.add('in-task-manager');
|
|
this.close( this.isActive() ? 'to-cardview' : 'immediate' );
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove adjustments made to display inside the task manager
|
|
*/
|
|
AppWindow.prototype.leaveTaskManager = function aw_leaveTaskManager() {
|
|
if (this.element) {
|
|
this.element.classList.remove('in-task-manager');
|
|
if (this._dirtyStyleProperties) {
|
|
this.unapplyStyle(this._dirtyStyleProperties);
|
|
this._dirtyStyleProperties = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Apply a transform to the element
|
|
* @param {Object} nameValues object with transform property names as keys
|
|
* and values to apply to the element
|
|
* @memberOf AppWindow.prototype
|
|
*/
|
|
AppWindow.prototype.transform = function(nameValues) {
|
|
var strFunctions = Object.keys(nameValues).map(function(key) {
|
|
return key + '(' + nameValues[key] + ')';
|
|
}, this).join(' ');
|
|
this.applyStyle({ MozTransform: strFunctions });
|
|
};
|
|
|
|
/**
|
|
* Batch apply style properties
|
|
* @param {Object} nameValues object with style property names as keys
|
|
* and values to apply to the element
|
|
* @memberOf AppWindow.prototype
|
|
*/
|
|
AppWindow.prototype.applyStyle = function(nameValues) {
|
|
var dirty = this._dirtyStyleProperties || (this._dirtyStyleProperties = {});
|
|
var style = this.element.style;
|
|
for (var property in nameValues) {
|
|
if (undefined === nameValues[property]) {
|
|
delete style[[property]];
|
|
} else {
|
|
style[property] = nameValues[property];
|
|
}
|
|
dirty[property] = true;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Remove inline style properties
|
|
* @param {Object} nameValues object with style property names as keys
|
|
* @memberOf AppWindow.prototype
|
|
*/
|
|
AppWindow.prototype.unapplyStyle = function(nameValues) {
|
|
var style = this.element.style;
|
|
for (var pname in nameValues) {
|
|
style[pname] = '';
|
|
delete style[pname];
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Show the default contextmenu for an AppWindow
|
|
* @memberOf AppWindow.prototype
|
|
*/
|
|
AppWindow.prototype.showDefaultContextMenu = function() {
|
|
if (this.contextmenu) {
|
|
this.contextmenu.showDefaultMenu();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Hide the contextmenu for an AppWindow
|
|
* @memberOf AppWindow.prototype
|
|
*/
|
|
AppWindow.prototype.hideContextMenu = function() {
|
|
if (this.contextmenu) {
|
|
this.contextmenu.hide();
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype._handle__blur = function() {
|
|
var win = this;
|
|
while (win.frontWindow && win.frontWindow.isActive()) {
|
|
win = win.frontWindow;
|
|
}
|
|
win.blur();
|
|
};
|
|
|
|
AppWindow.prototype._handle__focus = function() {
|
|
var win = this;
|
|
while (win.frontWindow && win.frontWindow.isActive()) {
|
|
win = win.frontWindow;
|
|
}
|
|
win.focus();
|
|
};
|
|
|
|
/**
|
|
* Request to be foreground to the visibilityManager.
|
|
* If it's fine to let us to be active, this.setVisible(true) will be called.
|
|
*/
|
|
AppWindow.prototype.requestForeground =
|
|
function aw_requestForeground() {
|
|
this.publish('requestforeground');
|
|
};
|
|
|
|
/**
|
|
* The window is killable by user manual action or not.
|
|
* @return {Boolean} The app is killable or not.
|
|
*/
|
|
AppWindow.prototype.killable = function() {
|
|
// This property is updated whenever an attentionWindow
|
|
// is created or destroyed.
|
|
if (this.attentionWindow || this.isHomescreen) {
|
|
return false;
|
|
} else {
|
|
return true;
|
|
}
|
|
};
|
|
|
|
AppWindow.prototype.hasPermission =
|
|
function aw_hasPermission(name) {
|
|
if (typeof(this._hasPermission) !== 'undefined' &&
|
|
this._hasPermission[name]) {
|
|
return this._hasPermission[name];
|
|
}
|
|
|
|
var mozPerms = navigator.mozPermissionSettings;
|
|
if (!mozPerms || !this.manifestURL) {
|
|
return false;
|
|
}
|
|
|
|
var value =
|
|
mozPerms.get(name, this.manifestURL, this.origin, false);
|
|
|
|
if (!this._hasPermission) {
|
|
this._hasPermission = {};
|
|
}
|
|
this._hasPermission[name] = (value === 'allow');
|
|
return this._hasPermission[name];
|
|
};
|
|
|
|
AppWindow.prototype.isHidden = function() {
|
|
return !this.element || this.element.classList.contains('hidden');
|
|
};
|
|
|
|
/**
|
|
* _hidewindow event handler.
|
|
*
|
|
* The event occurs when there's a higher priority window
|
|
* which is not an AppWindow show up.
|
|
* AppWindowManager will redirect the event to the current
|
|
* active app.
|
|
*
|
|
* If the event is because of attention window coming,
|
|
* evt.detail will be the instance of the attention window.
|
|
* If we are the opener of the attention window,
|
|
* we should not be sent to background due to
|
|
* @param {Event} evt The hidewindow event
|
|
*/
|
|
AppWindow.prototype._handle__hidewindow = function (evt) {
|
|
var attention = evt.detail;
|
|
if (attention.parentWindow &&
|
|
attention.parentWindow.instanceID === this.instanceID) {
|
|
return;
|
|
}
|
|
if (!this.isActive()) {
|
|
return;
|
|
}
|
|
this.setVisible(false);
|
|
};
|
|
|
|
|
|
/**
|
|
* Track current SSL state of the browser.
|
|
*/
|
|
AppWindow.prototype._sslState = '';
|
|
|
|
/**
|
|
* Handle mozbrowsersecuritychange events from the browser
|
|
*/
|
|
AppWindow.prototype._handle_mozbrowsersecuritychange =
|
|
function aw__handle_mozbrowsersecuritychange(evt) {
|
|
var state = this._sslState = evt.detail.state;
|
|
this.publish('securitychange', state);
|
|
};
|
|
|
|
/**
|
|
* Get the current SSL state for the browser
|
|
*/
|
|
AppWindow.prototype.getSSLState = function() {
|
|
return this._sslState;
|
|
};
|
|
|
|
|
|
/**
|
|
* Statusbar will bypass touch event to us via this method
|
|
* @param {Object} evt Touch event object
|
|
* @param {Number} barHeight The height of the statusbar
|
|
*/
|
|
AppWindow.prototype.handleStatusbarTouch = function(evt, barHeight) {
|
|
if (this.statusbar) {
|
|
this.statusbar.handleStatusbarTouch(evt, barHeight);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Return a promise that resolves to an icon URL.
|
|
*
|
|
* @param {number?} iconSize
|
|
* @returns {Promise}
|
|
*/
|
|
AppWindow.prototype.getSiteIconUrl =
|
|
function ac_getSiteIconUrl(iconSize = ICON_SIZE) {
|
|
var siteObj = {};
|
|
|
|
if (this.webManifestURL && this.webManifest) {
|
|
siteObj.webManifestUrl = this.webManifestURL;
|
|
siteObj.webManifest = this.webManifest;
|
|
}
|
|
|
|
if (this.manifest && this.manifest.icons) {
|
|
//Getting the icons from the FirefoxOS manifest
|
|
if (!this.manifest.origin) {
|
|
this.manifest.origin = new URL(this.manifestURL).origin;
|
|
}
|
|
siteObj.manifestUrl = this.manifestURL;
|
|
siteObj.manifest = this.manifest;
|
|
siteObj.origin = new URL(this.origin).origin;
|
|
}
|
|
|
|
if (this.webManifestURL && !this.webManifest) {
|
|
this.debug('getSiteIconUrl: Manifest not loaded yet.');
|
|
}
|
|
|
|
return this.getIconBlob(this.config.url, iconSize,
|
|
{icons: this.favicons},
|
|
siteObj);
|
|
};
|
|
|
|
/**
|
|
* Return a promise resolving to an icon blob.
|
|
*
|
|
* @param {string} origin
|
|
* @param {number} iconSize
|
|
* @param {Object?} placeObj
|
|
* @param {Object?} siteObj
|
|
* @returns {Promise}
|
|
*/
|
|
AppWindow.prototype.getIconBlob = function ac_getIconBlob(origin, iconSize,
|
|
placeObj = {}, siteObj = {}) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
LazyLoader.load('/shared/js/icons_helper.js').then(() => {
|
|
IconsHelper.getIconBlob(origin, iconSize, placeObj, siteObj)
|
|
.then(iconObject => {
|
|
resolve({
|
|
blob: iconObject.blob,
|
|
originalUrl: iconObject.originalUrl,
|
|
url: 'url(' + URL.createObjectURL(iconObject.blob) + ')'
|
|
});
|
|
})
|
|
.catch(err => {
|
|
reject(err);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
exports.AppWindow = AppWindow;
|
|
}(window));
|