/* 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 `
`; }; /** * 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));