VulpesOS/assets/webapps/system.gaiamobile.org/js/app_window.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

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
* ![appWindow Life cycle state machine](http://i.imgur.com/ELuEopw.png)
*
* @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 resize flow chart](http://i.imgur.com/bUMm4VM.png)
*/
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));