Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/apps/system/js/window_manager.js
diff options
context:
space:
mode:
Diffstat (limited to 'apps/system/js/window_manager.js')
-rw-r--r--apps/system/js/window_manager.js2011
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('&nbsp;', '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
+ };
+}());
+