diff options
Diffstat (limited to 'apps/system/js/window_manager.js')
-rw-r--r-- | apps/system/js/window_manager.js | 2011 |
1 files changed, 2011 insertions, 0 deletions
diff --git a/apps/system/js/window_manager.js b/apps/system/js/window_manager.js new file mode 100644 index 0000000..e53605e --- /dev/null +++ b/apps/system/js/window_manager.js @@ -0,0 +1,2011 @@ +/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- / +/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ + +// +// This file calls getElementById without waiting for an onload event, so it +// must have a defer attribute or be included at the end of the <body>. +// +// This module is responsible for launching apps and for allowing +// the user to switch among apps and kill apps. Specifically, it handles: +// launching apps, +// killing apps +// keeping track of the set of running apps (which we call tasks here) +// keeping track of which task is displayed (the foreground task) +// changing the foreground task +// hiding all apps to display the homescreen +// displaying the app switcher to allow the user to switch and kill apps +// performing appropriate transition animations between: +// the homescreen and an app +// the homescreen and the switcher +// an app and the homescreen +// the switcher and the homescreen +// the switcher and the current foreground task +// the switcher and a different task +// Handling Home key events to switch to the homescreen and the switcher +// +// The public API of the module is small. It defines an WindowManager object +// with these methods: +// +// launch(origin): switch to the specified running app +// kill(origin, callback): stop specified app +// reload(origin): reload the given app +// getDisplayedApp(): return the origin of the currently displayed app +// setOrientationForApp(origin): set the phone to orientation to a given app +// getAppFrame(origin): returns the iframe element for the specified origin +// which is assumed to be running. This is only currently used +// for tests and chrome stuff: see the end of the file +// getRunningApps(): get the app references of the running apps. +// +// TODO +// The "origin" does not actually refer to app's origin but rather a identifier +// of the app reference that one gets from |getDisplayedApp()| or +// iterates |getRunningApps|. The string is make up of the specified +// launching entry point, origin, or the website url launched by wrapper. +// It would be ideal if the variable get correctly named and it's rule is being +// properly documented. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=796629 +// + +var WindowManager = (function() { + 'use strict'; + + function debug(str) { + dump('WindowManager: ' + str + '\n'); + } + + // Holds the origin of the home screen, which should be the first + // app we launch through web activity during boot + var homescreen = null; + var homescreenURL = ''; + var homescreenManifestURL = ''; + var ftu = null; + var ftuManifestURL = ''; + var ftuURL = ''; + var isRunningFirstRunApp = false; + // keep the reference of inline activity frame here + var inlineActivityFrames = []; + var activityCallerOrigin = ''; + + // Some document elements we use + var windows = document.getElementById('windows'); + var screenElement = document.getElementById('screen'); + var wrapperHeader = document.querySelector('#wrapper-activity-indicator'); + var wrapperFooter = document.querySelector('#wrapper-footer'); + var kTransitionTimeout = 1000; + + // Set this to true to debugging the transitions and state change + var slowTransition = false; + if (slowTransition) { + windows.classList.add('slow-transition'); + } + + // + // The set of running apps. + // This is a map from app origin to an object like this: + // { + // name: the app's name + // manifest: the app's manifest object + // frame: the iframe element that the app is displayed in + // launchTime: last time when app gets active + // } + // + var runningApps = {}; + var numRunningApps = 0; // appendFrame() and removeFrame() maintain this count + var nextAppId = 0; // to give each app's iframe a unique id attribute + + // The origin of the currently displayed app, or null if there isn't one + var displayedApp = null; + + // Function to hide init starting logo + function handleInitlogo(callback) { + var initlogo = document.getElementById('initlogo'); + initlogo.classList.add('hide'); + initlogo.addEventListener('transitionend', function delInitlogo() { + initlogo.removeEventListener('transitionend', delInitlogo); + initlogo.parentNode.removeChild(initlogo); + if (callback) { + callback(); + } + }); + }; + + // Public function. Return the origin of the currently displayed app + // or null if there is none. + function getDisplayedApp() { + return displayedApp || null; + } + + function requireFullscreen(origin) { + var app = runningApps[origin]; + if (!app) + return false; + + var manifest = app.manifest; + if (manifest.entry_points && manifest.type == 'certified') { + var entryPoint = manifest.entry_points[origin.split('/')[3]]; + if (entryPoint) + return entryPoint.fullscreen; + return false; + } else { + return manifest.fullscreen; + } + } + + // Make the specified app the displayed app. + // Public function. Pass null to make the homescreen visible + function launch(origin) { + // If the origin is indeed valid we make that app as the displayed app. + if (isRunning(origin)) { + setDisplayedApp(origin); + return; + } + + // If the origin is null, make the homescreen visible. + if (origin == null) { + setDisplayedApp(homescreen); + return; + } + + // At this point, we have no choice but to show the homescreen. + // We cannot launch/relaunch a given app based on the "origin" because + // we would need the manifest URL and the specific entry point. + console.warn('No running app is being identified as "' + origin + '". ' + + 'Showing home screen instead.'); + setDisplayedApp(homescreen); + } + + function isRunning(origin) { + return runningApps.hasOwnProperty(origin); + } + + function getAppFrame(origin) { + if (isRunning(origin)) + return runningApps[origin].frame; + else + return null; + } + + // 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 + function setAppSize(origin, changeActivityFrame) { + var app = runningApps[origin]; + if (!app) + return; + + var frame = app.frame; + var manifest = app.manifest; + + var cssWidth = window.innerWidth + 'px'; + var cssHeight = window.innerHeight - StatusBar.height; + if ('wrapper' in frame.dataset) { + cssHeight -= 10; + } + cssHeight += 'px'; + + if (!screenElement.classList.contains('attention') && + requireFullscreen(origin)) { + cssHeight = window.innerHeight + 'px'; + } + + frame.style.width = cssWidth; + frame.style.height = cssHeight; + + // We will call setInlineActivityFrameSize() + // if changeActivityFrame is not explicitly set to false. + if (changeActivityFrame !== false) + setInlineActivityFrameSize(); + } + + // App's height is relevant to keyboard height + function setAppHeight(keyboardHeight) { + var app = runningApps[displayedApp]; + if (!app) + return; + + var frame = app.frame; + var manifest = app.manifest; + + var cssHeight = + window.innerHeight - StatusBar.height - keyboardHeight + 'px'; + + if (!screenElement.classList.contains('attention') && + requireFullscreen(displayedApp)) { + cssHeight = window.innerHeight - keyboardHeight + 'px'; + } + + frame.style.height = cssHeight; + + setInlineActivityFrameSize(); + } + + // Copy the dimension of the currently displayed app + function setInlineActivityFrameSize() { + if (!inlineActivityFrames.length) + return; + + var app = runningApps[displayedApp]; + var appFrame = app.frame; + var frame = inlineActivityFrames[inlineActivityFrames.length - 1]; + + frame.style.width = appFrame.style.width; + + if (document.mozFullScreen) { + frame.style.height = window.innerHeight + 'px'; + frame.style.top = '0px'; + } else { + if ('wrapper' in appFrame.dataset) { + frame.style.height = window.innerHeight - StatusBar.height + 'px'; + } else { + frame.style.height = appFrame.style.height; + } + frame.style.top = appFrame.offsetTop + 'px'; + } + } + + function setFrameBackgroundBlob(frame, blob, transparent) { + URL.revokeObjectURL(frame.dataset.bgObjectURL); + delete frame.dataset.bgObjectURL; + + var objectURL = URL.createObjectURL(blob); + frame.dataset.bgObjectURL = objectURL; + var backgroundCSS = + '-moz-linear-gradient(top, rgba(0,0,0,0.5) 0%, rgba(0,0,0,0.5) 100%),' + + 'url(' + objectURL + '),' + + ((transparent) ? 'transparent' : '#fff'); + + frame.style.background = backgroundCSS; + } + + function clearFrameBackground(frame) { + if (!('bgObjectURL' in frame.dataset)) + return; + + URL.revokeObjectURL(frame.dataset.bgObjectURL); + delete frame.dataset.bgObjectURL; + frame.style.background = ''; + } + + var openFrame = null; + var closeFrame = null; + var openCallback = null; + var closeCallback = null; + var transitionOpenCallback = null; + var transitionCloseCallback = null; + + // Use setOpenFrame() to reset the CSS classes set + // to the current openFrame (before overwriting the reference) + function setOpenFrame(frame) { + if (openFrame) { + removeFrameClasses(openFrame); + } + + openFrame = frame; + } + + // Use setCloseFrame() to reset the CSS classes set + // to the current closeFrame (before overwriting the reference) + function setCloseFrame(frame) { + if (closeFrame) { + removeFrameClasses(closeFrame); + // closeFrame should not be set to active + closeFrame.classList.remove('active'); + } + + closeFrame = frame; + } + + // Remove these visible className from frame so we will not ended + // up having a frozen frame in the middle of the transition + function removeFrameClasses(frame) { + var classNames = ['opening', 'closing', 'opening-switching', + 'opening-card', 'closing-card']; + + var classList = frame.classList; + + classNames.forEach(function removeClass(className) { + classList.remove(className); + }); + } + + windows.addEventListener('transitionend', function frameTransitionend(evt) { + var prop = evt.propertyName; + var frame = evt.target; + if (prop !== 'transform') + return; + + var classList = frame.classList; + + if (classList.contains('inlineActivity')) { + if (classList.contains('active')) { + if (openFrame) + openFrame.firstChild.focus(); + + setOpenFrame(null); + } else { + windows.removeChild(frame); + } + + return; + } + + if (screenElement.classList.contains('switch-app')) { + if (classList.contains('closing')) { + classList.remove('closing'); + classList.add('closing-card'); + + if (openFrame) { + if (openFrame.classList.contains('opening-card')) { + openFrame.classList.remove('opening-card'); + openFrame.classList.add('opening-switching'); + } else { + // Skip the opening-card and opening-switching transition + // because the closing-card transition had already finished here. + if (openFrame.classList.contains('fullscreen-app')) { + screenElement.classList.add('fullscreen-app'); + } + openFrame.classList.add('opening'); + } + } + } else if (classList.contains('closing-card')) { + windowClosed(frame); + setTimeout(closeCallback); + closeCallback = null; + + } else if (classList.contains('opening-switching')) { + // If the opening app need to be full screen, switch to full screen + if (classList.contains('fullscreen-app')) { + screenElement.classList.add('fullscreen-app'); + } + + classList.remove('opening-switching'); + classList.add('opening'); + } else if (classList.contains('opening')) { + windowOpened(frame); + + setTimeout(openCallback); + openCallback = null; + + setCloseFrame(null); + setOpenFrame(null); + screenElement.classList.remove('switch-app'); + } + + return; + } + + if (classList.contains('opening')) { + windowOpened(frame); + + setTimeout(openCallback); + openCallback = null; + + setOpenFrame(null); + } else if (classList.contains('closing')) { + windowClosed(frame); + + setTimeout(closeCallback); + closeCallback = null; + + setCloseFrame(null); + } + }); + + // Executes when the opening transition scale the app + // to full size. + function windowOpened(frame) { + var iframe = frame.firstChild; + + frame.classList.add('active'); + windows.classList.add('active'); + + if ('wrapper' in frame.dataset) { + wrapperFooter.classList.add('visible'); + } + + // Take the focus away from the currently displayed app + var app = runningApps[displayedApp]; + if (app && app.iframe) + app.iframe.blur(); + + // Give the focus to the frame + iframe.focus(); + + if (!TrustedUIManager.isVisible() && !isRunningFirstRunApp) { + // Set homescreen visibility to false + toggleHomescreen(false); + } + + // Set displayedApp to the new value + displayedApp = iframe.dataset.frameOrigin; + + // Set orientation for the new app + setOrientationForApp(displayedApp); + + // Dispatch an 'appopen' event. + var manifestURL = runningApps[displayedApp].manifestURL; + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent('appopen', true, false, { + manifestURL: manifestURL, + origin: displayedApp + }); + frame.dispatchEvent(evt); + } + + // Executes when app closing transition finishes. + function windowClosed(frame) { + var iframe = frame.firstChild; + + // If the FTU is closing, make sure we save this state + if (iframe.src == ftuURL) { + isRunningFirstRunApp = false; + document.getElementById('screen').classList.remove('ftu'); + window.asyncStorage.setItem('ftu.enabled', false); + // Done with FTU, letting everyone know + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent('ftudone', + /* canBubble */ true, /* cancelable */ false, {}); + window.dispatchEvent(evt); + } + + frame.classList.remove('active'); + windows.classList.remove('active'); + + // set the closed frame visibility to false + if ('setVisible' in iframe) + iframe.setVisible(false); + + screenElement.classList.remove('fullscreen-app'); + } + + // The following things needs to happen when firstpaint happens. + // We centralize all that here but not all of them applies. + windows.addEventListener('mozbrowserfirstpaint', function firstpaint(evt) { + var iframe = evt.target; + var frame = iframe.parentNode; + + // remove the unpainted flag + delete iframe.dataset.unpainted; + + setTimeout(function firstpainted() { + // Save the screenshot + // Remove the background only until we actually got the screenshot, + // because the getScreenshot() call will be pushed back by + // painting/loading in the child process; when we got the screenshot, + // that means the app is mostly loaded. + // (as opposed to plain white firstpaint) + saveAppScreenshot(frame, function screenshotTaken() { + // Remove the default background + frame.classList.remove('default-background'); + + // Remove the screenshot from frame + clearFrameBackground(frame); + }); + }); + }); + + // setFrameBackground() will attach the screenshot background to + // the given frame. + // The callback could be sync or async (depend on whether we need + // the screenshot from database or not) + function setFrameBackground(frame, callback, transparent) { + var iframe = frame.firstChild; + // If the frame is painted, or there is already background image present + // start the transition right away. + if (!('unpainted' in iframe.dataset) || + ('bgObjectURL' in frame.dataset)) { + callback(); + return; + } + + // Get the screenshot from the database + getAppScreenshotFromDatabase(iframe.src || iframe.dataset.frameOrigin, + function(screenshot) { + // If firstpaint is faster than database, we will not transition + // with screenshot. + if (!('unpainted' in iframe.dataset)) { + callback(); + return; + } + + if (!screenshot) { + // put a default background + frame.classList.add('default-background'); + callback(); + return; + } + + // set the screenshot as the background of the frame itself. + // we are safe to do so since there is nothing on it yet. + setFrameBackgroundBlob(frame, screenshot, transparent); + + // start the transition + callback(); + }); + } + + // On-disk database for window manager. + // It's only for app screenshots right now. + var database = null; + var DB_SCREENSHOT_OBJSTORE = 'screenshots'; + + (function openDatabase() { + var DB_VERSION = 2; + var DB_NAME = 'window_manager'; + + var req = window.indexedDB.open(DB_NAME, DB_VERSION); + req.onerror = function() { + console.error('Window Manager: opening database failed.'); + }; + req.onupgradeneeded = function databaseUpgradeneeded() { + database = req.result; + + if (database.objectStoreNames.contains(DB_SCREENSHOT_OBJSTORE)) + database.deleteObjectStore(DB_SCREENSHOT_OBJSTORE); + + var store = database.createObjectStore( + DB_SCREENSHOT_OBJSTORE, { keyPath: 'url' }); + }; + + req.onsuccess = function databaseSuccess() { + database = req.result; + }; + })(); + + function putAppScreenshotToDatabase(url, data) { + if (!database) + return; + + var txn = database.transaction(DB_SCREENSHOT_OBJSTORE, 'readwrite'); + txn.onerror = function() { + console.warn( + 'Window Manager: transaction error while trying to save screenshot.'); + }; + var store = txn.objectStore(DB_SCREENSHOT_OBJSTORE); + var req = store.put({ + url: url, + screenshot: data + }); + req.onerror = function(evt) { + console.warn( + 'Window Manager: put error while trying to save screenshot.'); + }; + } + + function getAppScreenshotFromDatabase(url, callback) { + if (!database) { + console.warn( + 'Window Manager: Neither database nor app frame is ' + + 'ready for getting screenshot.'); + + callback(); + return; + } + + var req = database.transaction(DB_SCREENSHOT_OBJSTORE) + .objectStore(DB_SCREENSHOT_OBJSTORE).get(url); + req.onsuccess = function() { + if (!req.result) { + console.log('Window Manager: No screenshot in database. ' + + 'This is expected from a fresh installed app.'); + callback(); + + return; + } + + callback(req.result.screenshot, true); + } + req.onerror = function(evt) { + console.warn('Window Manager: get screenshot from database failed.'); + callback(); + }; + } + + function deleteAppScreenshotFromDatabase(url) { + var txn = database.transaction(DB_SCREENSHOT_OBJSTORE); + var store = txn.objectStore(DB_SCREENSHOT_OBJSTORE); + + store.delete(url); + } + + function getAppScreenshotFromFrame(frame, callback) { + if (!frame) { + callback(); + return; + } + + var iframe = frame.firstChild; + var req = iframe.getScreenshot(iframe.offsetWidth, iframe.offsetHeight); + + req.onsuccess = function gotScreenshotFromFrame(evt) { + var result = evt.target.result; + callback(result, false); + }; + + req.onerror = function gotScreenshotFromFrameError(evt) { + console.warn('Window Manager: getScreenshot failed.'); + callback(); + }; + } + + // Meta method for get the screenshot from the app frame, + // and save it to database. + function saveAppScreenshot(frame, callback) { + getAppScreenshotFromFrame(frame, function gotScreenshot(screenshot) { + if (callback) + callback(screenshot); + + if (!screenshot) + return; + + var iframe = frame.firstChild; + putAppScreenshotToDatabase(iframe.src || iframe.dataset.frameOrigin, + screenshot); + }); + } + + // Perform an "open" animation for the app's iframe + function openWindow(origin, callback) { + var app = runningApps[origin]; + setOpenFrame(app.frame); + + openCallback = callback || function() {}; + + // set the size of the opening app + setAppSize(origin); + + if (origin === homescreen) { + // We cannot apply background screenshot to home screen app since + // the screenshot is encoded in JPEG and the alpha channel is + // not perserved. See + // https://bugzilla.mozilla.org/show_bug.cgi?id=801676#c33 + // If that resolves, + // setFrameBackground(openFrame, gotBackground, true); + // will simply work here. + + // Call the openCallback only once. We have to use tmp var as + // openCallback can be a method calling the callback + // (like the `removeFrame` callback in `kill()` ). + var tmpCallback = openCallback; + openCallback = null; + tmpCallback(); + + windows.classList.add('active'); + openFrame.classList.add('homescreen'); + openFrame.firstChild.focus(); + setOpenFrame(null); + displayedApp = origin; + + return; + } + + if (requireFullscreen(origin)) + screenElement.classList.add('fullscreen-app'); + + transitionOpenCallback = function startOpeningTransition() { + // We have been canceled by another transition. + if (!openFrame || transitionOpenCallback != startOpeningTransition) + return; + + // Make sure we're not called twice. + transitionOpenCallback = null; + + if (!screenElement.classList.contains('switch-app')) { + openFrame.classList.add('opening'); + } else if (!openFrame.classList.contains('opening')) { + openFrame.classList.add('opening-card'); + } + }; + + if ('unpainted' in openFrame.firstChild.dataset) { + waitForNextPaintOrBackground(openFrame, transitionOpenCallback); + } else { + waitForNextPaint(openFrame, transitionOpenCallback); + } + + // Set the frame to be visible. + if ('setVisible' in openFrame.firstChild) { + if (!AttentionScreen.isFullyVisible()) { + openFrame.firstChild.setVisible(true); + } else { + // If attention screen is fully visible now, + // don't give the open frame visible. + // This is the case that homescreen is restarted behind attention screen + openFrame.firstChild.setVisible(false); + } + } + } + + function waitForNextPaintOrBackground(frame, callback) { + var waiting = true; + function proceed() { + if (waiting) { + waiting = false; + callback(); + } + } + + waitForNextPaint(frame, proceed); + setFrameBackground(frame, proceed); + } + + function waitForNextPaint(frame, callback) { + function onNextPaint() { + clearTimeout(timeout); + callback(); + } + + var iframe = frame.firstChild; + + // Register a timeout in case we don't receive + // nextpaint in an acceptable time frame. + var timeout = setTimeout(function() { + if ('removeNextPaintListener' in iframe) + iframe.removeNextPaintListener(onNextPaint); + callback(); + }, kTransitionTimeout); + + if ('addNextPaintListener' in iframe) + iframe.addNextPaintListener(onNextPaint); + } + + // Perform a "close" animation for the app's iframe + function closeWindow(origin, callback) { + var app = runningApps[origin]; + setCloseFrame(app.frame); + closeCallback = callback || function() {}; + + // Animate the window close. Ensure the homescreen is in the + // foreground since it will be shown during the animation. + var homescreenFrame = ensureHomescreen(); + + // invoke openWindow to show homescreen here + openWindow(homescreen, null); + + // Take keyboard focus away from the closing window + closeFrame.firstChild.blur(); + + // set orientation for homescreen app + setOrientationForApp(homescreen); + + // Set the size of both homescreen app and the closing app + // since the orientation had changed. + setAppSize(homescreen); + setAppSize(origin); + + // Send a synthentic 'appwillclose' event. + // The keyboard uses this and the appclose event to know when to close + // See https://github.com/andreasgal/gaia/issues/832 + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent('appwillclose', true, false, { origin: origin }); + closeFrame.dispatchEvent(evt); + + if ('wrapper' in closeFrame.dataset) { + wrapperHeader.classList.remove('visible'); + wrapperFooter.classList.remove('visible'); + } + + transitionCloseCallback = function startClosingTransition() { + // We have been canceled by another transition. + if (!closeFrame || transitionCloseCallback != startClosingTransition) + return; + + // Make sure we're not called twice. + transitionCloseCallback = null; + + // Start the transition + closeFrame.classList.add('closing'); + closeFrame.classList.remove('active'); + }; + + waitForNextPaint(homescreenFrame, transitionCloseCallback); + } + + // Perform a "switching" animation for the closing frame and the opening frame + function switchWindow(origin, callback) { + // This will trigger different transition to both openWindow() + // and closeWindow() transition. + screenElement.classList.add('switch-app'); + + // Ask closeWindow() to start closing the displayedApp + closeWindow(displayedApp, callback); + + // Ask openWindow() to show a card on the right waiting to be opened + openWindow(origin); + } + + // Ensure the homescreen is loaded and return its frame. Restarts + // the homescreen app if it was killed in the background. + // Note: this function would not invoke openWindow(homescreen), + // which should be handled in setDisplayedApp and in closeWindow() + function ensureHomescreen(reset) { + // If the url of the homescreen is not known at this point do nothing. + if (!homescreen || !homescreenManifestURL) { + return null; + } + + if (!isRunning(homescreen)) { + var app = Applications.getByManifestURL(homescreenManifestURL); + appendFrame(null, homescreen, homescreenURL, + app.manifest.name, app.manifest, app.manifestURL); + runningApps[homescreen].iframe.dataset.start = Date.now(); + setAppSize(homescreen); + } else if (reset) { + runningApps[homescreen].iframe.src = homescreenURL; + setAppSize(homescreen); + } + + return runningApps[homescreen].frame; + } + + function retrieveHomescreen(callback) { + var lock = navigator.mozSettings.createLock(); + var setting = lock.get('homescreen.manifestURL'); + setting.onsuccess = function() { + var app = + Applications.getByManifestURL(this.result['homescreen.manifestURL']); + + // XXX This is a one-day workaround to not break everybody and make sure + // work can continue. + if (!app) { + var tmpURL = document.location.toString() + .replace('system', 'homescreen') + .replace('index.html', 'manifest.webapp'); + app = Applications.getByManifestURL(tmpURL); + } + + if (app) { + homescreenManifestURL = app.manifestURL; + homescreen = app.origin; + homescreenURL = app.origin + '/index.html#root'; + + callback(app); + } + } + } + + function skipFTU() { + document.getElementById('screen').classList.remove('ftuStarting'); + handleInitlogo(); + setDisplayedApp(homescreen); + } + + // Check if the FTU was executed or not, if not, get a + // reference to the app and launch it. + function retrieveFTU() { + window.asyncStorage.getItem('ftu.enabled', function getItem(launchFTU) { + document.getElementById('screen').classList.add('ftuStarting'); + if (launchFTU === false) { + skipFTU(); + return; + } + var lock = navigator.mozSettings.createLock(); + var req = lock.get('ftu.manifestURL'); + req.onsuccess = function() { + ftuManifestURL = this.result['ftu.manifestURL']; + if (!ftuManifestURL) { + dump('FTU manifest cannot be found skipping.\n'); + skipFTU(); + return; + } + ftu = Applications.getByManifestURL(ftuManifestURL); + if (!ftu) { + dump('Opps, bogus FTU manifest.\n'); + skipFTU(); + return; + } + ftuURL = ftu.origin + ftu.manifest.entry_points['ftu'].launch_path; + ftu.launch('ftu'); + }; + req.onerror = function() { + dump('Couldn\'t get the ftu manifestURL.\n'); + skipFTU(); + }; + }); + } + + // Hide current app + function hideCurrentApp(callback) { + if (displayedApp == null || displayedApp == homescreen) + return; + + toggleHomescreen(true); + var frame = getAppFrame(displayedApp); + frame.classList.add('back'); + frame.classList.remove('restored'); + if (callback) { + frame.addEventListener('transitionend', function execCallback() { + frame.style.visibility = 'hidden'; + frame.removeEventListener('transitionend', execCallback); + callback(); + }); + } + } + + // If app parameter is passed, + // it means there's a specific app needs to be restored + // instead of current app + function restoreCurrentApp(app) { + if (app) { + // Restore app visibility immediately but don't open it. + var frame = getAppFrame(app); + frame.style.visibility = 'visible'; + frame.classList.remove('back'); + } else { + app = displayedApp; + toggleHomescreen(false); + var frame = getAppFrame(app); + frame.style.visibility = 'visible'; + frame.classList.remove('back'); + frame.classList.add('restored'); + frame.addEventListener('transitionend', function removeRestored() { + frame.removeEventListener('transitionend', removeRestored); + frame.classList.remove('restored'); + }); + } + } + + function toggleHomescreen(visible) { + var homescreenFrame = ensureHomescreen(); + if (homescreenFrame && 'setVisible' in homescreenFrame.firstChild) + homescreenFrame.firstChild.setVisible(visible); + } + + // Switch to a different app + function setDisplayedApp(origin, callback) { + var currentApp = displayedApp, newApp = origin || homescreen; + var isFirstRunApplication = !currentApp && (origin == ftuURL); + + var homescreenFrame = null; + if (!isFirstRunApplication) { + // Returns the frame reference of the home screen app. + // Restarts the homescreen app if it was killed in the background. + homescreenFrame = ensureHomescreen(); + } + + // Cancel transitions waiting to be started. + transitionOpenCallback = null; + transitionCloseCallback = null; + + // Discard any existing activity + stopInlineActivity(true); + + // Before starting a new transition, let's make sure current transitions + // are stopped and the state classes are cleaned up. + // visibility status should also be reset. + if (openFrame && 'setVisible' in openFrame.firstChild) + openFrame.firstChild.setVisible(false); + if (closeFrame && 'setVisible' in closeFrame.firstChild) + closeFrame.firstChild.setVisible(false); + + if (!isFirstRunApplication && newApp == homescreen && !AttentionScreen.isFullyVisible()) { + toggleHomescreen(true); + } + + setOpenFrame(null); + setCloseFrame(null); + screenElement.classList.remove('switch-app'); + screenElement.classList.remove('fullscreen-app'); + + // Dispatch an appwillopen event only when we open an app + if (newApp != currentApp) { + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent('appwillopen', true, true, { origin: newApp }); + + var app = runningApps[newApp]; + // Allows listeners to cancel app opening and so stay on homescreen + if (!app.frame.dispatchEvent(evt)) { + if (typeof(callback) == 'function') + callback(); + return; + } + + var iframe = app.iframe; + + // unpainted means that the app is cold booting + // if it is, we're going to listen for Browser API's loadend event + // which indicates that the iframe's document load is complete + // + // if the app is not cold booting (is in memory) we will listen + // to appopen event, which is fired when the transition to the + // app window is complete. + // + // [w] - warm boot (app is in memory, just transition to it) + // [c] - cold boot (app has to be booted, we show it's document load + // time) + var type; + if ('unpainted' in iframe.dataset) { + type = 'mozbrowserloadend'; + } else { + iframe.dataset.start = Date.now(); + type = 'appopen'; + } + + app.frame.addEventListener(type, function apploaded(e) { + e.target.removeEventListener(e.type, apploaded); + + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent('apploadtime', true, false, { + time: parseInt(Date.now() - iframe.dataset.start), + type: (e.type == 'appopen') ? 'w' : 'c' + }); + iframe.dispatchEvent(evt); + }); + } + + // Case 1: the app is already displayed + if (currentApp && currentApp == newApp) { + if (newApp == homescreen) { + // relaunch homescreen + openWindow(homescreen, callback); + } else if (callback) { + // Just run the callback right away if it is not homescreen + callback(); + } + } + // Case 2: null --> app + else if (isFirstRunApplication) { + isRunningFirstRunApp = true; + openWindow(newApp, function windowOpened() { + handleInitlogo(function() { + var mainScreen = document.getElementById('screen'); + mainScreen.classList.add('ftu'); + mainScreen.classList.remove('ftuStarting'); + }); + }); + } + // Case 3: null->homescreen || homescreen->app + else if ((!currentApp && newApp == homescreen) || + (currentApp == homescreen && newApp)) { + openWindow(newApp, callback); + } + // Case 4: app->homescreen + else if (currentApp && currentApp != homescreen && newApp == homescreen) { + // For screenshot to catch current window size + closeWindow(currentApp, callback); + } + // Case 5: app-to-app transition + else { + switchWindow(newApp, callback); + } + // Set homescreen as active, + // to control the z-index between homescreen & keyboard iframe + if ((newApp == homescreen) && homescreenFrame) { + homescreenFrame.classList.add('active'); + } else { + homescreenFrame.classList.remove('active'); + } + + // Record the time when app was launched, + // need this to display apps in proper order on CardsView. + // We would also need this to determined the freshness of the frame + // for making screenshots. + if (newApp) + runningApps[newApp].launchTime = Date.now(); + + // If the app has a attention screen open, displaying it + AttentionScreen.showForOrigin(newApp); + } + + function setOrientationForApp(origin) { + if (origin == null) { // No app is currently running. + screen.mozLockOrientation('portrait-primary'); + return; + } + + var app = runningApps[origin]; + if (!app) + return; + var manifest = app.manifest; + + if (manifest.orientation) { + var rv = screen.mozLockOrientation(manifest.orientation); + if (rv === false) { + console.warn('screen.mozLockOrientation() returned false for', + origin, 'orientation', manifest.orientation); + } + } + else { // If no orientation was requested, then let it rotate + screen.mozUnlockOrientation(); + } + } + + var isOutOfProcessDisabled = false; + SettingsListener.observe('debug.oop.disabled', false, function(value) { + isOutOfProcessDisabled = value; + }); + + function createFrame(origFrame, origin, url, name, manifest, manifestURL) { + var iframe = origFrame || document.createElement('iframe'); + iframe.setAttribute('mozallowfullscreen', 'true'); + + var frame = document.createElement('div'); + frame.appendChild(iframe); + frame.className = 'appWindow'; + + iframe.dataset.frameOrigin = origin; + // Save original frame URL in order to restore it on frame load error + iframe.dataset.frameURL = url; + + // Note that we don't set the frame size here. That will happen + // when we display the app in setDisplayedApp() + + // frames are began unpainted. + iframe.dataset.unpainted = true; + + if (!manifestURL) { + frame.setAttribute('data-wrapper', 'true'); + return frame; + } + + // Most apps currently need to be hosted in a special 'mozbrowser' iframe. + // They also need to be marked as 'mozapp' to be recognized as apps by the + // platform. + iframe.setAttribute('mozbrowser', 'true'); + + // These apps currently have bugs preventing them from being + // run out of process. All other apps will be run OOP. + // + var outOfProcessBlackList = [ + 'Browser' + // Requires nested content processes (bug 761935). This is not + // on the schedule for v1. + ]; + + if (!isOutOfProcessDisabled && + outOfProcessBlackList.indexOf(name) === -1) { + // FIXME: content shouldn't control this directly + iframe.setAttribute('remote', 'true'); + } + + iframe.setAttribute('mozapp', manifestURL); + iframe.src = url; + return frame; + } + + function appendFrame(origFrame, origin, url, name, manifest, manifestURL) { + // Create the <iframe mozbrowser mozapp> that hosts the app + var frame = + createFrame(origFrame, origin, url, name, manifest, manifestURL); + var iframe = frame.firstChild; + frame.id = 'appframe' + nextAppId++; + iframe.dataset.frameType = 'window'; + + // Give a name to the frame for differentiating between main frame and + // inline frame. With the name we can get frames of the same app using the + // window.open method. + iframe.name = 'main'; + + // If this frame corresponds to the homescreen, set mozapptype=homescreen + // so we're less likely to kill this frame's process when we're running low + // on memory. + // + // We must do this before we the appendChild() call below. Once + // we add this frame to the document, we can't change its app type. + if (origin === homescreen) { + iframe.setAttribute('mozapptype', 'homescreen'); + } + + // Add the iframe to the document + windows.appendChild(frame); + + // And map the app origin to the info we need for the app + var app = new AppWindow({ + origin: origin, + name: name, + manifest: manifest, + manifestURL: manifestURL, + frame: frame, + iframe: iframe, + launchTime: 0 + }); + runningApps[origin] = app; + + if (requireFullscreen(origin)) { + frame.classList.add('fullscreen-app'); + } + if (origin === ftuURL) { + // Add a way to identify ftu app + // (Used by SimLock) + frame.classList.add('ftu'); + } + + // A frame should start with visible false + if ('setVisible' in iframe) + iframe.setVisible(false); + + numRunningApps++; + + return app; + } + + function startInlineActivity(origin, url, name, manifest, manifestURL) { + // Create the <iframe mozbrowser mozapp> that hosts the app + var frame = createFrame(null, origin, url, name, manifest, manifestURL); + var iframe = frame.firstChild; + frame.classList.add('inlineActivity'); + iframe.dataset.frameType = 'inline-activity'; + + // Give a name to the frame for differentiating between main frame and + // inline frame. With the name we can get frames of the same app using the + // window.open method. + iframe.name = 'inline'; + + // Save the reference + inlineActivityFrames.push(frame); + + // Set the size + setInlineActivityFrameSize(); + + // Add the iframe to the document + windows.appendChild(frame); + + // Open the frame, first, store the reference + openFrame = frame; + + // set the frame to visible state + if ('setVisible' in iframe) + iframe.setVisible(true); + + setFrameBackground(openFrame, function gotBackground() { + // Start the transition when this async/sync callback is called. + openFrame.classList.add('active'); + if (inlineActivityFrames.length == 1) + activityCallerOrigin = displayedApp; + if ('wrapper' in runningApps[displayedApp].frame.dataset) { + wrapperFooter.classList.remove('visible'); + wrapperHeader.classList.remove('visible'); + } + }); + } + + function removeFrame(origin) { + var app = runningApps[origin]; + var frame = app.frame; + + if (frame) { + windows.removeChild(frame); + clearFrameBackground(frame); + } + + if (openFrame == frame) { + setOpenFrame(null); + setTimeout(openCallback); + openCallback = null; + } + if (closeFrame == frame) { + setCloseFrame(null); + setTimeout(closeCallback); + closeCallback = null; + } + + delete runningApps[origin]; + numRunningApps--; + } + + function removeInlineFrame(frame) { + // If frame is transitioning we should remove the reference + if (openFrame == frame) + setOpenFrame(null); + + // If frame is never set visible, we can remove the frame directly + // without closing transition + if (!frame.classList.contains('active')) { + windows.removeChild(frame); + return; + } + // Take keyboard focus away from the closing window + frame.firstChild.blur(); + // Remove the active class and start the closing transition + frame.classList.remove('active'); + } + + // If all is not specified, + // remove the top most frame + function stopInlineActivity(all) { + if (!inlineActivityFrames.length) + return; + + if (!all) { + var frame = inlineActivityFrames.pop(); + removeInlineFrame(frame); + } else { + // stop all activity frames + // Remore the inlineActivityFrame reference + for (var frame of inlineActivityFrames) { + removeInlineFrame(frame); + } + inlineActivityFrames = []; + } + + if (!inlineActivityFrames.length) { + // Give back focus to the displayed app + var app = runningApps[displayedApp]; + if (app && app.iframe) { + app.iframe.focus(); + if ('wrapper' in app.frame.dataset) { + wrapperFooter.classList.add('visible'); + } + } + screenElement.classList.remove('inline-activity'); + } + } + + // Watch activity completion here instead of activity.js + // Because we know when and who to re-launch when activity ends. + window.addEventListener('mozChromeEvent', function(e) { + if (e.detail.type == 'activity-done') { + // Remove the top most frame every time we get an 'activity-done' event. + stopInlineActivity(); + if (!inlineActivityFrames.length) { + setDisplayedApp(activityCallerOrigin); + activityCallerOrigin = ''; + } + } + }); + + // There are two types of mozChromeEvent we need to handle + // in order to launch the app for Gecko + window.addEventListener('mozChromeEvent', function(e) { + var startTime = Date.now(); + + var manifestURL = e.detail.manifestURL; + if (!manifestURL) + return; + + var app = Applications.getByManifestURL(manifestURL); + if (!app) + return; + + var manifest = app.manifest; + var name = new ManifestHelper(manifest).name; + var origin = app.origin; + + // Check if it's a virtual app from a entry point. + // If so, change the app name and origin to the + // entry point. + var entryPoints = manifest.entry_points; + if (entryPoints && manifest.type == 'certified') { + var givenPath = e.detail.url.substr(origin.length); + + // Workaround here until the bug (to be filed) is fixed + // Basicly, gecko is sending the URL without launch_path sometimes + for (var ep in entryPoints) { + var currentEp = entryPoints[ep]; + var path = givenPath; + if (path.indexOf('?') != -1) { + path = path.substr(0, path.indexOf('?')); + } + + //Remove the origin and / to find if if the url is the entry point + if (path.indexOf('/' + ep) == 0 && + (currentEp.launch_path == path)) { + origin = origin + currentEp.launch_path; + name = new ManifestHelper(currentEp).name; + } + } + } + switch (e.detail.type) { + // mozApps API is asking us to launch the app + // We will launch it in foreground + case 'webapps-launch': + if (origin == homescreen) { + // No need to append a frame if is homescreen + setDisplayedApp(); + } else { + if (!isRunning(origin)) { + appendFrame(null, origin, e.detail.url, + name, app.manifest, app.manifestURL); + } + runningApps[origin].iframe.dataset.start = startTime; + setDisplayedApp(origin, null, 'window'); + } + break; + // System Message Handler API is asking us to open the specific URL + // that handles the pending system message. + // We will launch it in background if it's not handling an activity. + case 'open-app': + // If the system message goes to System app, + // we should not be launching that in a frame. + if (e.detail.url === window.location.href) + return; + + if (e.detail.isActivity && e.detail.target.disposition && + e.detail.target.disposition == 'inline') { + // Inline activities behaves more like a dialog, + // let's deal them here. + + startInlineActivity(origin, e.detail.url, + name, manifest, app.manifestURL); + + return; + } + + if (isRunning(origin)) { + // If the app is in foreground, it's too risky to change it's + // URL. We'll ignore this request. + if (displayedApp !== origin) { + var iframe = getAppFrame(origin).firstChild; + + // If the app is opened and it is loaded to the correct page, + // then there is nothing to do. + if (iframe.src !== e.detail.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 = e.detail.url; + } + } + } else if (origin !== homescreen) { + // XXX: We could ended opening URls not for the app frame + // in the app frame. But we don't care. + appendFrame(null, origin, e.detail.url, + name, manifest, app.manifestURL); + + // set the size of the iframe + // so Cards View will get a correct screenshot of the frame + if (!e.detail.isActivity) + setAppSize(origin, false); + } else { + ensureHomescreen(); + } + + // We will only bring web activity handling apps to the foreground + if (!e.detail.isActivity) + return; + + // XXX: the correct way would be for UtilityTray to close itself + // when there is a appwillopen/appopen event. + UtilityTray.hide(); + + setDisplayedApp(origin); + + break; + } + }); + + // If the application tried to close themselves by calling window.close() + // we will handle that here. + // XXX: this event is fired twice: + // https://bugzilla.mozilla.org/show_bug.cgi?id=814583 + window.addEventListener('mozbrowserclose', function(e) { + if (!'frameType' in e.target.dataset) + return; + + switch (e.target.dataset.frameType) { + case 'window': + kill(e.target.dataset.frameOrigin); + break; + + case 'inline-activity': + stopInlineActivity(true); + break; + } + }); + + // Deal with locationchange + window.addEventListener('mozbrowserlocationchange', function(e) { + if (!'frameType' in e.target.dataset) + return; + + e.target.dataset.url = e.detail; + }); + + // Deal with application uninstall event + // if the application is being uninstalled, we ensure it stop running here. + window.addEventListener('applicationuninstall', function(e) { + kill(e.detail.application.origin); + + deleteAppScreenshotFromDatabase(e.detail.application.origin); + }); + + // When an UI layer is overlapping the current app, + // WindowManager should set the visibility of app iframe to false + // And reset to true when the layer is gone. + // We may need to handle windowclosing, windowopened in the future. + var attentionScreenTimer = null; + + var overlayEvents = [ + 'lock', + 'will-unlock', + 'attentionscreenshow', + 'attentionscreenhide', + 'status-active', + 'status-inactive' + ]; + + function overlayEventHandler(evt) { + if (attentionScreenTimer) + clearTimeout(attentionScreenTimer); + switch (evt.type) { + case 'status-active': + case 'attentionscreenhide': + case 'will-unlock': + if (LockScreen.locked) + return; + if (inlineActivityFrames.length) { + setVisibilityForInlineActivity(true); + } else { + setVisibilityForCurrentApp(true); + } + break; + case 'lock': + setVisibilityForCurrentApp(false); + break; + + /* + * Because in-transition is needed in attention screen, + * We set a timer here to deal with visibility change + */ + case 'status-inactive': + if (!AttentionScreen.isVisible()) + return; + case 'attentionscreenshow': + if (evt.detail && evt.detail.origin && + evt.detail.origin != displayedApp) { + attentionScreenTimer = setTimeout(function setVisibility() { + if (inlineActivityFrames.length) { + setVisibilityForInlineActivity(false); + } else { + setVisibilityForCurrentApp(false); + } + }, 3000); + + // Immediatly blur the frame in order to ensure hiding the keyboard + var app = runningApps[displayedApp]; + if (app) + app.iframe.blur(); + } + break; + } + } + + overlayEvents.forEach(function overlayEventIterator(event) { + window.addEventListener(event, overlayEventHandler); + }); + + function setVisibilityForInlineActivity(visible) { + if (!inlineActivityFrames.length) + return; + + var topFrame = inlineActivityFrames[inlineActivityFrames.length - 1].firstChild; + if ('setVisible' in topFrame) { + topFrame.setVisible(visible); + } + + // Restore/give away focus on visiblity change + // so that the app can take back its focus + if (visible) { + topFrame.focus(); + } else { + topFrame.blur(); + } + } + + function setVisibilityForCurrentApp(visible) { + var app = runningApps[displayedApp]; + if (!app) + return; + if ('setVisible' in app.iframe) + app.iframe.setVisible(visible); + + // Restore/give away focus on visiblity change + // so that the app can take back its focus + if (visible) + app.iframe.focus(); + else + app.iframe.blur(); + } + + function handleAppCrash(origin, manifestURL) { + if (origin && manifestURL) { + // When inline activity frame crashes, + // query the localized name from manifest + var app = Applications.getByManifestURL(manifestURL); + CrashReporter.setAppName(getAppName(origin, app.manifest)); + } else { + var app = runningApps[displayedApp]; + CrashReporter.setAppName(app.name); + } + } + + function getAppName(origin, manifest) { + if (!manifest) + return ''; + + if (manifest.entry_points && manifest.type == 'certified') { + var entryPoint = manifest.entry_points[origin.split('/')[3]]; + return new ManifestHelper(entryPoint).name; + } + return new ManifestHelper(manifest).name; + } + + // Deal with crashed apps + window.addEventListener('mozbrowsererror', function(e) { + if (!'frameType' in e.target.dataset) + return; + + var origin = e.target.dataset.frameOrigin; + var manifestURL = e.target.getAttribute('mozapp'); + + if (e.target.dataset.frameType == 'inline-activity') { + stopInlineActivity(true); + handleAppCrash(origin, manifestURL); + return; + } + + if (e.target.dataset.frameType !== 'window') + return; + + /* + detail.type = error (Server Not Found case) + is handled in Modal Dialog + */ + if (e.detail.type !== 'fatal') + return; + + // If the crashing app is currently displayed, we will present + // the user with a banner notification. + if (displayedApp == origin) + handleAppCrash(); + + // If the crashing app is the home screen app and it is the displaying app + // we will need to relaunch it right away. + // Alternatively, if home screen is not the displaying app, + // we will not relaunch it until the foreground app is closed. + // (to be dealt in setDisplayedApp(), not here) + if (displayedApp == homescreen) { + kill(origin, function relaunchHomescreen() { + setDisplayedApp(homescreen); + }); + return; + } + + // Actually remove the frame, and trigger the closing transition + // if the app is currently displaying + kill(origin); + }); + + + function hasPermission(app, permission) { + var mozPerms = navigator.mozPermissionSettings; + if (!mozPerms) + return false; + + var value = mozPerms.get(permission, app.manifestURL, app.origin, false); + + return (value === 'allow'); + } + + // Use a setting in order to be "called" by settings app + navigator.mozSettings.addObserver( + 'clear.remote-windows.data', + function clearRemoteWindowsData(setting) { + var shouldClear = setting.settingValue; + if (!shouldClear) + return; + + // Delete all storage and cookies from our content processes + var request = navigator.mozApps.getSelf(); + request.onsuccess = function() { + request.result.clearBrowserData(); + }; + + // Reset the setting value to false + var lock = navigator.mozSettings.createLock(); + lock.set({'clear.remote-windows.data': false}); + }); + + // Watch for window.open usages in order to open wrapper frames + window.addEventListener('mozbrowseropenwindow', function handleWrapper(evt) { + var detail = evt.detail; + var features; + try { + features = JSON.parse(detail.features); + } catch (e) { + features = {}; + } + + // Handles only call to window.open with `{remote: true}` feature. + if (!features.remote) + return; + + // XXX bug 819882: for now, only allows homescreen to open oop windows + var callerIframe = evt.target; + var callerFrame = callerIframe.parentNode; + var manifestURL = callerIframe.getAttribute('mozapp'); + var callerApp = Applications.getByManifestURL(manifestURL); + if (!callerApp || !callerFrame.classList.contains('homescreen')) + return; + var callerOrigin = callerApp.origin; + + // So, we are going to open a remote window. + // Now, avoid PopupManager listener to be fired. + evt.stopImmediatePropagation(); + + var name = detail.name; + var url = detail.url; + + // Use fake origin for named windows in order to be able to reuse them, + // otherwise always open a new window for '_blank'. + var origin = null; + var app = null; + if (name == '_blank') { + origin = url; + + // Just bring on top if a wrapper window is already running with this url + if (origin in runningApps && + runningApps[origin].windowName == '_blank') { + setDisplayedApp(origin); + return; + } + } else { + origin = 'window:' + name + ',source:' + callerOrigin; + + var runningApp = runningApps[origin]; + if (runningApp && runningApp.windowName === name) { + if (runningApp.iframe.src === url) { + // If the url is already loaded, just display the app + setDisplayedApp(origin); + return; + } else { + // Wrapper context shouldn't be shared between two apps -> killing + kill(origin); + } + } + } + + var title = '', icon = '', remote = false, useAsyncPanZoom = false; + var originName, originURL, searchName, searchURL; + + try { + var features = JSON.parse(detail.features); + var regExp = new RegExp(' ', 'g'); + + title = features.name.replace(regExp, ' ') || url; + icon = features.icon || ''; + + if (features.origin) { + originName = features.origin.name.replace(regExp, ' '); + originURL = decodeURIComponent(features.origin.url); + } + + if (features.search) { + searchName = features.search.name.replace(regExp, ' '); + searchURL = decodeURIComponent(features.search.url); + } + + if (features.useAsyncPanZoom) + useAsyncPanZoom = true; + } catch (ex) { } + + // If we don't reuse an existing app, open a brand new one + var iframe; + if (!app) { + // Bug 807438: Move new window document OOP + // Ignore `event.detail.frameElement` for now in order + // to create a remote system app frame. + // So that new window documents are going to share + // system app content processes data jar. + iframe = document.createElement('iframe'); + iframe.setAttribute('mozbrowser', 'true'); + iframe.setAttribute('remote', 'true'); + + iframe.addEventListener('mozbrowserloadstart', function start() { + iframe.dataset.loading = true; + wrapperHeader.classList.add('visible'); + }); + + iframe.addEventListener('mozbrowserloadend', function end() { + delete iframe.dataset.loading; + wrapperHeader.classList.remove('visible'); + }); + + // `mozasyncpanzoom` only works when added before attaching the iframe + // node to the document. + if (useAsyncPanZoom) { + iframe.dataset.useAsyncPanZoom = true; + iframe.setAttribute('mozasyncpanzoom', 'true'); + } + + var app = appendFrame(iframe, origin, url, title, { + 'name': title + }, null); + + // Set the window name in order to reuse this app if we try to open + // a new window with same name + app.windowName = name; + } else { + iframe = app.iframe; + + // Update app name for the card view + app.manifest.name = title; + } + + iframe.dataset.name = title; + iframe.dataset.icon = icon; + + if (originName) + iframe.dataset.originName = originName; + if (originURL) + iframe.dataset.originURL = originURL; + + if (searchName) + iframe.dataset.searchName = searchName; + if (searchURL) + iframe.dataset.searchURL = searchURL; + + // First load blank page in order to hide previous website + iframe.src = url; + + setDisplayedApp(origin); + }, true); // Use capture in order to catch the event before PopupManager does + + + // Stop running the app with the specified origin + function kill(origin, callback) { + if (!isRunning(origin)) + return; + + // As we can't immediatly remove runningApps entry, + // we flag it as being killed in order to avoid trying to remove it twice. + // (Check required because of bug 814583) + if (runningApps[origin].killed) + return; + runningApps[origin].killed = true; + + // If the app is the currently displayed app, switch to the homescreen + if (origin === displayedApp) { + // when the homescreen is displayed and being + // killed we need to forcibly restart it... + if (origin === homescreen) { + removeFrame(origin); + + // XXX workaround bug 810431. + // we need this here and not in other situations + // as it is expected that homescreen frame is available. + setTimeout(function() { + setDisplayedApp(); + + if (callback) { + callback(); + } + }); + } else { + setDisplayedApp(homescreen, function() { + removeFrame(origin); + if (callback) + setTimeout(callback); + }); + } + + } else { + removeFrame(origin); + } + + // Send a synthentic 'appterminated' event. + // Let other system app module know an app is + // being killed, removed or crashed. + var evt = document.createEvent('CustomEvent'); + evt.initCustomEvent('appterminated', true, false, { origin: origin }); + window.dispatchEvent(evt); + } + + // Reload the frame of the running app + function reload(origin) { + if (!isRunning(origin)) + return; + + var app = runningApps[origin]; + app.reload(); + } + + // When a resize event occurs, resize the running app, if there is one + // When the status bar is active it doubles in height so we need a resize + var appResizeEvents = ['resize', 'status-active', 'status-inactive', + 'keyboardchange', 'keyboardhide', + 'attentionscreenhide']; + appResizeEvents.forEach(function eventIterator(event) { + window.addEventListener(event, function on(evt) { + if (event == 'keyboardchange') { + // Cancel fullscreen if keyboard pops + if (document.mozFullScreen) + document.mozCancelFullScreen(); + + setAppHeight(evt.detail.height); + } else if (displayedApp) { + setAppSize(displayedApp); + } + }); + }); + + window.addEventListener('home', function(e) { + // If the lockscreen is active, it will stop propagation on this event + // and we'll never see it here. Similarly, other overlays may use this + // event to hide themselves and may prevent the event from getting here. + // Note that for this to work, the lockscreen and other overlays must + // be included in index.html before this one, so they can register their + // event handlers before we do. + + // If we are currently transitioning, the user would like to cancel + // it instead of toggling homescreen panels. + var inTransition = !!(openFrame || closeFrame); + + if (document.mozFullScreen) { + document.mozCancelFullScreen(); + } + + if (displayedApp !== homescreen || inTransition) { + if (displayedApp != ftuURL) { + setDisplayedApp(homescreen); + } else { + e.preventDefault(); + } + } else { + stopInlineActivity(true); + ensureHomescreen(true); + } + }); + + // Cancel dragstart event to workaround + // https://bugzilla.mozilla.org/show_bug.cgi?id=783076 + // which stops OOP home screen pannable with left mouse button on + // B2G/Desktop. + windows.addEventListener('dragstart', function(evt) { + evt.preventDefault(); + }, true); + + // With all important event handlers in place, we can now notify + // Gecko that we're ready for certain system services to send us + // messages (e.g. the radio). + // Note that shell.js starts listen for the mozContentEvent event at + // mozbrowserloadstart, which sometimes does not happen till window.onload. + window.addEventListener('load', function wm_loaded() { + window.removeEventListener('load', wm_loaded); + + var evt = new CustomEvent('mozContentEvent', + { bubbles: true, cancelable: false, + detail: { type: 'system-message-listener-ready' } }); + window.dispatchEvent(evt); + }); + + // This is code copied from + // http://dl.dropbox.com/u/8727858/physical-events/index.html + // It appears to workaround the Nexus S bug where we're not + // getting orientation data. See: + // https://bugzilla.mozilla.org/show_bug.cgi?id=753245 + // It seems it needs to be in both window_manager.js and bootstrap.js. + function dumbListener2(event) {} + window.addEventListener('devicemotion', dumbListener2); + + window.setTimeout(function() { + window.removeEventListener('devicemotion', dumbListener2); + }, 2000); + + // Return the object that holds the public API + return { + isFtuRunning: function() { + return isRunningFirstRunApp; + }, + launch: launch, + kill: kill, + reload: reload, + getDisplayedApp: getDisplayedApp, + setOrientationForApp: setOrientationForApp, + getAppFrame: getAppFrame, + getRunningApps: function() { + return runningApps; + }, + setDisplayedApp: setDisplayedApp, + getCurrentDisplayedApp: function() { + return runningApps[displayedApp]; + }, + hideCurrentApp: hideCurrentApp, + restoreCurrentApp: restoreCurrentApp, + retrieveHomescreen: retrieveHomescreen, + retrieveFTU: retrieveFTU + }; +}()); + |