Merge m-c to fx-team
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Fri, 02 Aug 2013 15:47:01 +0200
changeset 154373 bcbc5a0ed405aa33a5c0eaff4e4f71c808ebeeea
parent 154280 d3a0ac52d5d44452afc354567dc4ed527dd9d393 (current diff)
parent 154372 ff1477ebf69e4a46bc02372e447cf20d5b31257f (diff)
child 154374 9ce5efc16a8ce8d8aab4c351ca9da7b31379d93c
push id382
push userakeybl@mozilla.com
push dateMon, 21 Oct 2013 21:47:13 +0000
treeherdermozilla-release@5f1868ee45cb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone25.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-c to fx-team
browser/metro/base/content/bindings/appbar.xml
browser/metro/base/content/browser.xul
browser/metro/theme/browser.css
mobile/android/base/locales/en-US/android_strings.dtd
mobile/android/base/strings.xml.in
mobile/android/base/tests/testSettingsMenuItems.java.in
mobile/android/chrome/content/browser.js
toolkit/webapps/WebappOSUtils.jsm
--- a/browser/base/content/browser-sets.inc
+++ b/browser/base/content/browser-sets.inc
@@ -171,17 +171,18 @@
     <broadcaster id="workOfflineMenuitemState"/>
     <broadcaster id="socialSidebarBroadcaster" hidden="true"/>
     <broadcaster id="socialActiveBroadcaster" hidden="true"/>
 
     <!-- DevTools broadcasters -->
     <broadcaster id="devtoolsMenuBroadcaster_DevToolbox"
                  label="&devToolboxMenuItem.label;"
                  type="checkbox" autocheck="false"
-                 command="Tools:DevToolbox"/>
+                 command="Tools:DevToolbox"
+                 key="key_developerTools"/>
     <broadcaster id="devtoolsMenuBroadcaster_DevToolbar"
                  label="&devToolbarMenu.label;"
                  type="checkbox" autocheck="false"
                  command="Tools:DevToolbar"
                  key="key_devToolbar"/>
     <broadcaster id="devtoolsMenuBroadcaster_ChromeDebugger"
                  label="&chromeDebuggerMenu.label;"
                  command="Tools:ChromeDebugger"/>
@@ -256,19 +257,19 @@
 #endif
 #ifdef XP_GNOME
     <key id="key_search2" key="&searchFocusUnix.commandkey;" command="Tools:Search" modifiers="accel"/>
     <key id="key_openDownloads" key="&downloadsUnix.commandkey;" command="Tools:Downloads" modifiers="accel,shift"/>
 #else
     <key id="key_openDownloads" key="&downloads.commandkey;" command="Tools:Downloads" modifiers="accel"/>
 #endif
     <key id="key_openAddons" key="&addons.commandkey;" command="Tools:Addons" modifiers="accel,shift"/>
+    <key id="key_developerTools" keycode="&devToolsCmd.keycode;" keytext="&devToolsCmd.keytext;" command="Tools:DevToolbox"/>
     <key id="key_browserConsole" key="&browserConsoleCmd.commandkey;" command="Tools:BrowserConsole" modifiers="accel,shift"/>
-    <key id="key_devToolbar" keycode="&devToolbar.keycode;" modifiers="shift"
-         keytext="&devToolbar.keytext;" command="Tools:DevToolbarFocus"/>
+    <key id="key_devToolbar" keycode="&devToolbar.keycode;" modifiers="shift" keytext="&devToolbar.keytext;" command="Tools:DevToolbarFocus"/>
     <key id="key_responsiveUI" key="&responsiveDesignTool.commandkey;" command="Tools:ResponsiveUI"
 #ifdef XP_MACOSX
         modifiers="accel,alt"
 #else
         modifiers="accel,shift"
 #endif
     />
     <key id="key_scratchpad" keycode="&scratchpad.keycode;" modifiers="shift"
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -1272,18 +1272,16 @@ var gBrowserInit = {
 
     SessionStore.promiseInitialized.then(() => {
       // Enable the Restore Last Session command if needed
       if (SessionStore.canRestoreLastSession &&
           !PrivateBrowsingUtils.isWindowPrivate(window))
         goSetCommandEnabled("Browser:RestoreLastSession", true);
 
       TabView.init();
-
-      setTimeout(function () { BrowserChromeTest.markAsReady(); }, 0);
     });
 
     Services.obs.notifyObservers(window, "browser-delayed-startup-finished", "");
     TelemetryTimestamps.add("delayedStartupFinished");
   },
 
   // Returns the URI(s) to load at startup.
   _getUriToLoad: function () {
@@ -7313,25 +7311,8 @@ var MousePosTracker = {
 
 function focusNextFrame(event) {
   let fm = Services.focus;
   let dir = event.shiftKey ? fm.MOVEFOCUS_BACKWARDDOC : fm.MOVEFOCUS_FORWARDDOC;
   let element = fm.moveFocus(window, null, dir, fm.FLAG_BYKEY);
   if (element.ownerDocument == document)
     focusAndSelectUrlBar();
 }
-let BrowserChromeTest = {
-  _cb: null,
-  _ready: false,
-  markAsReady: function () {
-    this._ready = true;
-    if (this._cb) {
-      this._cb();
-      this._cb = null;
-    }
-  },
-  runWhenReady: function (cb) {
-    if (this._ready)
-      cb();
-    else
-      this._cb = cb;
-  }
-};
--- a/browser/components/privatebrowsing/test/browser/head.js
+++ b/browser/components/privatebrowsing/test/browser/head.js
@@ -1,38 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 function whenNewWindowLoaded(aOptions, aCallback) {
   let win = OpenBrowserWindow(aOptions);
   let gotLoad = false;
-  let gotActivate = (Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager).activeWindow == win);
+  let gotActivate = Services.focus.activeWindow == win;
 
   function maybeRunCallback() {
     if (gotLoad && gotActivate) {
-      win.BrowserChromeTest.runWhenReady(function() {
-        executeSoon(function() { aCallback(win); });
-      });
+      executeSoon(function() { aCallback(win); });
     }
   }
 
   if (!gotActivate) {
     win.addEventListener("activate", function onActivate() {
       info("Got activate.");
       win.removeEventListener("activate", onActivate, false);
       gotActivate = true;
       maybeRunCallback();
     }, false);
   } else {
     info("Was activated.");
   }
 
-  win.addEventListener("load", function onLoad() {
-    info("Got load");
-    win.removeEventListener("load", onLoad, false);
-    gotLoad = true;
-    maybeRunCallback();
-  }, false);
+  Services.obs.addObserver(function observer(aSubject, aTopic) {
+    if (win == aSubject) {
+      info("Delayed startup finished");
+      Services.obs.removeObserver(observer, aTopic);
+      gotLoad = true;
+      maybeRunCallback();
+    }
+  }, "browser-delayed-startup-finished", false);
+
   return win;
 }
 
 function openWindow(aParent, aOptions, a3) {
   let { Promise: { defer } } = Components.utils.import("resource://gre/modules/Promise.jsm", {});
   let { promise, resolve } = defer();
 
   let win = aParent.OpenBrowserWindow(aOptions);
--- a/browser/components/search/test/head.js
+++ b/browser/components/search/test/head.js
@@ -1,41 +1,42 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 function whenNewWindowLoaded(aOptions, aCallback) {
   let win = OpenBrowserWindow(aOptions);
   let gotLoad = false;
-  let gotActivate = (Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager).activeWindow == win);
+  let gotActivate = Services.focus.activeWindow == win;
 
   function maybeRunCallback() {
     if (gotLoad && gotActivate) {
-      win.BrowserChromeTest.runWhenReady(function() {
-        executeSoon(function() { aCallback(win); });
-      });
+      executeSoon(function() { aCallback(win); });
     }
   }
 
   if (!gotActivate) {
     win.addEventListener("activate", function onActivate() {
       info("Got activate.");
       win.removeEventListener("activate", onActivate, false);
       gotActivate = true;
       maybeRunCallback();
     }, false);
   } else {
     info("Was activated.");
   }
 
-  win.addEventListener("load", function onLoad() {
-    info("Got load");
-    win.removeEventListener("load", onLoad, false);
-    gotLoad = true;
-    maybeRunCallback();
-  }, false);
+  Services.obs.addObserver(function observer(aSubject, aTopic) {
+    if (win == aSubject) {
+      info("Delayed startup finished");
+      Services.obs.removeObserver(observer, aTopic);
+      gotLoad = true;
+      maybeRunCallback();
+    }
+  }, "browser-delayed-startup-finished", false);
+
   return win;
 }
 
 /**
  * Recursively compare two objects and check that every property of expectedObj has the same value
  * on actualObj.
  */
 function isSubObjectOf(expectedObj, actualObj, name) {
--- a/browser/devtools/debugger/test/browser_dbg_multiple-windows.js
+++ b/browser/devtools/debugger/test/browser_dbg_multiple-windows.js
@@ -39,58 +39,61 @@ function test_first_tab()
   });
 }
 
 function test_open_window()
 {
   gSecondWindow = window.open(TAB2_URL, "secondWindow");
   ok(!!gSecondWindow, "Second window created.");
   gSecondWindow.focus();
-  let top = windowMediator.getMostRecentWindow("navigator:browser");
-  var main2 = gSecondWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                   .getInterface(Components.interfaces.nsIWebNavigation)
-                   .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
-                   .rootTreeItem
-                   .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                   .getInterface(Components.interfaces.nsIDOMWindow);
-  is(top, main2, "The second window is on top.");
+
+  let topWin = windowMediator.getMostRecentWindow("navigator:browser");
+  var chromeWin = gSecondWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIWebNavigation)
+                               .QueryInterface(Ci.nsIDocShellTreeItem)
+                               .rootTreeItem
+                               .QueryInterface(Ci.nsIInterfaceRequestor)
+                               .getInterface(Ci.nsIDOMWindow);
+  is(topWin, chromeWin, "The second window is on top.");
+
   let gotLoad = false;
-  let gotActivate = (Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager).activeWindow == main2);
+  let gotActivate = Services.focus.activeWindow == chromeWin;
+
   function maybeWindowLoadedAndActive() {
     if (gotLoad && gotActivate) {
-      top.BrowserChromeTest.runWhenReady(function() {
-        executeSoon(function() {
-          gClient.listTabs(function(aResponse) {
-            is(aResponse.selected, 2, "Tab2 is selected.");
-            test_focus_first();
-          });
+      executeSoon(function() {
+        gClient.listTabs(function(aResponse) {
+          is(aResponse.selected, 2, "Tab2 is selected.");
+          test_focus_first();
         });
-      })
+      });
     }
   }
 
+  // Wait for chrome window activation, if necessary
   if (!gotActivate) {
-    main2.addEventListener("activate", function() {
-        main2.removeEventListener("activate", arguments.callee, true);
-        gotActivate = true;
-        maybeWindowLoadedAndActive();
-      },
-      true
-    );
+    chromeWin.addEventListener("activate", function onactivate() {
+      info("activate event");
+
+      chromeWin.removeEventListener("activate", onactivate, true);
+      gotActivate = true;
+      maybeWindowLoadedAndActive();
+    }, true);
   }
-  main2.document.addEventListener("load", function(e) {
-      if (e.target.documentURI != TAB2_URL) {
-        return;
-      }
-      main2.document.removeEventListener("load", arguments.callee, true);
-      gotLoad = true;
-      maybeWindowLoadedAndActive();
-    },
-    true
-  );
+
+  // Wait for the requested content document to load
+  chromeWin.document.addEventListener("load", function onload(e) {
+    if (e.target.documentURI != TAB2_URL) {
+      return;
+    }
+    info("content loaded");
+    chromeWin.document.removeEventListener("load", onload, true);
+    gotLoad = true;
+    maybeWindowLoadedAndActive();
+  }, true);
 }
 
 function test_focus_first()
 {
   window.content.addEventListener("focus", function onFocus() {
     window.content.removeEventListener("focus", onFocus, false);
     let top = windowMediator.getMostRecentWindow("navigator:browser");
     var main1 = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
--- a/browser/devtools/framework/gDevTools.jsm
+++ b/browser/devtools/framework/gDevTools.jsm
@@ -8,22 +8,24 @@ this.EXPORTED_SYMBOLS = [ "gDevTools", "
 
 const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource:///modules/devtools/shared/event-emitter.js");
 let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
 Cu.import("resource://gre/modules/devtools/Loader.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
 
 var ProfilerController = devtools.require("devtools/profiler/controller");
 
 const FORBIDDEN_IDS = new Set(["toolbox", ""]);
 const MAX_ORDINAL = 99;
 
+
 /**
  * DevTools is a class that represents a set of developer tools, it holds a
  * set of tools and keeps track of open toolboxes in the browser.
  */
 this.DevTools = function DevTools() {
   this._tools = new Map();     // Map<toolId, tool>
   this._toolboxes = new Map(); // Map<target, toolbox>
 
@@ -369,16 +371,23 @@ let gDevToolsBrowser = {
    *
    * @param {XULDocument} doc
    *        The document to which menuitems and handlers are to be added
    */
   registerBrowserWindow: function DT_registerBrowserWindow(win) {
     gDevToolsBrowser._trackedBrowserWindows.add(win);
     gDevToolsBrowser._addAllToolsToMenu(win.document);
 
+    this._getFirebug().then((firebug) => {
+      if (firebug && firebug.hasOwnProperty("isActive") && firebug.isActive) {
+        let broadcaster = win.document.getElementById("devtoolsMenuBroadcaster_DevToolbox");
+        broadcaster.removeAttribute("key");
+      }
+    })
+
     let tabContainer = win.document.getElementById("tabbrowser-tabs")
     tabContainer.addEventListener("TabSelect",
                                   gDevToolsBrowser._updateMenuCheckbox, false);
   },
 
   /**
    * Add a <key> to <keyset id="devtoolsKeyset">.
    * Appending a <key> element is not always enough. The <keyset> needs
@@ -387,25 +396,40 @@ let gDevToolsBrowser = {
    *
    * @param {XULDocument} doc
    *        The document to which keys are to be added
    * @param {XULElement} or {DocumentFragment} keys
    *        Keys to add
    */
   attachKeybindingsToBrowser: function DT_attachKeybindingsToBrowser(doc, keys) {
     let devtoolsKeyset = doc.getElementById("devtoolsKeyset");
+
     if (!devtoolsKeyset) {
       devtoolsKeyset = doc.createElement("keyset");
       devtoolsKeyset.setAttribute("id", "devtoolsKeyset");
     }
     devtoolsKeyset.appendChild(keys);
     let mainKeyset = doc.getElementById("mainKeyset");
     mainKeyset.parentNode.insertBefore(devtoolsKeyset, mainKeyset);
   },
 
+
+  /**
+   * Detect the presence of a Firebug.
+   *
+   * @return promise
+   */
+  _getFirebug: function DT_getFirebug() {
+    let deferred = promise.defer();
+    AddonManager.getAddonByID("firebug@software.joehewitt.com", (addon) => {
+      deferred.resolve(addon);
+    });
+    return deferred.promise;
+  },
+
   /**
    * Add the menuitem for a tool to all open browser windows.
    *
    * @param {object} toolDefinition
    *        properties of the tool to add
    */
   _addToolToWindows: function DT_addToolToWindows(toolDefinition) {
     // No menu item or global shortcut is required for options panel.
@@ -720,16 +744,17 @@ let gDevToolsBrowser = {
   /**
    * All browser windows have been closed, tidy up remaining objects.
    */
   destroy: function() {
     gDevTools.off("toolbox-ready", gDevToolsBrowser._connectToProfiler);
     Services.obs.removeObserver(gDevToolsBrowser.destroy, "quit-application");
   },
 }
+
 this.gDevToolsBrowser = gDevToolsBrowser;
 
 gDevTools.on("tool-registered", function(ev, toolId) {
   let toolDefinition = gDevTools._tools.get(toolId);
   gDevToolsBrowser._addToolToWindows(toolDefinition);
 });
 
 gDevTools.on("tool-unregistered", function(ev, toolId) {
--- a/browser/devtools/markupview/markup-view.js
+++ b/browser/devtools/markupview/markup-view.js
@@ -15,16 +15,20 @@ const DEFAULT_MAX_CHILDREN = 100;
 let {UndoStack} = require("devtools/shared/undo");
 let EventEmitter = require("devtools/shared/event-emitter");
 let {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
 let promise = require("sdk/core/promise");
 
 Cu.import("resource:///modules/devtools/LayoutHelpers.jsm");
 Cu.import("resource://gre/modules/devtools/Templater.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyGetter(this, "AutocompletePopup", function() {
+  return Cu.import("resource:///modules/devtools/AutocompletePopup.jsm", {}).AutocompletePopup;
+});
 
 /**
  * Vocabulary for the purposes of this file:
  *
  * MarkupContainer - the structure that holds an editor and its
  *  immediate children in the markup panel.
  * Node - A content node.
  * object.elt - A UI element in the markup panel.
@@ -48,16 +52,24 @@ function MarkupView(aInspector, aFrame, 
   this._elt = this.doc.querySelector("#root");
 
   try {
     this.maxChildren = Services.prefs.getIntPref("devtools.markup.pagesize");
   } catch(ex) {
     this.maxChildren = DEFAULT_MAX_CHILDREN;
   }
 
+  // Creating the popup to be used to show CSS suggestions.
+  let options = {
+    fixedWidth: true,
+    autoSelect: true,
+    theme: "auto"
+  };
+  this.popup = new AutocompletePopup(this.doc.defaultView.parent.document, options);
+
   this.undo = new UndoStack();
   this.undo.installController(aControllerWindow);
 
   this._containers = new WeakMap();
 
   this._boundMutationObserver = this._mutationObserver.bind(this);
   this.walker.on("mutations", this._boundMutationObserver)
 
@@ -671,16 +683,19 @@ MarkupView.prototype = {
   /**
    * Tear down the markup panel.
    */
   destroy: function MT_destroy()
   {
     this.undo.destroy();
     delete this.undo;
 
+    this.popup.destroy();
+    delete this.popup;
+
     this._frame.removeEventListener("focus", this._boundFocus, false);
     delete this._boundFocus;
 
     if (this._boundUpdatePreview) {
       this._frame.contentWindow.removeEventListener("scroll", this._boundUpdatePreview, true);
       delete this._boundUpdatePreview;
     }
 
@@ -1125,16 +1140,18 @@ function ElementEditor(aContainer, aNode
     });
   }
 
   // Make the new attribute space editable.
   editableField({
     element: this.newAttr,
     trigger: "dblclick",
     stopOnReturn: true,
+    contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+    popup: this.markup.popup,
     done: (aVal, aCommit) => {
       if (!aCommit) {
         return;
       }
 
       try {
         let doMods = this._startModifyingAttributes();
         let undoMods = this._startModifyingAttributes();
@@ -1217,16 +1234,18 @@ ElementEditor.prototype = {
       this.attrList.insertBefore(attr, before);
 
       // Make the attribute editable.
       editableField({
         element: inner,
         trigger: "dblclick",
         stopOnReturn: true,
         selectAll: false,
+        contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
+        popup: this.markup.popup,
         start: (aEditor, aEvent) => {
           // If the editing was started inside the name or value areas,
           // select accordingly.
           if (aEvent && aEvent.target === name) {
             aEditor.input.setSelectionRange(0, name.textContent.length);
           } else if (aEvent && aEvent.target === val) {
             let length = val.textContent.length;
             let editorLength = aEditor.input.value.length;
--- a/browser/devtools/markupview/test/Makefile.in
+++ b/browser/devtools/markupview/test/Makefile.in
@@ -14,12 +14,13 @@ MOCHITEST_BROWSER_FILES := \
 		browser_inspector_markup_navigation.html \
 		browser_inspector_markup_navigation.js \
 		browser_inspector_markup_mutation.html \
 		browser_inspector_markup_mutation.js \
 		browser_inspector_markup_edit.html \
     browser_inspector_markup_edit.js \
     browser_inspector_markup_subset.html \
     browser_inspector_markup_subset.js \
+    browser_bug896181_css_mixed_completion_new_attribute.js \
 		head.js \
 		$(NULL)
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/devtools/markupview/test/browser_bug896181_css_mixed_completion_new_attribute.js
@@ -0,0 +1,167 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test CSS state is correctly determined and the corresponding suggestions are
+// displayed. i.e. CSS property suggestions are shown when cursor is like:
+// ```style="di|"``` where | is teh cursor; And CSS value suggestion is
+// displayed when the cursor is like: ```style="display:n|"``` properly. No
+// suggestions should ever appear when the attribute is not a style attribute.
+// The correctness and cycling of the suggestions is covered in the ruleview
+// tests.
+
+function test() {
+  let inspector;
+  let {
+    getInplaceEditorForSpan: inplaceEditor
+  } = devtools.require("devtools/shared/inplace-editor");
+
+  waitForExplicitFinish();
+
+  // Will hold the doc we're viewing
+  let doc;
+
+  // Holds the MarkupTool object we're testing.
+  let markup;
+  let editor;
+  let state;
+  // format :
+  //  [
+  //    what key to press,
+  //    expected input box value after keypress,
+  //    expected input.selectionStart,
+  //    expected input.selectionEnd,
+  //    is popup expected to be open ?
+  //  ]
+  let testData = [
+    ['s', 's', 1, 1, false],
+    ['t', 'st', 2, 2, false],
+    ['y', 'sty', 3, 3, false],
+    ['l', 'styl', 4, 4, false],
+    ['e', 'style', 5, 5, false],
+    ['=', 'style=', 6, 6, false],
+    ['"', 'style="', 7, 7, false],
+    ['d', 'style="direction', 8, 16, true],
+    ['VK_DOWN', 'style="display', 8, 14, true],
+    ['VK_RIGHT', 'style="display', 14, 14, false],
+    [':', 'style="display:', 15, 15, false],
+    ['n', 'style="display:none', 16, 19, false],
+    ['VK_BACK_SPACE', 'style="display:n', 16, 16, false],
+    ['VK_BACK_SPACE', 'style="display:', 15, 15, false],
+    [' ', 'style="display: ', 16, 16, false],
+    [' ', 'style="display:  ', 17, 17, false],
+    ['i', 'style="display:  inherit', 18, 24, true],
+    ['VK_RIGHT', 'style="display:  inherit', 24, 24, false],
+    [';', 'style="display:  inherit;', 25, 25, false],
+    [' ', 'style="display:  inherit; ', 26, 26, false],
+    [' ', 'style="display:  inherit;  ', 27, 27, false],
+    ['VK_LEFT', 'style="display:  inherit;  ', 26, 26, false],
+    ['c', 'style="display:  inherit; caption-side ', 27, 38, true],
+    ['o', 'style="display:  inherit; color ', 28, 31, true],
+    ['VK_RIGHT', 'style="display:  inherit; color ', 31, 31, false],
+    [' ', 'style="display:  inherit; color  ', 32, 32, false],
+    ['c', 'style="display:  inherit; color c ', 33, 33, false],
+    ['VK_BACK_SPACE', 'style="display:  inherit; color  ', 32, 32, false],
+    [':', 'style="display:  inherit; color : ', 33, 33, false],
+    ['c', 'style="display:  inherit; color :cadetblue ', 34, 42, true],
+    ['VK_DOWN', 'style="display:  inherit; color :chartreuse ', 34, 43, true],
+    ['VK_RETURN', 'style="display:  inherit; color :chartreuse"', -1, -1, false]
+  ];
+
+  function startTests() {
+    markup = inspector.markup;
+    markup.expandAll().then(() => {
+      let node = getContainerForRawNode(markup, doc.querySelector("#node14")).editor;
+      let attr = node.newAttr;
+      attr.focus();
+      EventUtils.sendKey("return", inspector.panelWin);
+      editor = inplaceEditor(attr);
+      checkStateAndMoveOn(0);
+    });
+  }
+
+  function checkStateAndMoveOn(index) {
+    if (index == testData.length) {
+      finishUp();
+      return;
+    }
+
+    let [key] = testData[index];
+    state = index;
+
+    info("pressing key " + key + " to get result: [" + testData[index].slice(1) +
+         "] for state " + state);
+    if (/(down|left|right|back_space|return)/ig.test(key)) {
+      info("added event listener for down|left|right|back_space|return keys");
+      editor.input.addEventListener("keypress", function onKeypress() {
+        if (editor.input) {
+          editor.input.removeEventListener("keypress", onKeypress);
+        }
+        info("inside event listener");
+        checkState();
+      }) 
+    }
+    else {
+      editor.once("after-suggest", checkState);
+    }
+    EventUtils.synthesizeKey(key, {}, inspector.panelWin);
+  }
+
+  function checkState() {
+    executeSoon(() => {
+      info("After keypress for state " + state);
+      let [key, completion, selStart, selEnd, popupOpen] = testData[state];
+      if (selEnd != -1) {
+        is(editor.input.value, completion,
+           "Correct value is autocompleted for state " + state);
+        is(editor.input.selectionStart, selStart,
+           "Selection is starting at the right location for state " + state);
+        is(editor.input.selectionEnd, selEnd,
+           "Selection is ending at the right location for state " + state);
+        if (popupOpen) {
+          ok(editor.popup._panel.state == "open" ||
+             editor.popup._panel.state == "showing",
+             "Popup is open for state " + state);
+        }
+        else {
+          ok(editor.popup._panel.state != "open" &&
+             editor.popup._panel.state != "showing",
+             "Popup is closed for state " + state);
+        }
+      }
+      else {
+        let editor = getContainerForRawNode(markup, doc.querySelector("#node14")).editor;
+        let attr = editor.attrs["style"].querySelector(".editable");
+        is(attr.textContent, completion,
+           "Correct value is persisted after pressing Enter for state " + state);
+      }
+      checkStateAndMoveOn(state + 1);
+    });
+  }
+
+  // Create the helper tab for parsing...
+  gBrowser.selectedTab = gBrowser.addTab();
+  gBrowser.selectedBrowser.addEventListener("load", function onload() {
+    gBrowser.selectedBrowser.removeEventListener("load", onload, true);
+    doc = content.document;
+    waitForFocus(setupTest, content);
+  }, true);
+  content.location = "http://mochi.test:8888/browser/browser/devtools/markupview/test/browser_inspector_markup_edit.html";
+
+  function setupTest() {
+    var target = TargetFactory.forTab(gBrowser.selectedTab);
+    gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
+      inspector = toolbox.getCurrentPanel();
+      startTests();
+    });
+  }
+
+  function finishUp() {
+    while (markup.undo.canUndo()) {
+      markup.undo.undo();
+    }
+    doc = inspector = null;
+    gBrowser.removeCurrentTab();
+    finish();
+  }
+}
--- a/browser/devtools/shared/inplace-editor.js
+++ b/browser/devtools/shared/inplace-editor.js
@@ -754,17 +754,23 @@ InplaceEditor.prototype = {
     } else if (increment && this.popup && this.popup.isOpen) {
       cycling = true;
       prevent = true;
       if (increment > 0) {
         this.popup.selectPreviousItem();
       } else {
         this.popup.selectNextItem();
       }
-      this.input.value = this.popup.selectedItem.label;
+      let input = this.input;
+      let pre = input.value.slice(0, input.selectionStart);
+      let post = input.value.slice(input.selectionEnd, input.value.length);
+      let item = this.popup.selectedItem;
+      let toComplete = item.label.slice(item.preLabel.length);
+      input.value = pre + toComplete + post;
+      input.setSelectionRange(pre.length, pre.length + toComplete.length);
       this._updateSize();
       // This emit is mainly for the purpose of making the test flow simpler.
       this.emit("after-suggest");
     }
 
     if (aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_BACK_SPACE ||
         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_DELETE ||
         aEvent.keyCode === Ci.nsIDOMKeyEvent.DOM_VK_LEFT ||
@@ -893,55 +899,80 @@ InplaceEditor.prototype = {
       }
 
       let input = this.input;
       // Input can be null in cases when you intantaneously switch out of it.
       if (!input) {
         return;
       }
       let query = input.value.slice(0, input.selectionStart);
+      let startCheckQuery = query;
       if (!query) {
         return;
       }
 
       let list = [];
       if (this.contentType == CONTENT_TYPES.CSS_PROPERTY) {
         list = CSSPropertyList;
       } else if (this.contentType == CONTENT_TYPES.CSS_VALUE) {
-        list = domUtils.getCSSValuesForProperty(this.property.name).sort();
+        list = domUtils.getCSSValuesForProperty(this.property.name);
+      } else if (this.contentType == CONTENT_TYPES.CSS_MIXED &&
+                 /^\s*style\s*=/.test(query)) {
+        // Detecting if cursor is at property or value;
+        let match = query.match(/([:;"'=]?)\s*([^"';:= ]+)$/);
+        if (match && match.length == 3) {
+          if (match[1] == ":") { // We are in CSS value completion
+            let propertyName =
+              query.match(/[;"'=]\s*([^"';:= ]+)\s*:\s*[^"';:= ]+$/)[1];
+            list = domUtils.getCSSValuesForProperty(propertyName);
+            startCheckQuery = match[2];
+          } else if (match[1]) { // We are in CSS property name completion
+            list = CSSPropertyList;
+            startCheckQuery = match[2];
+          }
+          if (!startCheckQuery) {
+            // This emit is mainly to make the test flow simpler.
+            this.emit("after-suggest", "nothing to autocomplete");
+            return;
+          }
+        }
       }
 
       list.some(item => {
-        if (item.startsWith(query)) {
-          input.value = item;
-          input.setSelectionRange(query.length, item.length);
+        if (item.startsWith(startCheckQuery)) {
+          input.value = query + item.slice(startCheckQuery.length) +
+                        input.value.slice(query.length);
+          input.setSelectionRange(query.length, query.length + item.length -
+                                                startCheckQuery.length);
           this._updateSize();
           return true;
         }
       });
 
       if (!this.popup) {
+        // This emit is mainly to make the test flow simpler.
+        this.emit("after-suggest", "no popup");
         return;
       }
       let finalList = [];
       let length = list.length;
       for (let i = 0, count = 0; i < length && count < MAX_POPUP_ENTRIES; i++) {
-        if (list[i].startsWith(query)) {
+        if (list[i].startsWith(startCheckQuery)) {
           count++;
           finalList.push({
-            preLabel: query,
+            preLabel: startCheckQuery,
             label: list[i]
           });
         }
         else if (count > 0) {
           // Since count was incremented, we had already crossed the entries
           // which would have started with query, assuming that list is sorted.
           break;
         }
-        else if (list[i][0] > query[0]) {
+        else if (list[i][0] > startCheckQuery[0]) {
           // We have crossed all possible matches alphabetically.
           break;
         }
       }
 
       if (finalList.length > 1) {
         this.popup.setItems(finalList);
         this.popup.openPopup(this.input);
--- a/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_existing_property.js
+++ b/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_existing_property.js
@@ -34,21 +34,16 @@ let testData = [
   ["d", "direction", 0, 3],
   ["VK_DOWN", "display", 1, 3],
   ["VK_DOWN", "dominant-baseline", 2, 3],
   ["VK_DOWN", "direction", 0, 3],
   ["VK_DOWN", "display", 1, 3],
   ["VK_UP", "direction", 0, 3],
   ["VK_UP", "dominant-baseline", 2, 3],
   ["VK_UP", "display", 1, 3],
-  ["VK_BACK_SPACE", "displa", -1, 0],
-  ["VK_BACK_SPACE", "displ", -1, 0],
-  ["VK_BACK_SPACE", "disp", -1, 0],
-  ["VK_BACK_SPACE", "dis", -1, 0],
-  ["VK_BACK_SPACE", "di", -1, 0],
   ["VK_BACK_SPACE", "d", -1, 0],
   ["i", "direction", 0, 2],
   ["s", "display", -1, 0],
   ["VK_BACK_SPACE", "dis", -1, 0],
   ["VK_BACK_SPACE", "di", -1, 0],
   ["VK_BACK_SPACE", "d", -1, 0],
   ["VK_BACK_SPACE", "", -1, 0],
   ["f", "fill", 0, MAX_ENTRIES],
--- a/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_new_property.js
+++ b/browser/devtools/styleinspector/test/browser_bug893965_css_property_completion_new_property.js
@@ -22,21 +22,16 @@ let testData = [
   ["d", "direction", 0, 3],
   ["VK_DOWN", "display", 1, 3],
   ["VK_DOWN", "dominant-baseline", 2, 3],
   ["VK_DOWN", "direction", 0, 3],
   ["VK_DOWN", "display", 1, 3],
   ["VK_UP", "direction", 0, 3],
   ["VK_UP", "dominant-baseline", 2, 3],
   ["VK_UP", "display", 1, 3],
-  ["VK_BACK_SPACE", "displa", -1, 0],
-  ["VK_BACK_SPACE", "displ", -1, 0],
-  ["VK_BACK_SPACE", "disp", -1, 0],
-  ["VK_BACK_SPACE", "dis", -1, 0],
-  ["VK_BACK_SPACE", "di", -1, 0],
   ["VK_BACK_SPACE", "d", -1, 0],
   ["i", "direction", 0, 2],
   ["s", "display", -1, 0],
   ["VK_BACK_SPACE", "dis", -1, 0],
   ["VK_BACK_SPACE", "di", -1, 0],
   ["VK_BACK_SPACE", "d", -1, 0],
   ["VK_BACK_SPACE", "", -1, 0],
   ["f", "fill", 0, MAX_ENTRIES],
--- a/browser/locales/en-US/chrome/browser/browser.dtd
+++ b/browser/locales/en-US/chrome/browser/browser.dtd
@@ -202,16 +202,19 @@ These should match what Safari and other
 <!ENTITY downloadsUnix.commandkey     "y">
 <!ENTITY addons.label                 "Add-ons">
 <!ENTITY addons.accesskey             "A">
 <!ENTITY addons.commandkey            "A">
 
 <!ENTITY webDeveloperMenu.label       "Web Developer">
 <!ENTITY webDeveloperMenu.accesskey   "W">
 
+<!ENTITY devToolsCmd.keycode          "VK_F12">
+<!ENTITY devToolsCmd.keytext          "F12">
+
 <!ENTITY devtoolsConnect.label        "Connect…">
 <!ENTITY devtoolsConnect.accesskey    "e">
 
 <!ENTITY errorConsoleCmd.label        "Error Console">
 <!ENTITY errorConsoleCmd.accesskey    "C">
 
 <!ENTITY remoteWebConsoleCmd.label    "Remote Web Console">
 
--- a/browser/metro/base/content/ContextCommands.js
+++ b/browser/metro/base/content/ContextCommands.js
@@ -63,16 +63,18 @@ var ContextCommands = {
 
     if (target.localName == "browser") {
       // content
       if (ContextMenuUI.popupState.string) {
         this.sendCommand("copy");
 
         SelectionHelperUI.closeEditSession(true);
       }
+    } else if (ContextMenuUI.popupState.string) {
+      this.clipboard.copyString(ContextMenuUI.popupState.string, this.docRef);
     } else {
       // chrome
       target.editor.copy();
     }
 
     target.focus();
   },
 
--- a/browser/metro/base/content/bindings/appbar.xml
+++ b/browser/metro/base/content/bindings/appbar.xml
@@ -68,15 +68,10 @@
               this.dismiss();
             } else {
               this.show();
             }
           ]]>
         </body>
       </method>
     </implementation>
-
-    <handlers>
-      <!-- Work around for bug 835175 -->
-      <handler event="click">false;</handler>
-    </handlers>
   </binding>
 </bindings>
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/content/bindings/circularprogress.xml
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<bindings xmlns="http://www.mozilla.org/xbl"
+          xmlns:xbl="http://www.mozilla.org/xbl"
+          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+          xmlns:html="http://www.w3.org/1999/xhtml">
+  <binding id="circular-progress-indicator">
+    <content>
+      <xul:stack>
+        <xul:toolbarbutton anonid="progressButton" class="circularprogressindicator-progressButton appbar-secondary"/>
+        <html:canvas anonid="progressRing" class="circularprogressindicator-progressRing" width="46" height="46"></html:canvas>
+      </xul:stack>
+    </content>
+    <implementation>
+      <field name="_progressCanvas">
+          document.getAnonymousElementByAttribute(this, "anonid", "progressRing");
+      </field>
+      <field name="_progressCircleCtx">null</field>
+      <field name="_img">null</field>
+      <constructor>
+        <![CDATA[
+          this._progressCircleCtx = this._progressCanvas.getContext('2d');
+          this._img = new Image();
+        ]]>
+      </constructor>
+      <method name="updateProgress">
+        <parameter name="percentComplete"/>
+        <body>
+          <![CDATA[
+            const PROGRESS_RING_IMG = "chrome://browser/skin/images/progresscircle.png";
+
+            let startAngle = 1.5 * Math.PI;
+            let endAngle = startAngle + (2 * Math.PI * (percentComplete / 100));
+
+            let ctx = this._progressCircleCtx;
+            ctx.clearRect(0, 0,
+              this._progressCanvas.width, this._progressCanvas.height);
+
+            // Save the state, so we can undo the clipping
+            ctx.save();
+
+            ctx.beginPath();
+            let center = this._progressCanvas.width / 2;
+            ctx.arc(center, center, center, startAngle, endAngle, false);
+            ctx.lineTo(center, center);
+            ctx.closePath();
+            ctx.clip();
+
+            // Draw circle image.
+            if (this._img && this._img.src) {
+              ctx.drawImage(this._img, 0, 0);
+            } else {
+              this._img.onload = function() {
+                ctx.drawImage(this._img, 0, 0);
+              }.bind(this);
+              this._img.src = PROGRESS_RING_IMG;
+            }
+
+            ctx.restore();
+            return [startAngle, endAngle];
+          ]]>
+        </body>
+      </method>
+      <method name="reset">
+        <body>
+          <![CDATA[
+            this._progressCircleCtx.clearRect(0, 0,
+              this._progressCanvas.width, this._progressCanvas.height);
+          ]]>
+        </body>
+      </method>
+    </implementation>
+  </binding>
+</bindings>
\ No newline at end of file
--- a/browser/metro/base/content/bindings/flyoutpanel.xml
+++ b/browser/metro/base/content/bindings/flyoutpanel.xml
@@ -94,15 +94,10 @@
         </body>
       </method>
 
       <field name="anonScrollBox" readonly="true"><![CDATA[
         // Expose the anyonymous scrollbox so ScrollUtils.getScrollboxFromElement can find it.
         document.getAnonymousElementByAttribute(this, "class", "flyoutpanel-contents");
       ]]></field>
     </implementation>
-
-    <handlers>
-      <!-- Work around for bug 835175 -->
-      <handler event="click">false;</handler>
-    </handlers>
   </binding>
 </bindings>
--- a/browser/metro/base/content/browser.css
+++ b/browser/metro/base/content/browser.css
@@ -37,16 +37,20 @@ settings {
 setting {
   display: none;
 }
 
 autoscroller {
   -moz-binding: url('chrome://browser/content/bindings/popup.xml#element-popup');
 }
 
+circularprogressindicator {
+  -moz-binding: url('chrome://browser/content/bindings/circularprogress.xml#circular-progress-indicator');
+}
+
 setting[type="bool"] {
   display: -moz-box;
   -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-bool");
 }
 
 setting[type="bool"][localized="true"] {
   display: -moz-box;
   -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-localized-bool");
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -176,18 +176,17 @@
   </keyset>
 
   <stack id="stack" flex="1">
     <observes element="bcast_urlbarState" attribute="*"/>
     <!-- Page Area -->
     <vbox id="page">
       <vbox id="tray" class="tray-toolbar" observes="bcast_windowState" >
         <!-- Tabs -->
-        <!-- onclick handler to work around bug 837242 -->
-        <hbox id="tabs-container" observes="bcast_windowState" onclick="void(0);">
+        <hbox id="tabs-container" observes="bcast_windowState">
           <box id="tabs" flex="1"
                 observes="bcast_preciseInput"
                 onselect="BrowserUI.selectTabAndDismiss(this);"
                 onclosetab="BrowserUI.closeTab(this);"/>
           <vbox id="tabs-controls">
             <toolbarbutton id="newtab-button" command="cmd_newTab" label="&newtab.label;"/>
           </vbox>
         </hbox>
@@ -238,18 +237,17 @@
 
       <!-- Content viewport -->
       <stack id="content-viewport">
         <deck id="browsers" flex="1" observes="bcast_preciseInput"/>
         <box id="vertical-scroller" class="scroller" orient="vertical" end="0" top="0"/>
         <box id="horizontal-scroller" class="scroller" orient="horizontal" left="0" bottom="0"/>
 
         <!-- Content touch selection overlay -->
-        <!-- onclick addresses dom bug 835175 -->
-        <box onclick="false" class="selection-overlay-hidden" id="content-selection-overlay"/>
+        <box class="selection-overlay-hidden" id="content-selection-overlay"/>
       </stack>
     </vbox>
 
     <html:div id="overlay-back" class="overlay-button"
               observes="cmd_back"></html:div>
     <html:div id="overlay-plus" class="overlay-button"
               observes="cmd_back"></html:div>
 
@@ -288,16 +286,18 @@
             <toolbarbutton id="reload-button" class="urlbar-button"
                            oncommand="CommandUpdater.doCommand(
                                         event.shiftKey ? 'cmd_forceReload'
                                                        : 'cmd_reload');"/>
             <toolbarbutton id="stop-button" class="urlbar-button"
                            command="cmd_stop"/>
           </hbox>
 
+          <circularprogressindicator id="download-progress"
+                                     oncommand="Appbar.onDownloadButton()"/>
           <stack id="toolbar-contextual">
             <observes element="bcast_windowState" attribute="*"/>
             <observes element="bcast_urlbarState" attribute="*"/>
 
             <hbox id="toolbar-context-page" pack="end">
               <toolbarbutton id="download-button" class="appbar-secondary"
                              oncommand="Appbar.onDownloadButton()"/>
               <toolbarbutton id="star-button" class="appbar-primary"
@@ -731,21 +731,19 @@
             <radio id="prefs-dnt-nopref" label="&doNotTrack.options.noPreference;" value="-1"/>
             <radio id="prefs-dnt-oktrack" label="&doNotTrack.options.trackingOkay;" value="0"/>
           </radiogroup>
         </setting>
       </settings>
     </flyoutpanel>
 
     <!-- Chrome touch selection overlay -->
-    <!-- onclick addresses dom bug 835175 -->
-    <box onclick="false" class="selection-overlay-hidden" id="chrome-selection-overlay"/>
+    <box class="selection-overlay-hidden" id="chrome-selection-overlay"/>
 
     <box onclick="event.stopPropagation();" id="context-container" class="menu-container" hidden="true">
-      <!-- onclick is dom bug 835175 -->
       <vbox id="context-popup" class="menu-popup">
         <richlistbox id="context-commands" bindingType="contextmenu" flex="1">
           <!-- priority="low" items are hidden by default when a context is being displayed
                for two or more media types. (e.g. a linked image) -->
           <!-- content types preceeded by '!' act as exclusion rules, the menu item will not
                be displayed if the content type is present. -->
           <!-- Note the order of richlistitem here is important as it is reflected in the
                menu itself. -->
--- a/browser/metro/base/content/console.js
+++ b/browser/metro/base/content/console.js
@@ -238,17 +238,19 @@ let ConsolePanelView = {
     let row = aEvent.target;
     let text = ["msg", "href", "line", "code", "col"].map(function(attr) row.getAttribute(attr))
                .filter(function(x) x).join("\n");
 
     ContextMenuUI.showContextMenu({
       target: row,
       json: {
         types: ["copy"],
-        string: text
+        string: text,
+        xPos: aEvent.clientX,
+        yPos: aEvent.clientY
       }
     });
   },
 
   onEvalKeyPress: function cv_onEvalKeyPress(aEvent) {
     if (aEvent.keyCode == 13)
       this.evaluateTypein();
   },
--- a/browser/metro/base/content/downloads.js
+++ b/browser/metro/base/content/downloads.js
@@ -7,16 +7,17 @@ const URI_GENERIC_ICON_DOWNLOAD = "chrom
 
 var Downloads = {
   /**
    * _downloadCount keeps track of the number of downloads that a single
    * notification bar groups together. A download is grouped with other
    * downloads if it starts before other downloads have completed.
    */
   _downloadCount: 0,
+  _downloadsInProgress: 0,
   _inited: false,
   _progressAlert: null,
   _lastSec: Infinity,
   _notificationBox: null,
   _progressNotification: null,
   _progressNotificationInfo: new Map(),
   _runDownloadBooleanMap: new Map(),
 
@@ -52,16 +53,18 @@ var Downloads = {
     Services.obs.addObserver(this, "dl-done", true);
     Services.obs.addObserver(this, "dl-run", true);
     Services.obs.addObserver(this, "dl-failed", true);
 
     this._notificationBox = Browser.getNotificationBox();
 
     this._progress = new DownloadProgressListener(this);
     this.manager.addListener(this._progress);
+
+    this._downloadProgressIndicator = document.getElementById("download-progress");
   },
 
   uninit: function dh_uninit() {
     if (this._inited) {
       Services.obs.removeObserver(this, "dl-start");
       Services.obs.removeObserver(this, "dl-done");
       Services.obs.removeObserver(this, "dl-run");
       Services.obs.removeObserver(this, "dl-failed");
@@ -92,35 +95,36 @@ var Downloads = {
     let download = this.manager.getDownload(id);
 
     if (download) {
       this.manager.removeDownload(id);
     }
   },
 
   cancelDownload: function dh_cancelDownload(aDownload) {
-    this._progressNotificationInfo.delete(aDownload.guid);
-    this._runDownloadBooleanMap.delete(aDownload.targetFile.path);
-    this._downloadCount--;
-    if (this._progressNotificationInfo.size == 0) {
-      this._notificationBox.removeNotification(this._progressNotification);
-      this._progressNotification = null;
-    }
-
     let fileURI = aDownload.target;
     if (!(fileURI && fileURI.spec)) {
       Util.dumpLn("Cant remove download file for: "+aDownload.id+", fileURI is invalid");
-      return;
     }
 
-    let file = this._getLocalFile(fileURI);
     try {
-      this.manager.cancelDownload(aDownload.id);
+      let file = this._getLocalFile(fileURI);
       if (file && file.exists())
         file.remove(false);
+      this.manager.cancelDownload(aDownload.id);
+
+      // If cancelling was successful, stop tracking the download.
+      this._progressNotificationInfo.delete(aDownload.guid);
+      this._runDownloadBooleanMap.delete(aDownload.targetFile.path);
+      this._downloadCount--;
+      this._downloadsInProgress--;
+      if (this._downloadsInProgress <= 0) {
+        this._notificationBox.removeNotification(this._progressNotification);
+        this._progressNotification = null;
+      }
     } catch (ex) {
       Util.dumpLn("Failed to cancel download, with id: "+aDownload.id+", download target URI spec: " + fileURI.spec);
       Util.dumpLn("Failed download source:"+(aDownload.source && aDownload.source.spec));
     }
   },
 
   // Cancels all downloads.
   cancelDownloads: function dh_cancelDownloads() {
@@ -191,23 +195,25 @@ var Downloads = {
 
     let buttons = [
       {
         isDefault: true,
         label: tryAgainButtonText,
         accessKey: "",
         callback: function() {
           Downloads.manager.retryDownload(aDownload.id);
+          Downloads._downloadProgressIndicator.reset();
         }
       },
       {
         label: cancelButtonText,
         accessKey: "",
         callback: function() {
           Downloads.cancelDownload(aDownload);
+          Downloads._downloadProgressIndicator.reset();
         }
       }
     ];
     this.showNotification("download-failed", message, buttons,
       this._notificationBox.PRIORITY_WARNING_HIGH);
   },
 
   _showDownloadCompleteNotification: function (aDownload) {
@@ -217,16 +223,17 @@ var Downloads = {
     let buttons = [
       {
         label: showInFilesButtonText,
         accessKey: "",
         callback: function() {
           let fileURI = aDownload.target;
           let file = Downloads._getLocalFile(fileURI);
           file.reveal();
+          Downloads._downloadProgressIndicator.reset();
         }
       }
     ];
 
     if (this._downloadCount > 1) {
       message = PluralForm.get(this._downloadCount,
                                Strings.browser.GetStringFromName("alertMultipleDownloadsComplete"))
                                .replace("#1", this._downloadCount)
@@ -237,34 +244,52 @@ var Downloads = {
         [aDownload.displayName], 1);
 
       buttons.unshift({
         isDefault: true,
         label: runButtonText,
         accessKey: "",
         callback: function() {
           Downloads.openDownload(aDownload);
+          Downloads._downloadProgressIndicator.reset();
         }
       });
     }
     this.showNotification("download-complete", message, buttons,
       this._notificationBox.PRIORITY_WARNING_MEDIUM);
   },
 
+  _updateCircularProgressMeter: function dv_updateCircularProgressMeter() {
+    if (!this._progressNotificationInfo) {
+      return;
+    }
+
+    let totPercent = 0;
+    for (let info of this._progressNotificationInfo) {
+      // info[0]          => download guid
+      // info[1].download => nsIDownload
+      totPercent += info[1].download.percentComplete;
+    }
+
+    let percentComplete = totPercent / this._progressNotificationInfo.size;
+    this._downloadProgressIndicator.updateProgress(percentComplete);
+  },
+
   _computeDownloadProgressString: function dv_computeDownloadProgressString(aDownload) {
     let totTransferred = 0, totSize = 0, totSecondsLeft = 0;
     for (let info of this._progressNotificationInfo) {
       let size = info[1].download.size;
       let amountTransferred = info[1].download.amountTransferred;
       let speed = info[1].download.speed;
 
       totTransferred += amountTransferred;
       totSize += size;
       totSecondsLeft += ((size - amountTransferred) / speed);
     }
+
     // Compute progress in bytes.
     let amountTransferred = Util.getDownloadSize(totTransferred);
     let size = Util.getDownloadSize(totSize);
     let progress = amountTransferred + "/" + size;
 
     // Compute progress in time.;
     let [timeLeft, newLast] = DownloadUtils.getTimeLeft(totSecondsLeft, this._lastSec);
     this._lastSec = newLast;
@@ -291,30 +316,32 @@ var Downloads = {
     let infoObj = this._progressNotificationInfo.get(aDownload.guid);
     infoObj.download = aDownload;
     this._progressNotificationInfo.set(aDownload.guid, infoObj);
   },
 
   updateInfobar: function dv_updateInfobar(aDownload) {
     this._saveDownloadData(aDownload);
     let message = this._computeDownloadProgressString(aDownload);
+    this._updateCircularProgressMeter();
 
     if (this._progressNotification == null ||
         !this._notificationBox.getNotificationWithValue("download-progress")) {
 
       let cancelButtonText =
               Strings.browser.GetStringFromName("downloadCancel");
 
       let buttons = [
         {
           isDefault: false,
           label: cancelButtonText,
           accessKey: "",
           callback: function() {
             Downloads.cancelDownloads();
+            Downloads._downloadProgressIndicator.reset();
           }
         }
       ];
 
       this._progressNotification =
         this.showNotification("download-progress", message, buttons,
         this._notificationBox.PRIORITY_WARNING_LOW);
     } else {
@@ -322,60 +349,64 @@ var Downloads = {
     }
   },
 
   updateDownload: function dv_updateDownload(aDownload) {
     if (this._progressNotification != null) {
       this._saveDownloadData(aDownload);
       this._progressNotification.label =
         this._computeDownloadProgressString(aDownload);
+      this._updateCircularProgressMeter();
     }
   },
 
   observe: function (aSubject, aTopic, aData) {
     let message = "";
     let msgTitle = "";
 
     switch (aTopic) {
       case "dl-run":
         let file = aSubject.QueryInterface(Ci.nsIFile);
         this._runDownloadBooleanMap.set(file.path, (aData == 'true'));
         break;
       case "dl-start":
         this._downloadCount++;
+        this._downloadsInProgress++;
         let download = aSubject.QueryInterface(Ci.nsIDownload);
         if (!this._progressNotificationInfo.get(download.guid)) {
           this._progressNotificationInfo.set(download.guid, {});
         }
         if (!this._progressAlert) {
           this._progressAlert = new AlertDownloadProgressListener();
           this.manager.addListener(this._progressAlert);
         }
         this.updateInfobar(download);
         break;
       case "dl-done":
+        this._downloadsInProgress--;
         download = aSubject.QueryInterface(Ci.nsIDownload);
         let runAfterDownload = this._runDownloadBooleanMap.get(download.targetFile.path);
         if (runAfterDownload) {
           this.openDownload(download);
         }
 
-        this._progressNotificationInfo.delete(download.guid);
         this._runDownloadBooleanMap.delete(download.targetFile.path);
-        if (this._progressNotificationInfo.size == 0) {
+        if (this._downloadsInProgress == 0) {
           if (this._downloadCount > 1 || !runAfterDownload) {
             this._showDownloadCompleteNotification(download);
           }
+          this._progressNotificationInfo.clear();
           this._downloadCount = 0;
           this._notificationBox.removeNotification(this._progressNotification);
           this._progressNotification = null;
         }
         break;
       case "dl-failed":
         download = aSubject.QueryInterface(Ci.nsIDownload);
+        this._showDownloadFailedNotification(download);
         break;
     }
   },
 
   QueryInterface: function (aIID) {
     if (!aIID.equals(Ci.nsIObserver) &&
         !aIID.equals(Ci.nsISupportsWeakReference) &&
         !aIID.equals(Ci.nsISupports))
--- a/browser/metro/base/jar.mn
+++ b/browser/metro/base/jar.mn
@@ -21,16 +21,17 @@ chrome.jar:
   content/bindings/arrowbox.xml                (content/bindings/arrowbox.xml)
   content/bindings/grid.xml                    (content/bindings/grid.xml)
   content/bindings/urlbar.xml                  (content/bindings/urlbar.xml)
   content/bindings/appbar.xml                  (content/bindings/appbar.xml)
   content/bindings/flyoutpanel.xml             (content/bindings/flyoutpanel.xml)
   content/bindings/selectionoverlay.xml        (content/bindings/selectionoverlay.xml)
   content/bindings/cssthrobber.xml             (content/bindings/cssthrobber.xml)
   content/bindings/popup.xml                   (content/bindings/popup.xml)
+  content/bindings/circularprogress.xml        (content/bindings/circularprogress.xml)
 
 * content/flyoutpanels/FlyoutPanelsUI.js       (content/flyoutpanels/FlyoutPanelsUI.js)
 * content/flyoutpanels/AboutFlyoutPanel.js     (content/flyoutpanels/AboutFlyoutPanel.js)
   content/flyoutpanels/PrefsFlyoutPanel.js     (content/flyoutpanels/PrefsFlyoutPanel.js)
 
   content/prompt/alert.xul                     (content/prompt/alert.xul)
   content/prompt/confirm.xul                   (content/prompt/confirm.xul)
   content/prompt/prompt.xul                    (content/prompt/prompt.xul)
--- a/browser/metro/base/tests/mochitest/Makefile.in
+++ b/browser/metro/base/tests/mochitest/Makefile.in
@@ -10,28 +10,30 @@ relativesrcdir  = @relativesrcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 MOCHITEST_METRO_FILES = \
   head.js \
   browser_urlbar.js \
   browser_bookmarks.js \
   browser_canonizeURL.js \
+  browser_circular_progress_indicator.js \
   browser_context_menu_tests.js \
   browser_context_menu_tests_01.html \
   browser_context_menu_tests_02.html \
   browser_context_menu_tests_03.html \
   browser_context_ui.js \
   browser_downloads.js \
   browser_findbar.js \
   browser_findbar.html \
   browser_history.js \
   browser_onscreen_keyboard.js \
   browser_onscreen_keyboard.html \
   browser_prefs_ui.js \
+  browser_progress_indicator.xul \
   browser_remotetabs.js \
   browser_tabs.js \
   browser_test.js \
   browser_tiles.js \
   browser_tilegrid.xul \
   browser_topsites.js \
   browser_form_auto_complete.js \
   browser_form_auto_complete.html \
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/browser_circular_progress_indicator.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let doc;
+
+function test() {
+  waitForExplicitFinish();
+  Task.spawn(function(){
+    netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+
+    info(chromeRoot + "browser_progress_indicator.xul");
+    yield addTab(chromeRoot + "browser_progress_indicator.xul");
+    doc = Browser.selectedTab.browser.contentWindow.document;
+  }).then(runTests);
+}
+
+gTests.push({
+  desc: "circular progress indicator binding is applied.",
+  run: function() {
+    ok(doc, "doc got defined");
+
+    let progressIndicator = doc.querySelector("#progress-indicator");
+    ok(progressIndicator, "progress-indicator is found");
+    is(typeof progressIndicator.reset, "function", "#progress-indicator has a reset() function");
+    is(typeof progressIndicator.updateProgress, "function", "#progress-indicator has a updateProgress() function");
+  }
+});
+
+gTests.push({
+  desc: "start and end angles are correct for various percents complete",
+  run: function() {
+    let progressIndicator = doc.querySelector("#progress-indicator");
+    ok(progressIndicator, "progress-indicator is found");
+    is(typeof progressIndicator.updateProgress, "function", "#progress-indicator has a updateProgress() function");
+
+    let expectedStartAngle = 1.5 * Math.PI;
+
+    let percentComplete = 0;
+    let [startAngle, endAngle] = progressIndicator.updateProgress(percentComplete);
+    is(startAngle, expectedStartAngle, "start angle is correct");
+    is(endAngle, startAngle + (2 * Math.PI * (percentComplete / 100)), "end angle is correct");
+
+    percentComplete = 0.05;
+    [startAngle, endAngle] = progressIndicator.updateProgress(percentComplete);
+    is(startAngle, expectedStartAngle, "start angle is correct");
+    is(endAngle, startAngle + (2 * Math.PI * (percentComplete / 100)), "end angle is correct");
+
+    percentComplete = 0.5;
+    [startAngle, endAngle] = progressIndicator.updateProgress(percentComplete);
+    is(startAngle, expectedStartAngle, "start angle is correct");
+    is(endAngle, startAngle + (2 * Math.PI * (percentComplete / 100)), "end angle is correct");
+
+    percentComplete = 1;
+    [startAngle, endAngle] = progressIndicator.updateProgress(percentComplete);
+    is(startAngle, expectedStartAngle, "start angle is correct");
+    is(endAngle, startAngle + (2 * Math.PI * (percentComplete / 100)), "end angle is correct");
+  }
+});
\ No newline at end of file
--- a/browser/metro/base/tests/mochitest/browser_downloads.js
+++ b/browser/metro/base/tests/mochitest/browser_downloads.js
@@ -326,9 +326,19 @@ gTests.push({
     }
     finally {
       // Clean up when the test finishes.
       yield resetDownloads();
     }
   }
 });
 
-
+/**
+ * Make sure the cancelled/aborted downloads are handled correctly.
+ */
+gTests.push({
+  desc: "Cancel/Abort Downloads",
+  run: function(){
+    todo(false, "Ensure that a cancelled/aborted download is in the correct state \
+      including correct values for state variables (e.g. _downloadCount, _downloadsInProgress) \
+      and the existence of the downloaded file.");
+  }
+});
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/browser/metro/base/tests/mochitest/browser_progress_indicator.xul
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+   - License, v. 2.0. If a copy of the MPL was not distributed with this
+   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://browser/skin/platform.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/skin/browser.css" type="text/css"?>
+<?xml-stylesheet href="chrome://browser/content/browser.css" type="text/css"?>
+
+<!DOCTYPE window []>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+  <circularprogressindicator id="progress-indicator" oncommand=""/>
+</window>
\ No newline at end of file
--- a/browser/metro/theme/browser.css
+++ b/browser/metro/theme/browser.css
@@ -439,16 +439,22 @@ documenttab[selected] .documenttab-selec
 #overlay-plus[mousedrag] {
   transition-property: left, right, transform, background-position,
                        background-color, background-size, border-color,
                        visibility, box-shadow;
 }
 
 /* Navigation bar ========================================================== */
 
+.circularprogressindicator-progressRing {
+  margin: 0 @toolbar_horizontal_spacing@;
+  pointer-events:none;
+  position: absolute;
+}
+
 /* Progress meter ---------------------------------------------------------- */
 
 #progress-container {
   display: block;
   position: absolute;
   top: -@progress_height@;
   height: @progress_height@;
   width: 100%;
@@ -684,23 +690,28 @@ documenttab[selected] .documenttab-selec
 #toolbar-contextual:not([autocomplete]) #toolbar-context-autocomplete,
 #toolbar-contextual[startpage] #toolbar-context-page,
 #toolbar-contextual[autocomplete] #toolbar-context-page {
   opacity: 0;
   visibility: hidden;
   pointer-events: none;
 }
 
-#download-button {
+.circularprogressindicator-progressButton {
+  margin: 0 @toolbar_horizontal_spacing@;
   -moz-image-region: rect(0px, 40px, 40px, 0px) !important;
 }
+<<<<<<< local
 #download-button:hover:not(:active) {
+=======
+.circularprogressindicator-progressButton:hover {
+>>>>>>> other
   -moz-image-region: rect(40px, 40px, 80px, 0px) !important;
 }
-#download-button:active {
+.circularprogressindicator-progressButton:active {
   -moz-image-region: rect(80px, 40px, 120px, 0px) !important;
 }
 
 #pin-button {
   list-style-image: url(chrome://browser/skin/images/navbar-pin.png);
 }
 
 #star-button {
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f0a4dbfdf948a8227a3076f6fa4876ee4b36b210
GIT binary patch
literal 363
zc%17D@N?(olHy`uVBq!ia0vp^dLYcf1|-9GYMTQowj^(N7l!{JxM1({$v_d#0*}aI
z1_s$S5N3>BB-{uTWH0gbb!ETHE+woYAbVaZ5GeG+)5S3)<KEkuCj}21aJU{_uXJ_k
zj*#hkbE?xGZP1r0{C0r3aYEwSg-&_1;@)?Ez5Chm>w}mZ4C*UbZX9IPVl}gH$X(F-
zVM<g*AFEGVMs8!_oE;a}_D{IHb=g+Or5D(uL-^Ki@zh-)x^3dcr;px$jF(Gy;I_P=
zbct)W$NL^X^~SP`X}69^X-Q00i+|ZPE%Q)<#5^tbKMnF9nC1BIE!4bgu)ue-pxIL2
z;yp&O|8BUg-ro7(Ez|1fq8F$AeVRU(<FcL2;t86yY)*$R%UL&F{>xaotT$+S?Yz51
zXLNeIt|!c2nEg4_ORVO^C#_43A4(T)XMV|j>jcxiSi24Uz0OlZC+Y*kfWgz%&t;uc
GLK6Vn&z7qI
--- a/browser/metro/theme/jar.mn
+++ b/browser/metro/theme/jar.mn
@@ -94,12 +94,13 @@ chrome.jar:
   skin/images/exitfullscreen-hdpi.png       (images/exitfullscreen-hdpi.png)
   skin/images/scrubber-hdpi.png             (images/scrubber-hdpi.png)
   skin/images/selection-monocle.png         (images/selection-monocle.png)
   skin/images/appbar-icons.png              (images/appbar-icons.png)
   skin/images/pinned-hdpi.png               (images/pinned-hdpi.png)
   skin/images/tile-selected-check-hdpi.png  (images/tile-selected-check-hdpi.png)
   skin/images/plus-34.png                   (images/plus-34.png)
   skin/images/plus-24.png                   (images/plus-24.png)
+  skin/images/progresscircle.png            (images/progresscircle.png)
 
   skin/images/overlay-back.png              (images/overlay-back.png)
   skin/images/overlay-plus.png              (images/overlay-plus.png)
   skin/images/autoscroll.png                (images/autoscroll.png)
--- a/browser/themes/linux/devtools/layoutview.css
+++ b/browser/themes/linux/devtools/layoutview.css
@@ -6,17 +6,17 @@
   background-image: url(layout-background-grid.png);
 }
 
 .theme-light .theme-body {
   background-image: url(layout-background-grid.png), radial-gradient(circle at 50% 70%, hsl(210,53%,45%) 0%, hsl(210,54%,33%) 100%);
 }
 
 .theme-body {
-  color: hsl(210,100%,85%);
+  color: hsl(210,100%,85%) !important;
   -moz-box-sizing: border-box;
 }
 
 #main {
   border-color: hsla(210,100%,85%,0.7);
   border-style: dotted;
 }
 
--- a/browser/themes/osx/devtools/layoutview.css
+++ b/browser/themes/osx/devtools/layoutview.css
@@ -6,17 +6,17 @@
   background-image: url(layout-background-grid.png);
 }
 
 .theme-light .theme-body {
   background-image: url(layout-background-grid.png), radial-gradient(circle at 50% 70%, hsl(210,53%,45%) 0%, hsl(210,54%,33%) 100%);
 }
 
 .theme-body {
-  color: hsl(210,100%,85%);
+  color: hsl(210,100%,85%) !important;
   -moz-box-sizing: border-box;
 }
 
 #main {
   border-color: hsla(210,100%,85%,0.7);
   border-style: dotted;
 }
 
--- a/browser/themes/shared/devtools/commandline.inc.css
+++ b/browser/themes/shared/devtools/commandline.inc.css
@@ -27,16 +27,20 @@
   margin: auto 10px;
 }
 
 #developer-toolbar-toolbox-button {
   list-style-image: url("chrome://browser/skin/devtools/toggle-tools.png");
   -moz-image-region: rect(0px, 16px, 16px, 0px);
 }
 
+#developer-toolbar-toolbox-button > label {
+  display: none;
+}
+
 #developer-toolbar-toolbox-button:hover {
   -moz-image-region: rect(0px, 32px, 16px, 16px);
 }
 
 #developer-toolbar-toolbox-button:hover:active {
   -moz-image-region: rect(0px, 48px, 16px, 32px);
 }
 
--- a/browser/themes/shared/devtools/responsivedesign.inc.css
+++ b/browser/themes/shared/devtools/responsivedesign.inc.css
@@ -41,17 +41,17 @@
   transform: translate(12px, -12px);
   background-image: url("chrome://browser/skin/devtools/responsive-vertical-resizer.png");
 }
 
 .devtools-responsiveui-resizebarH {
   width: 24px;
   height: 7px;
   cursor: ns-resize;
-  transform: translate(12px, 12px);
+  transform: translate(-12px, 12px);
   background-image: url("chrome://browser/skin/devtools/responsive-horizontal-resizer.png");
 }
 
 .devtools-responsiveui-resizehandle {
   width: 16px;
   height: 16px;
   cursor: se-resize;
   transform: translate(12px, 12px);
--- a/browser/themes/shared/devtools/webconsole.inc.css
+++ b/browser/themes/shared/devtools/webconsole.inc.css
@@ -224,16 +224,17 @@
 .jsterm-complete-node {
   border: none;
   padding: 0 0 0 16px;
   -moz-appearance: none;
 }
 
 .jsterm-input-node {
   background: -moz-image-rect(url("chrome://browser/skin/devtools/commandline-icon.png"), 0, 32, 16, 16) no-repeat;
+  background-position: 0%;
 }
 
 :-moz-any(.jsterm-input-node,
           .jsterm-complete-node) > .textbox-input-box > .textbox-textarea {
   overflow-x: hidden;
 }
 
 .jsterm-complete-node > .textbox-input-box > .textbox-textarea {
--- a/browser/themes/windows/devtools/layoutview.css
+++ b/browser/themes/windows/devtools/layoutview.css
@@ -6,17 +6,17 @@
   background-image: url(layout-background-grid.png);
 }
 
 .theme-light .theme-body {
   background-image: url(layout-background-grid.png), radial-gradient(circle at 50% 70%, hsl(210,53%,45%) 0%, hsl(210,54%,33%) 100%);
 }
 
 .theme-body {
-  color: hsl(210,100%,85%);
+  color: hsl(210,100%,85%) !important;
   -moz-box-sizing: border-box;
 }
 
 #main {
   border-color: hsla(210,100%,85%,0.7);
   border-style: dotted;
 }
 
--- a/dom/tests/browser/browser_geolocation_privatebrowsing_perwindowpb.js
+++ b/dom/tests/browser/browser_geolocation_privatebrowsing_perwindowpb.js
@@ -1,8 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
 function test() {
   var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
   let baseProvider = "http://mochi.test:8888/browser/dom/tests/browser/network_geolocation.sjs";
   prefs.setCharPref("geo.wifi.uri", baseProvider + "?desired_access_token=fff");
 
   prefs.setBoolPref("geo.prompt.testing", true);
   prefs.setBoolPref("geo.prompt.testing.allow", true);
   var origScanValue = true; // same default in NetworkGeolocationProvider.js.
@@ -14,40 +17,45 @@ function test() {
   const testPageURL = "http://mochi.test:8888/browser/" +
     "dom/tests/browser/browser_geolocation_privatebrowsing_page.html";
   waitForExplicitFinish();
 
   var windowsToClose = [];
   function testOnWindow(aIsPrivate, aCallback) {
     let win = OpenBrowserWindow({private: aIsPrivate});
     let gotLoad = false;
-    let gotActivate = 
-      (Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager).activeWindow == win);
+    let gotActivate = Services.focus.activeWindow == win;
+
+    function maybeRunCallback() {
+      if (gotLoad && gotActivate) {
+        windowsToClose.push(win);
+        executeSoon(function() { aCallback(win); });
+      }
+    }
+
     if (!gotActivate) {
       win.addEventListener("activate", function onActivate() {
         info("got activate");
         win.removeEventListener("activate", onActivate, true);
         gotActivate = true;
-        if (gotLoad) {
-          windowsToClose.push(win);
-          win.BrowserChromeTest.runWhenReady(function() { aCallback(win) });
-        }
+        maybeRunCallback();
       }, true);
     } else {
       info("Was activated");
     }
-    win.addEventListener("load", function onLoad() {
-      info("Got load");
-      win.removeEventListener("load", onLoad, true);
-      gotLoad = true;
-      if (gotActivate) {
-        windowsToClose.push(win);
-        setTimeout(function() { aCallback(win) }, 1000);
+
+    Services.obs.addObserver(function observer(aSubject, aTopic) {
+      if (win == aSubject) {
+        info("Delayed startup finished");
+        Services.obs.removeObserver(observer, aTopic);
+        gotLoad = true;
+        maybeRunCallback();
       }
-    }, true);
+    }, "browser-delayed-startup-finished", false);
+
   }
 
   testOnWindow(false, function(aNormalWindow) {
     aNormalWindow.gBrowser.selectedBrowser.addEventListener("georesult", function load(ev) {
       aNormalWindow.gBrowser.selectedBrowser.removeEventListener("georesult", load, false);
       is(ev.detail, 200, "unexpected access token");
 
       prefs.setCharPref("geo.wifi.uri", baseProvider + "?desired_access_token=ggg");
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1728,51 +1728,64 @@ abstract public class GeckoApp
             mProfile = GeckoProfile.get(this);
         }
         return mProfile;
     }
 
     protected int getSessionRestoreState(Bundle savedInstanceState) {
         final SharedPreferences prefs = GeckoApp.getAppSharedPreferences();
         int restoreMode = RESTORE_NONE;
+        boolean allowCrashRestore = true;
 
         // If the version has changed, the user has done an upgrade, so restore
         // previous tabs.
         final int versionCode = getVersionCode();
         if (prefs.getInt(PREFS_VERSION_CODE, 0) != versionCode) {
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
                     prefs.edit()
                          .putInt(PREFS_VERSION_CODE, versionCode)
                          .commit();
                 }
             });
 
             restoreMode = RESTORE_NORMAL;
-        } else if (savedInstanceState != null ||
-                PreferenceManager.getDefaultSharedPreferences(this).getBoolean(GeckoPreferences.PREFS_RESTORE_SESSION, false)) {
+        } else if (savedInstanceState != null) {
             restoreMode = RESTORE_NORMAL;
+        } else {
+            String restorePref = PreferenceManager.getDefaultSharedPreferences(this)
+                                                  .getString(GeckoPreferences.PREFS_RESTORE_SESSION, "crash");
+            if ("always".equals(restorePref)) {
+                restoreMode = RESTORE_NORMAL;
+            } else {
+                restoreMode = RESTORE_NONE;
+                if ("never".equals(restorePref)) {
+                    allowCrashRestore = false;
+                }
+            }
         }
 
         // We record crashes in the crash reporter. If sessionstore.js
         // exists, but we didn't flag a crash in the crash reporter, we
         // were probably just force killed by the user, so we shouldn't do
         // a restore.
         if (prefs.getBoolean(PREFS_CRASHED, false)) {
             ThreadUtils.postToBackgroundThread(new Runnable() {
                 @Override
                 public void run() {
                     prefs.edit()
                          .putBoolean(PREFS_CRASHED, false)
                          .commit();
                 }
             });
 
-            restoreMode = RESTORE_CRASH;
+            if (allowCrashRestore) {
+                restoreMode = RESTORE_CRASH;
+            }
         }
 
         return restoreMode;
     }
 
     /**
      * Enable Android StrictMode checks (for supported OS versions).
      * http://developer.android.com/reference/android/os/StrictMode.html
--- a/mobile/android/base/GeckoPreferences.java
+++ b/mobile/android/base/GeckoPreferences.java
@@ -72,17 +72,17 @@ public class GeckoPreferences
     private static String PREFS_CRASHREPORTER_ENABLED = "datareporting.crashreporter.submitEnabled";
     private static String PREFS_MENU_CHAR_ENCODING = "browser.menu.showCharacterEncoding";
     private static String PREFS_MP_ENABLED = "privacy.masterpassword.enabled";
     private static String PREFS_UPDATER_AUTODOWNLOAD = "app.update.autodownload";
     private static String PREFS_GEO_REPORTING = "app.geo.reportdata";
     private static String PREFS_HEALTHREPORT_LINK = NON_PREF_PREFIX + "healthreport.link";
     private static String PREFS_DEVTOOLS_REMOTE_ENABLED = "devtools.debugger.remote-enabled";
 
-    public static String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession";
+    public static String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession2";
 
     // These values are chosen to be distinct from other Activity constants.
     private static int REQUEST_CODE_PREF_SCREEN = 5;
     private static int RESULT_CODE_EXIT_SETTINGS = 6;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         // For fragment-capable devices, display the default fragment if no explicit fragment to show.
@@ -316,16 +316,25 @@ public class GeckoPreferences
                         public boolean onPreferenceClick(Preference preference) {
                             // Display toast to remind setting up tcp forwarding.
                             if (((CheckBoxPreference) preference).isChecked()) {
                                 Toast.makeText(thisContext, R.string.devtools_remote_debugging_forward, Toast.LENGTH_SHORT).show();
                             }
                             return true;
                         }
                     });
+                } else if (PREFS_RESTORE_SESSION.equals(key)) {
+                    // Set the summary string to the current entry. The summary
+                    // for other list prefs will be set in the PrefsHelper
+                    // callback, but since this pref doesn't live in Gecko, we
+                    // need to handle it separately.
+                    ListPreference listPref = (ListPreference) pref;
+                    CharSequence selectedEntry = listPref.getEntry();
+                    listPref.setSummary(selectedEntry);
+                    continue;
                 }
 
                 // Some Preference UI elements are not actually preferences,
                 // but they require a key to work correctly. For example,
                 // "Clear private data" requires a key for its state to be
                 // saved when the orientation changes. It uses the
                 // "android.not_a_preference.privacy.clear" key - which doesn't
                 // exist in Gecko - to satisfy this requirement.
@@ -447,57 +456,57 @@ public class GeckoPreferences
         return prefs.getBoolean(name, def);
     }
 
     @Override
     public boolean onPreferenceChange(Preference preference, Object newValue) {
         String prefName = preference.getKey();
         if (PREFS_MP_ENABLED.equals(prefName)) {
             showDialog((Boolean) newValue ? DIALOG_CREATE_MASTER_PASSWORD : DIALOG_REMOVE_MASTER_PASSWORD);
+
+            // We don't want the "use master password" pref to change until the
+            // user has gone through the dialog.
             return false;
         } else if (PREFS_MENU_CHAR_ENCODING.equals(prefName)) {
             setCharEncodingState(((String) newValue).equals("true"));
         } else if (PREFS_ANNOUNCEMENTS_ENABLED.equals(prefName)) {
             // Send a broadcast intent to the product announcements service, either to start or
             // to stop the repeated background checks.
             broadcastAnnouncementsPref(GeckoAppShell.getContext(), ((Boolean) newValue).booleanValue());
         } else if (PREFS_UPDATER_AUTODOWNLOAD.equals(prefName)) {
             org.mozilla.gecko.updater.UpdateServiceHelper.registerForUpdates(GeckoAppShell.getContext(), (String) newValue);
         } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(prefName)) {
             // The healthreport pref only lives in Android, so we do not persist
             // to Gecko, but we do broadcast intent to the health report
             // background uploader service, which will start or stop the
             // repeated background upload attempts.
             broadcastHealthReportUploadPref(GeckoAppShell.getContext(), ((Boolean) newValue).booleanValue());
-            return true;
         } else if (PREFS_GEO_REPORTING.equals(prefName)) {
             // Translate boolean value to int for geo reporting pref.
-            PrefsHelper.setPref(prefName, (Boolean) newValue ? 1 : 0);
-            return true;
-        } else if (PREFS_RESTORE_SESSION.equals(prefName)) {
-            // Do nothing else; the pref will be persisted in the shared prefs,
-            // and it will be read at startup in Java before a session restore.
-            return true;
+            newValue = ((Boolean) newValue) ? 1 : 0;
         }
 
-        if (!TextUtils.isEmpty(prefName)) {
+        // Send Gecko-side pref changes to Gecko
+        if (!TextUtils.isEmpty(prefName) && !prefName.startsWith(NON_PREF_PREFIX)) {
             PrefsHelper.setPref(prefName, newValue);
         }
+
         if (preference instanceof ListPreference) {
             // We need to find the entry for the new value
             int newIndex = ((ListPreference) preference).findIndexOfValue((String) newValue);
             CharSequence newEntry = ((ListPreference) preference).getEntries()[newIndex];
             ((ListPreference) preference).setSummary(newEntry);
         } else if (preference instanceof LinkPreference) {
             setResult(RESULT_CODE_EXIT_SETTINGS);
             finish();
         } else if (preference instanceof FontSizePreference) {
             final FontSizePreference fontSizePref = (FontSizePreference) preference;
             fontSizePref.setSummary(fontSizePref.getSavedFontSizeName());
         }
+
         return true;
     }
 
     private EditText getTextBox(int aHintText) {
         EditText input = new EditText(GeckoAppShell.getContext());
         int inputtype = InputType.TYPE_CLASS_TEXT;
         inputtype |= InputType.TYPE_TEXT_VARIATION_PASSWORD | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
         input.setInputType(inputtype);
--- a/mobile/android/base/background/healthreport/ProfileInformationCache.java
+++ b/mobile/android/base/background/healthreport/ProfileInformationCache.java
@@ -212,16 +212,20 @@ public class ProfileInformationCache imp
   public JSONObject getAddonsJSON() {
     return addons;
   }
 
   public void updateJSONForAddon(String id, String json) throws Exception {
     addons.put(id, new JSONObject(json));
   }
 
+  public void removeAddon(String id) {
+    addons.remove(id);
+  }
+
   /**
    * Will throw if you haven't done a full update at least once.
    */
   public void updateJSONForAddon(String id, JSONObject json) {
     if (addons == null) {
       throw new IllegalStateException("Cannot incrementally update add-ons without first initializing.");
     }
     try {
--- a/mobile/android/base/health/BrowserHealthRecorder.java
+++ b/mobile/android/base/health/BrowserHealthRecorder.java
@@ -58,16 +58,17 @@ import java.util.concurrent.atomic.Atomi
  *
  * Shut it down when you're done being a browser: {@link #close()}.
  */
 public class BrowserHealthRecorder implements GeckoEventListener {
     private static final String LOG_TAG = "GeckoHealthRec";
     private static final String PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
     private static final String EVENT_ADDONS_ALL = "Addons:All";
     private static final String EVENT_ADDONS_CHANGE = "Addons:Change";
+    private static final String EVENT_ADDONS_UNINSTALLING = "Addons:Uninstalling";
     private static final String EVENT_PREF_CHANGE = "Pref:Change";
  
     // This is raised from Gecko. It avoids browser.js having to know about the
     // location that invoked it (the URL bar).
     public static final String EVENT_KEYWORD_SEARCH = "Search:Keyword";
 
     // This is raised from Java. We include the location in the message.
     public static final String EVENT_SEARCH = "Search:Event";
@@ -133,16 +134,30 @@ public class BrowserHealthRecorder imple
             long wallStartTime = prefs.getLong(PREFS_SESSION_START, 0L);
             long realStartTime = 0L;
             Log.d(LOG_TAG, "Building SessionInformation from prefs: " +
                            wallStartTime + ", " + realStartTime + ", " +
                            wasStopped + ", " + wasOOM);
             return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped);
         }
 
+        /**
+         * Initialize a new SessionInformation instance to 'split' the current
+         * session.
+         */
+        public static SessionInformation forRuntimeTransition() {
+            final boolean wasOOM = false;
+            final boolean wasStopped = true;
+            final long wallStartTime = System.currentTimeMillis();
+            final long realStartTime = android.os.SystemClock.elapsedRealtime();
+            Log.v(LOG_TAG, "Recording runtime session transition: " +
+                           wallStartTime + ", " + realStartTime);
+            return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped);
+        }
+
         public boolean wasKilled() {
             return wasOOM || !wasStopped;
         }
 
         /**
          * Record the beginning of this session to SharedPreferences by
          * recording our start time. If a session was already recorded, it is
          * overwritten (there can only be one running session at a time). Does
@@ -280,16 +295,17 @@ public class BrowserHealthRecorder imple
             this.client.release();
             this.client = null;
         }
     }
 
     private void unregisterEventListeners() {
         this.dispatcher.unregisterEventListener(EVENT_ADDONS_ALL, this);
         this.dispatcher.unregisterEventListener(EVENT_ADDONS_CHANGE, this);
+        this.dispatcher.unregisterEventListener(EVENT_ADDONS_UNINSTALLING, this);
         this.dispatcher.unregisterEventListener(EVENT_PREF_CHANGE, this);
         this.dispatcher.unregisterEventListener(EVENT_KEYWORD_SEARCH, this);
         this.dispatcher.unregisterEventListener(EVENT_SEARCH, this);
     }
 
     public void onBlocklistPrefChanged(boolean to) {
         this.profileCache.beginInitialization();
         this.profileCache.setBlocklistEnabled(to);
@@ -304,36 +320,65 @@ public class BrowserHealthRecorder imple
         this.profileCache.beginInitialization();
         try {
             this.profileCache.updateJSONForAddon(id, json);
         } catch (IllegalStateException e) {
             Log.w(LOG_TAG, "Attempted to update add-on cache prior to full init.", e);
         }
     }
 
+    public void onAddonUninstalling(String id) {
+        this.profileCache.beginInitialization();
+        try {
+            this.profileCache.removeAddon(id);
+        } catch (IllegalStateException e) {
+            Log.w(LOG_TAG, "Attempted to update add-on cache prior to full init.", e);
+        }
+    }
+
     /**
-     * Call this when a material change has occurred in the running environment,
-     * such that a new environment should be computed and prepared for use in
-     * future events.
+     * Call this when a material change might have occurred in the running
+     * environment, such that a new environment should be computed and prepared
+     * for use in future events.
      *
      * Invoke this method after calls that mutate the environment, such as
      * {@link #onBlocklistPrefChanged(boolean)}.
      *
-     * TODO: record a session transition with the new environment.
+     * If this change resulted in a transition between two environments, {@link
+     * #onEnvironmentTransition(int, int)} will be invoked on the background
+     * thread.
      */
     public synchronized void onEnvironmentChanged() {
+        final int previousEnv = this.env;
         this.env = -1;
         try {
             profileCache.completeInitialization();
         } catch (java.io.IOException e) {
             Log.e(LOG_TAG, "Error completing profile cache initialization.", e);
             this.state = State.INITIALIZATION_FAILED;
             return;
         }
-        ensureEnvironment();
+
+        final int updatedEnv = ensureEnvironment();
+
+        if (updatedEnv == -1 ||
+            updatedEnv == previousEnv) {
+            Log.v(LOG_TAG, "Environment didn't change.");
+            return;
+        }
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    onEnvironmentTransition(previousEnv, updatedEnv);
+                } catch (Exception e) {
+                    Log.w(LOG_TAG, "Could not record environment transition.", e);
+                }
+            }
+        });
     }
 
     protected synchronized int ensureEnvironment() {
         if (!(state == State.INITIALIZING ||
               state == State.INITIALIZED)) {
             throw new IllegalStateException("Not initialized.");
         }
 
@@ -488,16 +533,17 @@ public class BrowserHealthRecorder imple
                     } catch (Exception e) {
                         Log.e(LOG_TAG, "Failed to init storage.", e);
                         state = State.INITIALIZATION_FAILED;
                         return;
                     }
 
                     try {
                         // Listen for add-ons and prefs changes.
+                        dispatcher.registerEventListener(EVENT_ADDONS_UNINSTALLING, self);
                         dispatcher.registerEventListener(EVENT_ADDONS_CHANGE, self);
                         dispatcher.registerEventListener(EVENT_PREF_CHANGE, self);
 
                         // Initialize each provider here.
                         initializeSessionsProvider();
                         initializeSearchProvider();
 
                         Log.d(LOG_TAG, "Ensuring environment.");
@@ -562,16 +608,37 @@ public class BrowserHealthRecorder imple
         PrefsHelper.getPrefs(new String[] {
                                  AppConstants.TELEMETRY_PREF_NAME,
                                  PREF_BLOCKLIST_ENABLED
                              },
                              handler);
         Log.d(LOG_TAG, "Requested prefs.");
     }
 
+    /**
+     * Invoked in the background whenever the environment transitions between
+     * two valid values.
+     */
+    protected void onEnvironmentTransition(int prev, int env) {
+        if (this.state != State.INITIALIZED) {
+            Log.d(LOG_TAG, "Not initialized: not recording env transition (" + prev + " => " + env + ").");
+            return;
+        }
+
+        final SharedPreferences prefs = GeckoApp.getAppSharedPreferences();
+        final SharedPreferences.Editor editor = prefs.edit();
+
+        recordSessionEnd("E", editor, prev);
+
+        final SessionInformation newSession = SessionInformation.forRuntimeTransition();
+        setCurrentSession(newSession);
+        newSession.recordBegin(editor);
+        editor.commit();
+    }
+
     @Override
     public void handleMessage(String event, JSONObject message) {
         try {
             if (EVENT_ADDONS_ALL.equals(event)) {
                 Log.d(LOG_TAG, "Got all add-ons.");
                 try {
                     JSONObject addons = message.getJSONObject("json");
                     Log.i(LOG_TAG, "Persisting " + addons.length() + " add-ons.");
@@ -586,22 +653,29 @@ public class BrowserHealthRecorder imple
                 if (state == State.INITIALIZING) {
                     initializeStorage();
                 } else {
                     this.onEnvironmentChanged();
                 }
 
                 return;
             }
+
+            if (EVENT_ADDONS_UNINSTALLING.equals(event)) {
+                this.onAddonUninstalling(message.getString("id"));
+                this.onEnvironmentChanged();
+                return;
+            }
+
             if (EVENT_ADDONS_CHANGE.equals(event)) {
-                Log.d(LOG_TAG, "Add-on changed: " + message.getString("id"));
                 this.onAddonChanged(message.getString("id"), message.getJSONObject("json"));
                 this.onEnvironmentChanged();
                 return;
             }
+
             if (EVENT_PREF_CHANGE.equals(event)) {
                 final String pref = message.getString("pref");
                 Log.d(LOG_TAG, "Pref changed: " + pref);
                 handlePrefValue(pref, message.getBoolean("value"));
                 this.onEnvironmentChanged();
                 return;
             }
 
@@ -866,24 +940,31 @@ public class BrowserHealthRecorder imple
                     return out;
                 }
         });
     }
 
     /**
      * Logic shared between crashed and normal sessions.
      */
-    private void recordSessionEntry(String field, SessionInformation session, JSONObject value) {
+    private void recordSessionEntry(String field, SessionInformation session, final int environment, JSONObject value) {
+        final HealthReportDatabaseStorage storage = this.storage;
+        if (storage == null) {
+            Log.d(LOG_TAG, "No storage: not recording session entry. Shutting down?");
+            return;
+        }
+
         try {
             final int sessionField = storage.getField(MEASUREMENT_NAME_SESSIONS,
                                                       MEASUREMENT_VERSION_SESSIONS,
                                                       field)
                                             .getID();
             final int day = storage.getDay(session.wallStartTime);
-            storage.recordDailyDiscrete(env, day, sessionField, value);
+            storage.recordDailyDiscrete(environment, day, sessionField, value);
+            Log.v(LOG_TAG, "Recorded session entry for env " + environment + ", current is " + env);
         } catch (Exception e) {
             Log.w(LOG_TAG, "Unable to record session completion.", e);
         }
     }
 
     public void checkForOrphanSessions() {
         if (!this.orphanChecked.compareAndSet(false, true)) {
             Log.w(LOG_TAG, "Attempting to check for orphan sessions more than once.");
@@ -900,28 +981,36 @@ public class BrowserHealthRecorder imple
 
         if (state != State.INITIALIZED) {
             // Something has gone awry.
             Log.e(LOG_TAG, "Attempted to record bad session end without initialized recorder.");
             return;
         }
 
         try {
-            recordSessionEntry("abnormal", this.previousSession, this.previousSession.getCrashedJSON());
+            recordSessionEntry("abnormal", this.previousSession, this.env,
+                               this.previousSession.getCrashedJSON());
         } catch (Exception e) {
             Log.w(LOG_TAG, "Unable to generate session JSON.", e);
 
             // Future: record this exception in FHR's own error submitter.
         }
     }
 
+    public void recordSessionEnd(String reason, SharedPreferences.Editor editor) {
+        recordSessionEnd(reason, editor, env);
+    }
+
     /**
      * Record that the current session ended. Does not commit the provided editor.
+     *
+     * @param environment An environment ID. This allows callers to record the
+     *                    end of a session due to an observed environment change.
      */
-    public void recordSessionEnd(String reason, SharedPreferences.Editor editor) {
+    public void recordSessionEnd(String reason, SharedPreferences.Editor editor, final int environment) {
         Log.d(LOG_TAG, "Recording session end: " + reason);
         if (state != State.INITIALIZED) {
             // Something has gone awry.
             Log.e(LOG_TAG, "Attempted to record session end without initialized recorder.");
             return;
         }
 
         final SessionInformation session = this.session;
@@ -935,17 +1024,17 @@ public class BrowserHealthRecorder imple
         if (session.wallStartTime <= 0) {
             Log.e(LOG_TAG, "Session start " + session.wallStartTime + " isn't valid! Can't record end.");
             return;
         }
 
         long realEndTime = android.os.SystemClock.elapsedRealtime();
         try {
             JSONObject json = session.getCompletionJSON(reason, realEndTime);
-            recordSessionEntry("normal", session, json);
+            recordSessionEntry("normal", session, environment, json);
         } catch (JSONException e) {
             Log.w(LOG_TAG, "Unable to generate session JSON.", e);
 
             // Continue so we don't hit it next time.
             // Future: record this exception in FHR's own error submitter.
         }
 
         // Track the end of this session in shared prefs, so it doesn't get
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -100,17 +100,20 @@
 <!ENTITY pref_char_encoding_off "Don\'t show menu">
 <!ENTITY pref_clear_private_data "Clear private data">
 <!ENTITY pref_plugins "Plugins">
 <!ENTITY pref_plugins_enabled "Enabled">
 <!ENTITY pref_plugins_tap_to_play "Tap to play">
 <!ENTITY pref_plugins_disabled "Disabled">
 <!ENTITY pref_text_size "Text size">
 <!ENTITY pref_reflow_on_zoom4 "Text reflow">
-<!ENTITY pref_restore_session "Always restore tabs">
+<!ENTITY pref_restore "Tabs">
+<!ENTITY pref_restore_always "Always restore">
+<!ENTITY pref_restore_crash "Restore after a crash">
+<!ENTITY pref_restore_never "Never restore">
 <!ENTITY pref_font_size_tiny "Tiny">
 <!ENTITY pref_font_size_small "Small">
 <!ENTITY pref_font_size_medium "Medium">
 <!ENTITY pref_font_size_large "Large">
 <!ENTITY pref_font_size_xlarge "Extra Large">
 <!ENTITY pref_font_size_set "Set">
 <!-- Localization note (pref_font_size_adjust_char): A button with a small version of this character
 (or combination of characters) is used to decrease the preview font size; a larger version of the
--- a/mobile/android/base/resources/values/arrays.xml
+++ b/mobile/android/base/resources/values/arrays.xml
@@ -94,16 +94,26 @@
         <item>private.data.downloadFiles</item>
         <item>private.data.formdata</item>
         <item>private.data.cookies_sessions</item>
         <item>private.data.passwords</item>
         <item>private.data.cache</item>
         <item>private.data.offlineApps</item>
         <item>private.data.siteSettings</item>
     </string-array>
+    <string-array name="pref_restore_entries">
+        <item>@string/pref_restore_always</item>
+        <item>@string/pref_restore_crash</item>
+        <item>@string/pref_restore_never</item>
+    </string-array>
+    <string-array name="pref_restore_values">
+        <item>always</item>
+        <item>crash</item>
+        <item>never</item>
+    </string-array>
     <string-array name="pref_update_autodownload_entries">
         <item>@string/pref_update_autodownload_enabled</item>
         <item>@string/pref_update_autodownload_wifi</item>
         <item>@string/pref_update_autodownload_disabled</item>
     </string-array>
     <string-array name="pref_update_autodownload_values">
         <item>enabled</item>
         <item>wifi</item>
--- a/mobile/android/base/resources/xml-v11/preferences_customize.xml
+++ b/mobile/android/base/resources/xml-v11/preferences_customize.xml
@@ -20,20 +20,22 @@
                   gecko:entries="@array/pref_import_android_entries"
                   gecko:entryKeys="@array/pref_import_android_keys"
                   gecko:initialValues="@array/pref_import_android_values"
                   android:title="@string/pref_import_android"
                   android:positiveButtonText="@string/bookmarkhistory_button_import"
                   android:negativeButtonText="@string/button_cancel"
                   android:persistent="false" />
 
-    <CheckBoxPreference android:key="android.not_a_preference.restoreSession"
-                        android:title="@string/pref_restore_session"
-                        android:defaultValue="false"
-                        android:persistent="true" />
+    <ListPreference android:key="android.not_a_preference.restoreSession2"
+                    android:title="@string/pref_restore"
+                    android:defaultValue="crash"
+                    android:entries="@array/pref_restore_entries"
+                    android:entryValues="@array/pref_restore_values"
+                    android:persistent="true" />
 
    <ListPreference android:key="app.update.autodownload"
                    android:title="@string/pref_update_autodownload"
                    android:entries="@array/pref_update_autodownload_entries"
                    android:entryValues="@array/pref_update_autodownload_values"
                    android:persistent="false" />
 
 </PreferenceScreen>
--- a/mobile/android/base/resources/xml-v11/preferences_customize_tablet.xml
+++ b/mobile/android/base/resources/xml-v11/preferences_customize_tablet.xml
@@ -26,20 +26,22 @@
             gecko:entries="@array/pref_import_android_entries"
             gecko:entryKeys="@array/pref_import_android_keys"
             gecko:initialValues="@array/pref_import_android_values"
             android:title="@string/pref_import_android"
             android:positiveButtonText="@string/bookmarkhistory_button_import"
             android:negativeButtonText="@string/button_cancel"
             android:persistent="false" />
 
-    <CheckBoxPreference android:key="android.not_a_preference.restoreSession"
-                        android:title="@string/pref_restore_session"
-                        android:defaultValue="false"
-                        android:persistent="true" />
+    <ListPreference android:key="android.not_a_preference.restoreSession2"
+                    android:title="@string/pref_restore"
+                    android:defaultValue="crash"
+                    android:entries="@array/pref_restore_entries"
+                    android:entryValues="@array/pref_restore_values"
+                    android:persistent="true" />
 
     <ListPreference android:key="app.update.autodownload"
                     android:title="@string/pref_update_autodownload"
                     android:entries="@array/pref_update_autodownload_entries"
                     android:entryValues="@array/pref_update_autodownload_values"
                     android:persistent="false" />
 
 </PreferenceScreen>
--- a/mobile/android/base/resources/xml/preferences_customize.xml.in
+++ b/mobile/android/base/resources/xml/preferences_customize.xml.in
@@ -22,20 +22,22 @@
                   gecko:entries="@array/pref_import_android_entries"
                   gecko:entryKeys="@array/pref_import_android_keys"
                   gecko:initialValues="@array/pref_import_android_values"
                   android:title="@string/pref_import_android"
                   android:positiveButtonText="@string/bookmarkhistory_button_import"
                   android:negativeButtonText="@string/button_cancel"
                   android:persistent="false" />
 
-    <CheckBoxPreference android:key="android.not_a_preference.restoreSession"
-                        android:title="@string/pref_restore_session"
-                        android:defaultValue="false"
-                        android:persistent="true" />
+    <ListPreference android:key="android.not_a_preference.restoreSession2"
+                    android:title="@string/pref_restore"
+                    android:defaultValue="crash"
+                    android:entries="@array/pref_restore_entries"
+                    android:entryValues="@array/pref_restore_values"
+                    android:persistent="true" />
 
    <ListPreference android:key="app.update.autodownload"
                    android:title="@string/pref_update_autodownload"
                    android:entries="@array/pref_update_autodownload_entries"
                    android:entryValues="@array/pref_update_autodownload_values"
                    android:persistent="false" />
 
 </PreferenceScreen>
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -120,17 +120,20 @@
   <string name="pref_font_size_small">&pref_font_size_small;</string>
   <string name="pref_font_size_medium">&pref_font_size_medium;</string>
   <string name="pref_font_size_large">&pref_font_size_large;</string>
   <string name="pref_font_size_xlarge">&pref_font_size_xlarge;</string>
   <string name="pref_font_size_set">&pref_font_size_set;</string>
   <string name="pref_font_size_adjust_char">&pref_font_size_adjust_char;</string>
   <string name="pref_font_size_preview_text">&pref_font_size_preview_text;</string>
   <string name="pref_reflow_on_zoom">&pref_reflow_on_zoom4;</string>
-  <string name="pref_restore_session">&pref_restore_session;</string>
+  <string name="pref_restore">&pref_restore;</string>
+  <string name="pref_restore_always">&pref_restore_always;</string>
+  <string name="pref_restore_crash">&pref_restore_crash;</string>
+  <string name="pref_restore_never">&pref_restore_never;</string>
   <string name="pref_show_product_announcements">&pref_show_product_announcements;</string>
   <string name="pref_sync">&pref_sync;</string>
   <string name="pref_search_suggestions">&pref_search_suggestions;</string>
   <string name="pref_private_data_history">&pref_private_data_history;</string>
   <string name="pref_private_data_formdata">&pref_private_data_formdata;</string>
   <string name="pref_private_data_cookies2">&pref_private_data_cookies2;</string>
   <string name="pref_private_data_passwords">&pref_private_data_passwords;</string>
   <string name="pref_private_data_cache">&pref_private_data_cache;</string>
--- a/mobile/android/base/tests/testSettingsMenuItems.java.in
+++ b/mobile/android/base/tests/testSettingsMenuItems.java.in
@@ -21,17 +21,17 @@ public class testSettingsMenuItems exten
     //     itemTitle { defaultValue [options] }
     //
     // where defaultValue is optional, and there can be multiple options.
     //
     // This test assumes menu items are in order (scrolling down for off-screen items).
     String[][] OPTIONS_CUSTOMIZE = {
          { "Search settings", "", "Show search suggestions", "Installed search engines" },
          { "Import from Android", "", "Bookmarks", "History", "Import" },
-         { "Always restore tabs" },
+         { "Tabs", "Restore after a crash", "Always restore", "Restore after a crash", "Never restore" },
          { "Automatic updates", "Only over Wi-Fi", "Enabled", "Only over Wi-Fi", "Disabled" },
      };
 
     String[][] OPTIONS_DISPLAY = {
         { "Text size" },
         { "Title bar", "Show page title", "Show page title", "Show page address" },
         { "Advanced" },
         { "Text reflow" },
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -5350,40 +5350,43 @@ let HealthReportStatusListener = {
 
     for (let field of this.COPY_FIELDS) {
       o[field] = aAddon[field];
     }
 
     return o;
   },
 
-  notifyJava: function (aAddon, aNeedsRestart) {
+  notifyJava: function (aAddon, aNeedsRestart, aAction="Addons:Change") {
     let json = this.jsonForAddon(aAddon);
     if (this._shouldIgnore(aAddon)) {
       json.ignore = true;
     }
-    sendMessageToJava({ type: "Addons:Change", id: aAddon.id, json: json });
+    sendMessageToJava({ type: aAction, id: aAddon.id, json: json });
   },
 
   // Add-on listeners.
   onEnabling: function (aAddon, aNeedsRestart) {
     this.notifyJava(aAddon, aNeedsRestart);
   },
   onDisabling: function (aAddon, aNeedsRestart) {
     this.notifyJava(aAddon, aNeedsRestart);
   },
   onInstalling: function (aAddon, aNeedsRestart) {
     this.notifyJava(aAddon, aNeedsRestart);
   },
   onUninstalling: function (aAddon, aNeedsRestart) {
-    this.notifyJava(aAddon, aNeedsRestart);
+    this.notifyJava(aAddon, aNeedsRestart, "Addons:Uninstalling");
   },
   onPropertyChanged: function (aAddon, aProperties) {
     this.notifyJava(aAddon);
   },
+  onOperationCancelled: function (aAddon) {
+    this.notifyJava(aAddon);
+  },
 
   sendAllAddonsToJava: function () {
     AddonManager.getAllAddons(function (aAddons) {
         let json = {};
         if (aAddons) {
           for (let i = 0; i < aAddons.length; ++i) {
             let addon = aAddons[i];
             try {
--- a/testing/mochitest/browser-test.js
+++ b/testing/mochitest/browser-test.js
@@ -8,21 +8,27 @@ if (Cc === undefined) {
   var Cu = Components.utils;
 }
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
   "resource://gre/modules/Services.jsm");
 
-window.addEventListener("load", testOnLoad, false);
+window.addEventListener("load", function testOnLoad() {
+  window.removeEventListener("load", testOnLoad);
+
+  window.addEventListener("MozAfterPaint", function testOnMozAfterPaint() {
+    window.removeEventListener("MozAfterPaint", testOnMozAfterPaint);
 
-function testOnLoad() {
-  window.removeEventListener("load", testOnLoad, false);
+    setTimeout(testInit, 0);
+  });
+});
 
+function testInit() {
   gConfig = readConfig();
   if (gConfig.testRoot == "browser" ||
       gConfig.testRoot == "metro" ||
       gConfig.testRoot == "webapprtChrome") {
     // Make sure to launch the test harness for the first opened window only
     var prefs = Services.prefs;
     if (prefs.prefHasUserValue("testing.browserTestHarness.running"))
       return;
@@ -88,25 +94,16 @@ Tester.prototype = {
   get currentTest() {
     return this.tests[this.currentTestIndex];
   },
   get done() {
     return this.currentTestIndex == this.tests.length - 1;
   },
 
   start: function Tester_start() {
-    // Check whether this window is ready to run tests.
-    if (window.BrowserChromeTest) {
-      BrowserChromeTest.runWhenReady(this.actuallyStart.bind(this));
-      return;
-    }
-    this.actuallyStart();
-  },
-
-  actuallyStart: function Tester_actuallyStart() {
     //if testOnLoad was not called, then gConfig is not defined
     if (!gConfig)
       gConfig = readConfig();
 
     if (gConfig.runUntilFailure)
       this.runUntilFailure = true;
 
     if (gConfig.repeat)
--- a/toolkit/components/jsdownloads/src/DownloadCore.jsm
+++ b/toolkit/components/jsdownloads/src/DownloadCore.jsm
@@ -1195,33 +1195,38 @@ DownloadCopySaver.prototype = {
           if (channel instanceof Ci.nsIHttpChannel &&
               download.source.referrer) {
             channel.referrer = NetUtil.newURI(download.source.referrer);
           }
 
           // If we have data that we can use to resume the download from where
           // it stopped, try to use it.
           let resumeAttempted = false;
+          let resumeFromBytes = 0;
           if (channel instanceof Ci.nsIResumableChannel && this.entityID &&
               partFilePath && keepPartialData) {
             try {
               let stat = yield OS.File.stat(partFilePath);
               channel.resumeAt(stat.size, this.entityID);
               resumeAttempted = true;
+              resumeFromBytes = stat.size;
             } catch (ex if ex instanceof OS.File.Error &&
                            ex.becauseNoSuchFile) { }
           }
 
           channel.notificationCallbacks = {
             QueryInterface: XPCOMUtils.generateQI([Ci.nsIInterfaceRequestor]),
             getInterface: XPCOMUtils.generateQI([Ci.nsIProgressEventSink]),
             onProgress: function DCSE_onProgress(aRequest, aContext, aProgress,
                                                  aProgressMax)
             {
-              aSetProgressBytesFn(aProgress, aProgressMax, aProgress > 0 &&
+              let currentBytes = resumeFromBytes + aProgress;
+              let totalBytes = aProgressMax == -1 ? -1 : (resumeFromBytes +
+                                                          aProgressMax);
+              aSetProgressBytesFn(currentBytes, totalBytes, aProgress > 0 &&
                                   partFilePath && keepPartialData);
             },
             onStatus: function () { },
           };
 
           // Open the channel, directing output to the background file saver.
           backgroundFileSaver.QueryInterface(Ci.nsIStreamListener);
           channel.asyncOpen({
--- a/toolkit/components/jsdownloads/src/DownloadLegacy.js
+++ b/toolkit/components/jsdownloads/src/DownloadLegacy.js
@@ -217,16 +217,18 @@ DownloadLegacyTransfer.prototype = {
         list = Downloads.getPrivateDownloadList();
       } else {
         list = Downloads.getPublicDownloadList();
       }
       return list.then(function (aList) aList.add(aDownload));
     }.bind(this)).then(null, Cu.reportError);
   },
 
+  setSha256Hash: function () { },
+
   //////////////////////////////////////////////////////////////////////////////
   //// Private methods and properties
 
   /**
    * This deferred object contains a promise that is resolved with the Download
    * object associated with this nsITransfer instance, when it is available.
    */
   _deferDownload: null,
--- a/toolkit/components/jsdownloads/test/unit/common_test_Download.js
+++ b/toolkit/components/jsdownloads/test/unit/common_test_Download.js
@@ -639,19 +639,40 @@ add_task(function test_cancel_midway_res
 
   // The target file should not exist, but we should have kept the partial data.
   do_check_false(yield OS.File.exists(download.target.path));
   yield promiseVerifyContents(download.target.partFilePath, TEST_DATA_SHORT);
 
   // Verify that the server sent the response from the start.
   do_check_eq(gMostRecentFirstBytePos, 0);
 
-  // The second time, we'll request and obtain the second part of the response.
+  // The second time, we'll request and obtain the second part of the response,
+  // but we still stop when half of the remaining progress is reached.
+  let deferMidway = Promise.defer();
+  download.onchange = function () {
+    if (!download.stopped && !download.canceled &&
+        download.currentBytes == Math.floor(TEST_DATA_SHORT.length * 3 / 2)) {
+      download.onchange = null;
+      deferMidway.resolve();
+    }
+  };
+
+  mustInterruptResponses();
+  let promiseAttempt = download.start();
+
+  // Continue when the number of bytes we received is correct, then check that
+  // progress is at about 75 percent.  The exact figure may vary because of
+  // rounding issues, since the total number of bytes in the response might not
+  // be a multiple of four.
+  yield deferMidway.promise;
+  do_check_true(download.progress > 72 && download.progress < 78);
+
+  // Now we allow the download to finish.
   continueResponses();
-  yield download.start();
+  yield promiseAttempt;
 
   // Check that the server now sent the second part only.
   do_check_eq(gMostRecentFirstBytePos, TEST_DATA_SHORT.length);
 
   // The target file should now have been created, and the ".part" file deleted.
   yield promiseVerifyContents(download.target.path,
                               TEST_DATA_SHORT + TEST_DATA_SHORT);
   do_check_false(yield OS.File.exists(download.target.partFilePath));
--- a/toolkit/components/social/FrameWorker.jsm
+++ b/toolkit/components/social/FrameWorker.jsm
@@ -136,26 +136,26 @@ FrameWorker.prototype = {
 
     // copy the window apis onto the sandbox namespace only functions or
     // objects that are naturally a part of an iframe, I'm assuming they are
     // safe to import this way
     let workerAPI = ['WebSocket', 'atob', 'btoa',
                      'clearInterval', 'clearTimeout', 'dump',
                      'setInterval', 'setTimeout', 'XMLHttpRequest',
                      'FileReader', 'Blob', 'EventSource', 'indexedDB',
-                     'location'];
+                     'location', 'Worker'];
 
     // Only expose localStorage if the caller opted-in
     if (this.exposeLocalStorage) {
       workerAPI.push('localStorage');
     }
 
-    // Bug 798660 - XHR and WebSocket have issues in a sandbox and need
+    // Bug 798660 - XHR, WebSocket and Worker have issues in a sandbox and need
     // to be unwrapped to work
-    let needsWaive = ['XMLHttpRequest', 'WebSocket'];
+    let needsWaive = ['XMLHttpRequest', 'WebSocket', 'Worker'];
     // Methods need to be bound with the proper |this|.
     let needsBind = ['atob', 'btoa', 'dump', 'setInterval', 'clearInterval',
                      'setTimeout', 'clearTimeout'];
     workerAPI.forEach(function(fn) {
       try {
         if (needsWaive.indexOf(fn) != -1)
           sandbox[fn] = XPCNativeWrapper.unwrap(workerWindow)[fn];
         else if (needsBind.indexOf(fn) != -1)
--- a/toolkit/components/social/test/browser/browser_frameworker.js
+++ b/toolkit/components/social/test/browser/browser_frameworker.js
@@ -1,16 +1,16 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 function makeWorkerUrl(runner) {
   let prefix =  "http://example.com/browser/toolkit/components/social/test/browser/echo.sjs?";
   if (typeof runner == "function") {
-    runner = "let run=" + runner.toSource() + ";run();";
+    runner = "var run=" + runner.toSource() + ";run();";
   }
   return prefix + encodeURI(runner);
 }
 
 var getFrameWorkerHandle;
 function test() {
   waitForExplicitFinish();
 
@@ -689,9 +689,40 @@ let tests = {
       if (m.topic == "social.indexeddb-result") {
         is(m.data.result, "ok", "created indexeddb");
         worker.terminate();
         cbnext();
       }
     }
     worker.port.postMessage({topic: "test-indexeddb-create"})
   },
+
+  testSubworker: function(cbnext) {
+    // the main "frameworker"...
+    let mainworker = function() {
+      onconnect = function(e) {
+        let port = e.ports[0];
+        port.onmessage = function(e) {
+          if (e.data.topic == "go") {
+            let suburl = e.data.data;
+            let worker = new Worker(suburl);
+            worker.onmessage = function(sube) {
+              port.postMessage({topic: "sub-message", data: sube.data});
+            }
+          }
+        }
+      }
+    }
+
+    // The "subworker" that is actually a real, bona-fide worker.
+    let subworker = function() {
+      postMessage("hello");
+    }
+    let worker = getFrameWorkerHandle(makeWorkerUrl(mainworker), undefined, "testSubWorker");
+    worker.port.onmessage = function(e) {
+      if (e.data.topic == "sub-message" && e.data.data == "hello") {
+        worker.terminate();
+        cbnext();
+      }
+    }
+    worker.port.postMessage({topic: "go", data: makeWorkerUrl(subworker)});
+  }
 }
--- a/widget/cocoa/nsMacWebAppUtils.mm
+++ b/widget/cocoa/nsMacWebAppUtils.mm
@@ -18,43 +18,40 @@
 
 NS_IMPL_ISUPPORTS1(nsMacWebAppUtils, nsIMacWebAppUtils)
 
 NS_IMETHODIMP nsMacWebAppUtils::PathForAppWithIdentifier(const nsAString& bundleIdentifier, nsAString& outPath) {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
   outPath.Truncate();
 
-  NSAutoreleasePool* ap = [[NSAutoreleasePool alloc] init];
+  nsAutoreleasePool localPool;
 
   //note that the result of this expression might be nil, meaning no matching app was found. 
   NSString* temp = [[NSWorkspace sharedWorkspace] absolutePathForAppBundleWithIdentifier:
                         [NSString stringWithCharacters:((nsString)bundleIdentifier).get() length:((nsString)bundleIdentifier).Length()]];
 
   if (temp) {
     // Copy out the resultant absolute path into outPath if non-nil.
     nsCocoaUtils::GetStringForNSString(temp, outPath);
   }
 
-  [ap release];
   return NS_OK;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
 
 NS_IMETHODIMP nsMacWebAppUtils::LaunchAppWithIdentifier(const nsAString& bundleIdentifier) {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSRESULT;
 
-  NSAutoreleasePool* ap = [[NSAutoreleasePool alloc] init];
+  nsAutoreleasePool localPool;
 
   // Note this might return false, meaning the app wasnt launched for some reason. 
   BOOL success = [[NSWorkspace sharedWorkspace] launchAppWithBundleIdentifier:
                         [NSString stringWithCharacters:((nsString)bundleIdentifier).get() length:((nsString)bundleIdentifier).Length()]
                         options: (NSWorkspaceLaunchOptions)0
                         additionalEventParamDescriptor: nil
                         launchIdentifier: NULL];
 
-
-  [ap release];
-  return NS_OK;
+  return success ? NS_OK : NS_ERROR_FAILURE;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }