Merge fx-team to m-c. a=merge
authorRyan VanderMeulen <ryanvm@gmail.com>
Thu, 03 Jul 2014 23:24:35 -0400
changeset 192328 e8df6826a571509fdd08d5538f8158671c4fd58b
parent 192306 02363e5245811abedbff3fdf522a26c37639ff92 (current diff)
parent 192327 00309d102efc5647b90c70ff7cca8dc99c46882d (diff)
child 192329 ae49277dc7ea2e243945c0a002c862cb2bd47252
child 192386 4fd9df60e73c999c4ce72b1e8e0ca0c7440447b7
child 192394 215d323a555b73b0899b20ee162315a0f558b3c4
push id45799
push userryanvm@gmail.com
push dateFri, 04 Jul 2014 03:25:44 +0000
treeherdermozilla-inbound@ae49277dc7ea [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone33.0a1
first release with
nightly linux32
e8df6826a571 / 33.0a1 / 20140704030208 / files
nightly linux64
e8df6826a571 / 33.0a1 / 20140704030208 / files
nightly mac
e8df6826a571 / 33.0a1 / 20140704030208 / files
nightly win32
e8df6826a571 / 33.0a1 / 20140704030208 / files
nightly win64
e8df6826a571 / 33.0a1 / 20140704030208 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge fx-team to m-c. a=merge
--- a/browser/base/content/browser-social.js
+++ b/browser/base/content/browser-social.js
@@ -546,24 +546,25 @@ SocialShare = {
       return false;
 
     if (!aURI || !(aURI.schemeIs('http') || aURI.schemeIs('https')))
       return false;
     return true;
   },
 
   update: function() {
-    let shareButton = this.shareButton;
-    if (!shareButton)
+    let widget = CustomizableUI.getWidget("social-share-button");
+    if (!widget)
       return;
-    // if we got here, the button is in the window somewhere, update it's hidden
-    // state based on available providers.
-    shareButton.hidden = !SocialUI.enabled ||
-                         [p for (p of Social.providers) if (p.shareURL)].length == 0;
-    let disabled = shareButton.hidden || !this.canSharePage(gBrowser.currentURI);
+    let shareButton = widget.forWindow(window).node;
+    // hidden state is based on available share providers and location of
+    // button. It's always visible and disabled in the customization palette.
+    shareButton.hidden = !SocialUI.enabled || (widget.areaType &&
+                         [p for (p of Social.providers) if (p.shareURL)].length == 0);
+    let disabled = !widget.areaType || shareButton.hidden || !this.canSharePage(gBrowser.currentURI);
 
     // 1. update the relevent command's disabled state so the keyboard
     // shortcut only works when available.
     // 2. If the button has been relocated to a place that is not visible by
     // default (e.g. menu panel) then the disabled attribute will not update
     // correctly based on the command, so we update the attribute directly as.
     let cmd = document.getElementById("Social:SharePage");
     if (disabled) {
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -934,16 +934,17 @@
 
 
         <toolbarbutton id="social-share-button"
                        class="toolbarbutton-1 chromeclass-toolbar-additional"
                        label="&sharePageCmd.label;"
                        tooltiptext="&sharePageCmd.label;"
                        cui-areatype="toolbar"
                        removable="true"
+                       hidden="true"
                        command="Social:SharePage"/>
       </hbox>
 
       <toolbarbutton id="nav-bar-overflow-button"
                      class="toolbarbutton-1 chromeclass-toolbar-additional overflow-button"
                      cui-areatype="toolbar"
                      skipintoolbarset="true"
                      tooltiptext="&navbarOverflow.label;"/>
--- a/browser/base/content/newtab/newTab.js
+++ b/browser/base/content/newtab/newTab.js
@@ -8,17 +8,16 @@ let Cu = Components.utils;
 let Ci = Components.interfaces;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PageThumbs.jsm");
 Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm");
 Cu.import("resource://gre/modules/DirectoryLinksProvider.jsm");
 Cu.import("resource://gre/modules/NewTabUtils.jsm");
-Cu.import("resource://gre/modules/Promise.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Rect",
   "resource://gre/modules/Geometry.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
   "resource://gre/modules/PrivateBrowsingUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
   "resource://gre/modules/UpdateChannel.jsm");
 
--- a/browser/base/content/newtab/transformations.js
+++ b/browser/base/content/newtab/transformations.js
@@ -174,29 +174,28 @@ let gTransformation = {
     let callback = aOptions && aOptions.callback;
     let unfreeze = aOptions && aOptions.unfreeze;
 
     aSites.forEach(function (aSite, aIndex) {
       // Do not re-arrange empty cells or the dragged site.
       if (!aSite || aSite == gDrag.draggedSite)
         return;
 
-      let deferred = Promise.defer();
-      batch.push(deferred.promise);
-      let cb = deferred.resolve;
-
-      if (!cells[aIndex])
-        // The site disappeared from the grid, hide it.
-        this.hideSite(aSite, cb);
-      else if (this._getNodeOpacity(aSite.node) != 1)
-        // The site disappeared before but is now back, show it.
-        this.showSite(aSite, cb);
-      else
-        // The site's position has changed, move it around.
-        this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: cb});
+      batch.push(new Promise(resolve => {
+        if (!cells[aIndex]) {
+          // The site disappeared from the grid, hide it.
+          this.hideSite(aSite, resolve);
+        } else if (this._getNodeOpacity(aSite.node) != 1) {
+          // The site disappeared before but is now back, show it.
+          this.showSite(aSite, resolve);
+        } else {
+          // The site's position has changed, move it around.
+          this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: resolve});
+        }
+      }));
     }, this);
 
     if (callback) {
       Promise.all(batch).then(callback);
     }
   },
 
   /**
--- a/browser/base/content/newtab/updater.js
+++ b/browser/base/content/newtab/updater.js
@@ -129,27 +129,26 @@ let gUpdater = {
     let batch = [];
 
     // Delete sites that were removed from the grid.
     gGrid.sites.forEach(function (aSite) {
       // The site must be valid and not in the current grid.
       if (!aSite || aSites.indexOf(aSite) != -1)
         return;
 
-      let deferred = Promise.defer();
-      batch.push(deferred.promise);
+      batch.push(new Promise(resolve => {
+        // Fade out the to-be-removed site.
+        gTransformation.hideSite(aSite, function () {
+          let node = aSite.node;
 
-      // Fade out the to-be-removed site.
-      gTransformation.hideSite(aSite, function () {
-        let node = aSite.node;
-
-        // Remove the site from the DOM.
-        node.parentNode.removeChild(node);
-        deferred.resolve();
-      });
+          // Remove the site from the DOM.
+          node.parentNode.removeChild(node);
+          resolve();
+        });
+      }));
     });
 
     Promise.all(batch).then(aCallback);
   },
 
   /**
    * Tries to fill empty cells with new links if available.
    * @param aLinks The array of links.
@@ -159,26 +158,25 @@ let gUpdater = {
     let {cells, sites} = gGrid;
     let batch = [];
 
     // Find empty cells and fill them.
     sites.forEach(function (aSite, aIndex) {
       if (aSite || !aLinks[aIndex])
         return;
 
-      let deferred = Promise.defer();
-      batch.push(deferred.promise);
-
-      // Create the new site and fade it in.
-      let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]);
+      batch.push(new Promise(resolve => {
+        // Create the new site and fade it in.
+        let site = gGrid.createSite(aLinks[aIndex], cells[aIndex]);
 
-      // Set the site's initial opacity to zero.
-      site.node.style.opacity = 0;
+        // Set the site's initial opacity to zero.
+        site.node.style.opacity = 0;
 
-      // Flush all style changes for the dynamically inserted site to make
-      // the fade-in transition work.
-      window.getComputedStyle(site.node).opacity;
-      gTransformation.showSite(site, function () deferred.resolve());
+        // Flush all style changes for the dynamically inserted site to make
+        // the fade-in transition work.
+        window.getComputedStyle(site.node).opacity;
+        gTransformation.showSite(site, resolve);
+      }));
     });
 
     Promise.all(batch).then(aCallback);
   }
 };
--- a/browser/base/content/test/newtab/browser_newtab_reportLinkAction.js
+++ b/browser/base/content/test/newtab/browser_newtab_reportLinkAction.js
@@ -42,16 +42,17 @@ function runTests() {
   expected.tile = 1;
   expected.pinned = false;
   yield EventUtils.synthesizeMouseAtCenter(pinButton, {}, getContentWindow());
 
   // Unpin that link
   expected.action = "unpin";
   expected.pinned = true;
   yield EventUtils.synthesizeMouseAtCenter(pinButton, {}, getContentWindow());
+  yield whenPagesUpdated();
 
   // Block the site in the 0th tile spot
   let blockedSite = getCell(0).node.querySelector(".newtab-site");
   let blockButton = blockedSite.querySelector(".newtab-control-block");
   expected.type = "organic";
   expected.link = 0;
   expected.action = "block";
   expected.tile = 0;
--- a/browser/branding/aurora/pref/firefox-branding.js
+++ b/browser/branding/aurora/pref/firefox-branding.js
@@ -6,17 +6,18 @@
 
 // We don't have pages ready for this, so leave these prefs empty for now (bug 648330)
 pref("startup.homepage_override_url","");
 pref("startup.homepage_welcome_url","");
 // The time interval between checks for a new version (in seconds)
 pref("app.update.interval", 28800); // 8 hours
 // The time interval between the downloading of mar file chunks in the
 // background (in seconds)
-pref("app.update.download.backgroundInterval", 60);
+// 0 means "download everything at once"
+pref("app.update.download.backgroundInterval", 0);
 // Give the user x seconds to react before showing the big UI. default=168 hours
 pref("app.update.promptWaitTime", 604800);
 // URL user can browse to manually if for some reason all update installation
 // attempts fail.
 pref("app.update.url.manual", "https://www.mozilla.org/firefox/aurora/");
 // A default value for the "More information about this update" link
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://www.mozilla.org/firefox/aurora/");
--- a/browser/branding/nightly/pref/firefox-branding.js
+++ b/browser/branding/nightly/pref/firefox-branding.js
@@ -3,17 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 pref("startup.homepage_override_url", "https://www.mozilla.org/projects/firefox/%VERSION%/whatsnew/?oldversion=%OLD_VERSION%");
 pref("startup.homepage_welcome_url", "https://www.mozilla.org/projects/firefox/%VERSION%/firstrun/");
 // The time interval between checks for a new version (in seconds)
 pref("app.update.interval", 7200); // 2 hours
 // The time interval between the downloading of mar file chunks in the
 // background (in seconds)
-pref("app.update.download.backgroundInterval", 60);
+// 0 means "download everything at once"
+pref("app.update.download.backgroundInterval", 0);
 // Give the user x seconds to react before showing the big UI. default=12 hours
 pref("app.update.promptWaitTime", 43200);
 // URL user can browse to manually if for some reason all update installation
 // attempts fail.
 pref("app.update.url.manual", "https://nightly.mozilla.org");
 // A default value for the "More information about this update" link
 // supplied in the "An update is available" page of the update wizard. 
 pref("app.update.url.details", "https://nightly.mozilla.org");
--- a/browser/components/preferences/in-content/privacy.xul
+++ b/browser/components/preferences/in-content/privacy.xul
@@ -73,17 +73,17 @@
       data-category="panePrivacy">
   <image class="header-icon"/>
   <label class="header-name">&panePrivacy.title;</label>
 </hbox>
 
 <!-- Tracking -->
 <groupbox id="trackingGroup" data-category="panePrivacy" hidden="true" align="start">
   <caption><label>&tracking.label;</label></caption>
-  <radiogroup id="doNotTrackSelection" orient="vertical"
+  <radiogroup id="doNotTrackSelection" orient="vertical" align="start"
               preference="privacy.donottrackheader.value"
               onsynctopreference="return gPrivacyPane.setTrackingPrefs()"
               onsyncfrompreference="return gPrivacyPane.getTrackingPrefs()">
     <radio id="dntnotrack" value="1" label="&dntTrackingNotOkay.label2;"
             accesskey="&dntTrackingNotOkay.accesskey;" />
     <radio id="dntdotrack" value="0" label="&dntTrackingOkay.label2;"
             accesskey="&dntTrackingOkay.accesskey;" />
     <radio id="dntnopref" value="-1" label="&dntTrackingNopref.label2;"
@@ -146,47 +146,41 @@
           >&dontrememberActions.clearHistory.label;</html:a>&dontrememberActions.post.label;</description>
         </vbox>
         <spacer flex="1" class="indent"/>
       </hbox>
     </vbox>
     <vbox id="historyCustomPane">
       <separator class="thin"/>
       <vbox class="indent">
-        <hbox>
+        <vbox align="start">
           <checkbox id="privateBrowsingAutoStart"
                     label="&privateBrowsingPermanent2.label;"
                     accesskey="&privateBrowsingPermanent2.accesskey;"
                     preference="browser.privatebrowsing.autostart"
                     oncommand="gPrivacyPane.updateAutostart()"/>
-          <spacer flex="1"/>
-        </hbox>
+        </vbox>
         <vbox class="indent">
-          <hbox>
+          <vbox align="start">
             <checkbox id="rememberHistory"
                       label="&rememberHistory2.label;"
                       accesskey="&rememberHistory2.accesskey;"
                       preference="places.history.enabled"/>
-            <spacer flex="1"/>
-          </hbox>
-          <hbox>
             <checkbox id="rememberForms"
                       label="&rememberSearchForm.label;"
                       accesskey="&rememberSearchForm.accesskey;"
                       preference="browser.formfill.enable"/>
-            <spacer flex="1"/>
-          </hbox>
-
+          </vbox>
           <hbox id="cookiesBox">
             <checkbox id="acceptCookies" label="&acceptCookies.label;"
                       preference="network.cookie.cookieBehavior"
                       accesskey="&acceptCookies.accesskey;"
                       onsyncfrompreference="return gPrivacyPane.readAcceptCookies();"
                       onsynctopreference="return gPrivacyPane.writeAcceptCookies();"/>
-            <spacer flex="1"/>
+            <spacer flex="1" />
             <button id="cookieExceptions" oncommand="gPrivacyPane.showCookieExceptions();"
                     label="&cookieExceptions.label;" accesskey="&cookieExceptions.accesskey;"
                     preference="pref.privacy.disable_button.cookie_exceptions"/>
           </hbox>
           <hbox id="acceptThirdPartyRow"
                 class="indent"
                 align="center">
             <label id="acceptThirdPartyLabel" control="acceptThirdPartyMenu"
--- a/browser/themes/osx/browser.css
+++ b/browser/themes/osx/browser.css
@@ -3613,16 +3613,17 @@ toolbarbutton.chevron > .toolbarbutton-m
 notification[value="translation"] {
   color: #484848;
   background-color: #EFEFEF;
   background-image: none;
   border-top: none;
   border-bottom: 1px solid #c4c4c4;
   padding-top: 1px;
   padding-bottom: 1px;
+  min-height: 35px;
 }
 
 .translate-infobar-element {
   margin-top: 0 !important;
   margin-bottom: 0 !important;
 }
 
 button.translate-infobar-element {
--- a/mobile/android/base/PrivateTab.java
+++ b/mobile/android/base/PrivateTab.java
@@ -5,16 +5,21 @@
 
 package org.mozilla.gecko;
 
 import android.content.Context;
 
 public class PrivateTab extends Tab {
     public PrivateTab(Context context, int id, String url, boolean external, int parentId, String title) {
         super(context, id, url, external, parentId, title);
+
+        // Init background to background_private to ensure flicker-free
+        // private tab creation. Page loads will reset it to white as expected.
+        final int bgColor = context.getResources().getColor(R.color.background_private);
+        setBackgroundColor(bgColor);
     }
 
     @Override
     protected void saveThumbnailToDB() {}
 
     @Override
     public boolean isPrivate() {
         return true;
--- a/mobile/android/base/gfx/LayerRenderer.java
+++ b/mobile/android/base/gfx/LayerRenderer.java
@@ -131,20 +131,18 @@ public class LayerRenderer implements Ta
         "varying vec2 vTexCoord;\n" +
         "uniform sampler2D sTexture;\n" +
         "void main() {\n" +
         "    gl_FragColor = texture2D(sTexture, vTexCoord);\n" +
         "}\n";
 
     public LayerRenderer(LayerView view) {
         mView = view;
-        try {
-            mOverscrollColor = view.getContext().getResources().getColor(R.color.background_normal);
-        } catch (Resources.NotFoundException nfe) { mOverscrollColor = Color.BLACK; }
-        
+        setOverscrollColor(R.color.background_normal);
+
         Bitmap scrollbarImage = view.getScrollbarImage();
         IntSize size = new IntSize(scrollbarImage.getWidth(), scrollbarImage.getHeight());
         scrollbarImage = expandCanvasToPowerOfTwo(scrollbarImage, size);
 
         mTasks = new CopyOnWriteArrayList<RenderTask>();
         mLastFrameTime = System.nanoTime();
 
         mVertScrollLayer = new ScrollbarLayer(this, scrollbarImage, size, true);
@@ -194,16 +192,22 @@ public class LayerRenderer implements Ta
     }
 
     void onSurfaceCreated(EGLConfig config) {
         checkMonitoringEnabled();
         createDefaultProgram();
         activateDefaultProgram();
     }
 
+    void setOverscrollColor(int colorId) {
+        try {
+            mOverscrollColor = mView.getContext().getResources().getColor(colorId);
+        } catch (Resources.NotFoundException nfe) { mOverscrollColor = Color.BLACK; }
+    }
+
     public void createDefaultProgram() {
         int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, DEFAULT_VERTEX_SHADER);
         int fragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, DEFAULT_FRAGMENT_SHADER);
 
         mProgram = GLES20.glCreateProgram();
         GLES20.glAttachShader(mProgram, vertexShader);   // add the vertex shader to program
         GLES20.glAttachShader(mProgram, fragmentShader); // add the fragment shader to program
         GLES20.glLinkProgram(mProgram);                  // creates OpenGL program executables
@@ -712,16 +716,20 @@ public class LayerRenderer implements Ta
     @Override
     public void onTabChanged(final Tab tab, Tabs.TabEvents msg, Object data) {
         // Sets the background of the newly selected tab. This background color
         // gets cleared in endDrawing(). This function runs on the UI thread,
         // but other code that touches the paint state is run on the compositor
         // thread, so this may need to be changed if any problems appear.
         if (msg == Tabs.TabEvents.SELECTED) {
             if (mView != null) {
+                final int overscrollColor =
+                        (tab.isPrivate() ? R.color.background_private : R.color.background_normal);
+                setOverscrollColor(overscrollColor);
+
                 if (mView.getChildAt(0) != null) {
                     mView.getChildAt(0).setBackgroundColor(tab.getBackgroundColor());
                 }
                 mView.setPaintState(LayerView.PAINT_START);
             }
         }
     }
 }
--- a/mobile/android/base/resources/layout/remote_tabs_setup_panel.xml
+++ b/mobile/android/base/resources/layout/remote_tabs_setup_panel.xml
@@ -1,18 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
-<org.mozilla.gecko.tabspanel.RemoteTabsSetupPanel
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:visibility="gone">
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <LinearLayout android:id="@+id/remote_tabs_setup_containing_layout"
                   style="@style/TabsPanelFrame"
                   android:layout_width="match_parent"
                   android:layout_height="match_parent">
 
         <LinearLayout style="@style/TabsPanelSection"
                       android:layout_width="match_parent"
@@ -51,9 +47,9 @@
                       android:layout_height="wrap_content"
                       style="@style/TabsPanelItem.TextAppearance.Linkified"
                       android:text="@string/fxaccount_getting_started_old_firefox"/>
 
         </LinearLayout>
 
     </LinearLayout>
 
-</org.mozilla.gecko.tabspanel.RemoteTabsSetupPanel>
+</merge>
--- a/mobile/android/base/resources/layout/remote_tabs_verification_panel.xml
+++ b/mobile/android/base/resources/layout/remote_tabs_verification_panel.xml
@@ -1,18 +1,14 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!-- 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/. -->
 
-<org.mozilla.gecko.tabspanel.RemoteTabsVerificationPanel
-        xmlns:android="http://schemas.android.com/apk/res/android"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:visibility="gone">
+<merge xmlns:android="http://schemas.android.com/apk/res/android">
 
     <LinearLayout android:id="@+id/remote_tabs_verification_containing_layout"
                   style="@style/TabsPanelFrame"
                   android:layout_width="match_parent"
                   android:layout_height="match_parent">
 
         <LinearLayout style="@style/TabsPanelSection"
                       android:layout_width="match_parent"
@@ -46,9 +42,9 @@
                       style="@style/TabsPanelItem.TextAppearance.Linkified.Resend"
                       android:layout_width="match_parent"
                       android:text="@string/fxaccount_confirm_account_resend_email"/>
 
         </LinearLayout>
 
     </LinearLayout>
 
-</org.mozilla.gecko.tabspanel.RemoteTabsVerificationPanel>
+</merge>
--- a/mobile/android/base/tabspanel/RemoteTabsPanel.java
+++ b/mobile/android/base/tabspanel/RemoteTabsPanel.java
@@ -94,30 +94,30 @@ class RemoteTabsPanel extends FrameLayou
         if (accountState.getNeededAction() == State.Action.NeedsVerification) {
             return RemotePanelType.VERIFICATION;
         }
 
         return RemotePanelType.CONTAINER;
     }
 
     private PanelView inflatePanel(final RemotePanelType panelType) {
-        final LayoutInflater inflater = LayoutInflater.from(getContext());
-        final View inflatedView;
+        final PanelView view;
         switch (panelType) {
             case SETUP:
-                inflatedView = inflater.inflate(R.layout.remote_tabs_setup_panel, null);
+                view = new RemoteTabsSetupPanel(getContext());
                 break;
 
             case VERIFICATION:
-                inflatedView = inflater.inflate(R.layout.remote_tabs_verification_panel, null);
+                view = new RemoteTabsVerificationPanel(getContext());
                 break;
 
             case CONTAINER:
-                inflatedView = inflater.inflate(R.layout.remote_tabs_container_panel, null);
+                final LayoutInflater inflater = LayoutInflater.from(getContext());
+                view = (PanelView) inflater.inflate(R.layout.remote_tabs_container_panel, null);
                 break;
 
             default:
                 throw new IllegalArgumentException("Unknown panelType, " + panelType);
         }
 
-        return (PanelView) inflatedView;
+        return view;
     }
 }
--- a/mobile/android/base/tabspanel/RemoteTabsSetupPanel.java
+++ b/mobile/android/base/tabspanel/RemoteTabsSetupPanel.java
@@ -9,39 +9,35 @@ import java.util.Locale;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.activities.FxAccountCreateAccountActivity;
 import org.mozilla.gecko.tabspanel.TabsPanel.PanelView;
 
 import android.content.Context;
 import android.content.Intent;
-import android.util.AttributeSet;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
 
 /**
  * A tabs panel which allows a user to get started setting up a Firefox
  * Accounts account. Currently used as one sub-panel in a sequence
  * contained by the {@link RemoteTabsPanel}.
  */
 class RemoteTabsSetupPanel extends ScrollView implements PanelView {
-    private LinearLayout containingLayout;
+    private final LinearLayout containingLayout;
 
     private TabsPanel tabsPanel;
 
-    public RemoteTabsSetupPanel(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
+    public RemoteTabsSetupPanel(Context context) {
+        super(context);
 
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
+        LayoutInflater.from(context).inflate(R.layout.remote_tabs_setup_panel, this);
         containingLayout = (LinearLayout) findViewById(R.id.remote_tabs_setup_containing_layout);
 
         final View setupGetStartedButton =
                 containingLayout.findViewById(R.id.remote_tabs_setup_get_started);
         setupGetStartedButton.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(final View v) {
                 final Context context = getContext();
--- a/mobile/android/base/tabspanel/RemoteTabsVerificationPanel.java
+++ b/mobile/android/base/tabspanel/RemoteTabsVerificationPanel.java
@@ -5,43 +5,39 @@
 package org.mozilla.gecko.tabspanel;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.fxa.FirefoxAccounts;
 import org.mozilla.gecko.fxa.login.State;
 import org.mozilla.gecko.tabspanel.TabsPanel.PanelView;
 
 import android.content.Context;
-import android.util.AttributeSet;
 import android.util.Log;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.widget.LinearLayout;
 import android.widget.ScrollView;
 import android.widget.TextView;
 
 /**
  * A tabs panel which allows a user to get resend the verification email
  * to confirm a Firefox Account. Currently used as one sub-panel in a sequence
  * contained by the {@link RemoteTabsPanel}.
  */
 class RemoteTabsVerificationPanel extends ScrollView implements PanelView {
     private static final String LOG_TAG = RemoteTabsVerificationPanel.class.getSimpleName();
 
-    private LinearLayout containingLayout;
+    private final LinearLayout containingLayout;
 
     private TabsPanel tabsPanel;
 
-    public RemoteTabsVerificationPanel(Context context, AttributeSet attrs) {
-        super(context, attrs);
-    }
+    public RemoteTabsVerificationPanel(Context context) {
+        super(context);
 
-    @Override
-    protected void onFinishInflate() {
-        super.onFinishInflate();
-
+        LayoutInflater.from(context).inflate(R.layout.remote_tabs_verification_panel, this);
         containingLayout = (LinearLayout) findViewById(R.id.remote_tabs_verification_containing_layout);
 
         final View resendLink = containingLayout.findViewById(R.id.remote_tabs_confirm_resend);
         resendLink.setOnClickListener(new OnClickListener() {
             @Override
             public void onClick(View v) {
                 final State accountState = FirefoxAccounts.getFirefoxAccountState(getContext());
                 final State.Action neededAction = accountState.getNeededAction();
--- a/mobile/android/base/tabspanel/TabsPanel.java
+++ b/mobile/android/base/tabspanel/TabsPanel.java
@@ -167,19 +167,25 @@ public class TabsPanel extends LinearLay
 
         mTabWidget.setTabSelectionListener(this);
 
         mMenuButton = (ImageButton) findViewById(R.id.menu);
         mMenuButton.setOnClickListener(new Button.OnClickListener() {
             @Override
             public void onClick(View view) {
                 final Menu menu = mPopupMenu.getMenu();
+
+                // Each panel has a "+" shortcut button, so don't show it for that panel.
+                menu.findItem(R.id.new_tab).setVisible(mCurrentPanel != Panel.NORMAL_TABS);
+                menu.findItem(R.id.new_private_tab).setVisible(mCurrentPanel != Panel.PRIVATE_TABS);
+
+                // Only show "Clear * tabs" for current panel.
                 menu.findItem(R.id.close_all_tabs).setVisible(mCurrentPanel == Panel.NORMAL_TABS);
                 menu.findItem(R.id.close_private_tabs).setVisible(mCurrentPanel == Panel.PRIVATE_TABS);
-
+ 
                 mPopupMenu.show();
             }
         });
         mPopupMenu.setAnchor(mMenuButton);
     }
 
     private void addTab() {
         if (mCurrentPanel == Panel.NORMAL_TABS) {
--- a/services/healthreport/docs/dataformat.rst
+++ b/services/healthreport/docs/dataformat.rst
@@ -1068,16 +1068,45 @@ Example
       ]
     }
 
 org.mozilla.crashes.crashes
 ---------------------------
 
 This measurement contains a historical record of application crashes.
 
+Version 4
+^^^^^^^^^
+
+This version follows up from version 3, adding submissions which are now
+tracked by the :ref:`crashes_crashmanager`.
+
+This measurement will be reported on each day there was a crash or crash
+submission. Records may contain the following fields, whose values indicate
+the number of crashes, hangs, or submissions that occurred on the given day:
+
+* main-crash
+* main-crash-submission-succeeded
+* main-crash-submission-failed
+* main-hang
+* main-hang-submission-succeeded
+* main-hang-submission-failed
+* content-crash
+* content-crash-submission-succeeded
+* content-crash-submission-failed
+* content-hang
+* content-hang-submission-succeeded
+* content-hang-submission-failed
+* plugin-crash
+* plugin-crash-submission-succeeded
+* plugin-crash-submission-failed
+* plugin-hang
+* plugin-hang-submission-succeeded
+* plugin-hang-submission-failed
+
 Version 3
 ^^^^^^^^^
 
 This version follows up from version 2, building on improvements to
 the :ref:`crashes_crashmanager`.
 
 This measurement will be reported on each day there was a
 crash. Records may contain the following fields, whose values indicate
@@ -1147,16 +1176,24 @@ Example
       "_v": 1,
       "pending": 1,
       "submitted": 2
     },
     "org.mozilla.crashes.crashes": {
       "_v": 2,
       "mainCrash": 2
     }
+    "org.mozilla.crashes.crashes": {
+      "_v": 4,
+      "main-crash": 2,
+      "main-crash-submission-succeeded": 1,
+      "main-crash-submission-failed": 1,
+      "main-hang": 1,
+      "plugin-crash": 2
+    }
 
 org.mozilla.healthreport.submissions
 ------------------------------------
 
 This measurement contains a history of FHR's own data submission activity.
 It was added in Firefox 23 in early May 2013.
 
 Version 2
--- a/services/healthreport/providers.jsm
+++ b/services/healthreport/providers.jsm
@@ -1042,46 +1042,79 @@ DailyCrashesMeasurement3.prototype = Obj
     "main-hang": DAILY_LAST_NUMERIC_FIELD,
     "content-crash": DAILY_LAST_NUMERIC_FIELD,
     "content-hang": DAILY_LAST_NUMERIC_FIELD,
     "plugin-crash": DAILY_LAST_NUMERIC_FIELD,
     "plugin-hang": DAILY_LAST_NUMERIC_FIELD,
   },
 });
 
+function DailyCrashesMeasurement4() {
+  Metrics.Measurement.call(this);
+}
+
+DailyCrashesMeasurement4.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "crashes",
+  version: 4,
+
+  fields: {
+    "main-crash": DAILY_LAST_NUMERIC_FIELD,
+    "main-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
+    "main-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
+    "main-hang": DAILY_LAST_NUMERIC_FIELD,
+    "main-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
+    "main-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
+    "content-crash": DAILY_LAST_NUMERIC_FIELD,
+    "content-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
+    "content-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
+    "content-hang": DAILY_LAST_NUMERIC_FIELD,
+    "content-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
+    "content-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
+    "plugin-crash": DAILY_LAST_NUMERIC_FIELD,
+    "plugin-crash-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
+    "plugin-crash-submission-failed": DAILY_LAST_NUMERIC_FIELD,
+    "plugin-hang": DAILY_LAST_NUMERIC_FIELD,
+    "plugin-hang-submission-succeeded": DAILY_LAST_NUMERIC_FIELD,
+    "plugin-hang-submission-failed": DAILY_LAST_NUMERIC_FIELD,
+  },
+});
+
 this.CrashesProvider = function () {
   Metrics.Provider.call(this);
 
   // So we can unit test.
   this._manager = Services.crashmanager;
 };
 
 CrashesProvider.prototype = Object.freeze({
   __proto__: Metrics.Provider.prototype,
 
   name: "org.mozilla.crashes",
 
   measurementTypes: [
     DailyCrashesMeasurement1,
     DailyCrashesMeasurement2,
     DailyCrashesMeasurement3,
+    DailyCrashesMeasurement4,
   ],
 
   pullOnly: true,
 
   collectDailyData: function () {
     return this.storage.enqueueTransaction(this._populateCrashCounts.bind(this));
   },
 
   _populateCrashCounts: function () {
     this._log.info("Grabbing crash counts from crash manager.");
     let crashCounts = yield this._manager.getCrashCountsByDay();
 
-    let m = this.getMeasurement("crashes", 3);
-    let fields = DailyCrashesMeasurement3.prototype.fields;
+    let m = this.getMeasurement("crashes", 4);
+    let fields = DailyCrashesMeasurement4.prototype.fields;
 
     for (let [day, types] of crashCounts) {
       let date = Metrics.daysToDate(day);
       for (let [type, count] of types) {
         if (!(type in fields)) {
           this._log.warn("Unknown crash type encountered: " + type);
           continue;
         }
--- a/services/healthreport/tests/xpcshell/test_provider_crashes.js
+++ b/services/healthreport/tests/xpcshell/test_provider_crashes.js
@@ -45,57 +45,75 @@ add_task(function* test_collect() {
   provider._manager = manager;
 
   let day1 = new Date(2014, 0, 1, 0, 0, 0);
   let day2 = new Date(2014, 0, 3, 0, 0, 0);
 
   yield manager.addCrash(manager.PROCESS_TYPE_MAIN,
                          manager.CRASH_TYPE_CRASH,
                          "mc1", day1);
+  yield manager.addSubmission(manager.PROCESS_TYPE_MAIN,
+                              manager.CRASH_TYPE_CRASH,
+                              true,
+                              "mc1", day1)
   yield manager.addCrash(manager.PROCESS_TYPE_MAIN,
                          manager.CRASH_TYPE_CRASH,
                          "mc2", day1);
+  yield manager.addSubmission(manager.PROCESS_TYPE_MAIN,
+                              manager.CRASH_TYPE_CRASH,
+                              false,
+                              "mc2", day1)
   yield manager.addCrash(manager.PROCESS_TYPE_CONTENT,
                          manager.CRASH_TYPE_HANG,
                          "ch", day1);
   yield manager.addCrash(manager.PROCESS_TYPE_PLUGIN,
                          manager.CRASH_TYPE_CRASH,
                          "pc", day1);
 
   yield manager.addCrash(manager.PROCESS_TYPE_MAIN,
                          manager.CRASH_TYPE_HANG,
                          "mh", day2);
   yield manager.addCrash(manager.PROCESS_TYPE_CONTENT,
                          manager.CRASH_TYPE_CRASH,
                          "cc", day2);
+  yield manager.addSubmission(manager.PROCESS_TYPE_CONTENT,
+                              manager.CRASH_TYPE_CRASH,
+                              true,
+                              "cc", day2)
   yield manager.addCrash(manager.PROCESS_TYPE_PLUGIN,
                          manager.CRASH_TYPE_HANG,
                          "ph", day2);
 
   yield provider.collectDailyData();
 
-  let m = provider.getMeasurement("crashes", 3);
+  let m = provider.getMeasurement("crashes", 4);
   let values = yield m.getValues();
   do_check_eq(values.days.size, 2);
   do_check_true(values.days.hasDay(day1));
   do_check_true(values.days.hasDay(day2));
 
   let value = values.days.getDay(day1);
   do_check_true(value.has("main-crash"));
   do_check_eq(value.get("main-crash"), 2);
+  do_check_true(value.has("main-crash-submission-succeeded"));
+  do_check_eq(value.get("main-crash-submission-succeeded"), 1);
+  do_check_true(value.has("main-crash-submission-failed"));
+  do_check_eq(value.get("main-crash-submission-failed"), 1);
   do_check_true(value.has("content-hang"));
   do_check_eq(value.get("content-hang"), 1);
   do_check_true(value.has("plugin-crash"));
   do_check_eq(value.get("plugin-crash"), 1);
 
   value = values.days.getDay(day2);
   do_check_true(value.has("main-hang"));
   do_check_eq(value.get("main-hang"), 1);
   do_check_true(value.has("content-crash"));
   do_check_eq(value.get("content-crash"), 1);
+  do_check_true(value.has("content-crash-submission-succeeded"));
+  do_check_eq(value.get("content-crash-submission-succeeded"), 1);
   do_check_true(value.has("plugin-hang"));
   do_check_eq(value.get("plugin-hang"), 1);
 
   // Check that adding a new crash increments counter on next collect.
   yield manager.addCrash(manager.PROCESS_TYPE_MAIN,
                          manager.CRASH_TYPE_HANG,
                          "mc3", day2);
 
--- a/toolkit/components/crashes/CrashManager.jsm
+++ b/toolkit/components/crashes/CrashManager.jsm
@@ -125,22 +125,31 @@ this.CrashManager.prototype = Object.fre
   PROCESS_TYPE_MAIN: "main",
 
   // A crash in a content process.
   PROCESS_TYPE_CONTENT: "content",
 
   // A crash in a plugin process.
   PROCESS_TYPE_PLUGIN: "plugin",
 
+  // A submission of a crash.
+  PROCESS_TYPE_SUBMISSION: "submission",
+
   // A real crash.
   CRASH_TYPE_CRASH: "crash",
 
   // A hang.
   CRASH_TYPE_HANG: "hang",
 
+  // A successful submission.
+  SUBMISSION_TYPE_SUCCEEDED: "succeeded",
+
+  // A failed submission.
+  SUBMISSION_TYPE_FAILED: "failed",
+
   DUMP_REGEX: /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.dmp$/i,
   SUBMITTED_REGEX: /^bp-(?:hr-)?([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.txt$/i,
   ALL_REGEX: /^(.*)$/,
 
   // How long the store object should persist in memory before being
   // automatically garbage collected.
   STORE_EXPIRATION_MS: 60 * 1000,
 
@@ -356,16 +365,48 @@ this.CrashManager.prototype = Object.fre
       let store = yield this._getStore();
       if (store.addCrash(processType, crashType, id, date)) {
         yield store.save();
       }
     }.bind(this));
   },
 
   /**
+   * Record the occurrence of a crash submission.
+   *
+   * @param processType (string) One of the PROCESS_TYPE constants.
+   * @param crashType (string) One of the CRASH_TYPE constants.
+   * @param succeeded (boolean) Whether the submission succeeded.
+   * @param id (string) Crash ID. Likely a UUID.
+   * @param date (Date) When the crash occurred.
+   *
+   * @return boolean True if the crash submission was recorded and false if not.
+   */
+  addSubmission: function (processType, crashType, succeeded, id, date) {
+    return Task.spawn(function* () {
+      let store = yield this._getStore();
+      if (this._addSubmissionAsCrash(store, processType, crashType, succeeded,
+                                     id, date)) {
+        yield store.save();
+      }
+    }.bind(this));
+  },
+
+  _addSubmissionAsCrash: function (store, processType, crashType, succeeded,
+                                   id, date) {
+    let id = id + "-" + this.PROCESS_TYPE_SUBMISSION;
+    let process = processType + "-" + crashType + "-" +
+                  this.PROCESS_TYPE_SUBMISSION;
+    let submission_type = (
+      succeeded ? this.SUBMISSION_TYPE_SUCCEEDED : this.SUBMISSION_TYPE_FAILED);
+
+    return store.addCrash(process, submission_type, id, date);
+  },
+
+  /**
    * Obtain the paths of all unprocessed events files.
    *
    * The promise-resolved array is sorted by file mtime, oldest to newest.
    */
   _getUnprocessedEventsFiles: function () {
     return Task.spawn(function* () {
       let entries = [];
 
@@ -420,37 +461,45 @@ this.CrashManager.prototype = Object.fre
       return this._handleEventFilePayload(store, entry, type, date, payload);
     }.bind(this));
   },
 
   _handleEventFilePayload: function (store, entry, type, date, payload) {
       // The payload types and formats are documented in docs/crash-events.rst.
       // Do not change the format of an existing type. Instead, invent a new
       // type.
-
-      // type in event file => [processType, crashType]
-      let eventMap = {
-        "crash.main.1": ["main", "crash"],
-      };
+      // DO NOT ADD NEW TYPES WITHOUT DOCUMENTING!
+      let lines = payload.split("\n");
 
-      if (type in eventMap) {
-        let lines = payload.split("\n");
-        if (lines.length > 1) {
-          this._log.warn("Multiple lines unexpected in payload for " +
-                         entry.path);
-          return this.EVENT_FILE_ERROR_MALFORMED;
-        }
+      switch (type) {
+        case "crash.main.1":
+          if (lines.length > 1) {
+            this._log.warn("Multiple lines unexpected in payload for " +
+                           entry.path);
+            return this.EVENT_FILE_ERROR_MALFORMED;
+          }
+          store.addCrash(this.PROCESS_TYPE_MAIN, this.CRASH_TYPE_CRASH,
+                         payload, date);
+          break;
 
-        store.addCrash(...eventMap[type], payload, date);
-        return this.EVENT_FILE_SUCCESS;
+        case "crash.submission.1":
+          if (lines.length == 3) {
+            this._addSubmissionAsCrash(store, this.PROCESS_TYPE_MAIN,
+                                       this.CRASH_TYPE_CRASH,
+                                       lines[1] === "true", lines[0], date);
+          } else {
+            return this.EVENT_FILE_ERROR_MALFORMED;
+          }
+          break;
+
+        default:
+          return this.EVENT_FILE_ERROR_UNKNOWN_EVENT;
       }
 
-      // DO NOT ADD NEW TYPES WITHOUT DOCUMENTING!
-
-      return this.EVENT_FILE_ERROR_UNKNOWN_EVENT;
+      return this.EVENT_FILE_SUCCESS;
   },
 
   /**
    * The resolved promise is an array of objects with the properties:
    *
    *   path -- String filename
    *   id -- regexp.match()[1] (likely the crash ID)
    *   date -- Date mtime of the file
--- a/toolkit/components/crashes/docs/crash-events.rst
+++ b/toolkit/components/crashes/docs/crash-events.rst
@@ -69,16 +69,28 @@ crash.main.1
 ^^^^^^^^^^^^
 
 This event is produced when the main process crashes.
 
 The payload of this event is the string crash ID, very likely a UUID.
 There should be ``UUID.dmp`` and ``UUID.extra`` files on disk, saved by
 Breakpad.
 
+crash.submission.1
+^^^^^^^^^^^^
+
+This event is produced when a crash is submitted.
+
+The payload of this event is delimited by UNIX newlines (*\n*) and contains the
+following fields:
+
+* The crash ID string
+* "true" if the submission succeeded or "false" otherwise
+* The remote crash ID string if the submission succeeded
+
 Aggregated Event Log
 ====================
 
 Crash events are aggregated together into a unified event *log*. Currently,
 this *log* is really a JSON file. However, this is an implementation detail
 and it could change at any time. The interface to crash data provided by
 the JavaScript API is the only supported interface.
 
--- a/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_manager.js
@@ -299,8 +299,47 @@ add_task(function* test_addCrash() {
   Assert.ok(crash.isOfType(m.PROCESS_TYPE_PLUGIN, m.CRASH_TYPE_CRASH));
 
   crash = map.get("plugin-hang");
   Assert.ok(!!crash);
   Assert.equal(crash.crashDate, DUMMY_DATE);
   Assert.equal(crash.type, m.PROCESS_TYPE_PLUGIN + "-" + m.CRASH_TYPE_HANG);
   Assert.ok(crash.isOfType(m.PROCESS_TYPE_PLUGIN, m.CRASH_TYPE_HANG));
 });
+
+add_task(function* test_addSubmission() {
+  let m = yield getManager();
+
+  let crashes = yield m.getCrashes();
+  Assert.equal(crashes.length, 0);
+
+  yield m.addSubmission(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_CRASH, true,
+                        "success", DUMMY_DATE);
+  yield m.addSubmission(m.PROCESS_TYPE_MAIN, m.CRASH_TYPE_CRASH, false,
+                        "failure", DUMMY_DATE);
+
+  crashes = yield m.getCrashes();
+  Assert.equal(crashes.length, 2);
+
+  let map = new Map(crashes.map(crash => [crash.id, crash]));
+
+  let crash = map.get("success-submission");
+  Assert.ok(!!crash);
+  Assert.equal(crash.crashDate, DUMMY_DATE);
+  Assert.equal(crash.type,
+               m.PROCESS_TYPE_MAIN + "-" + m.CRASH_TYPE_CRASH + "-" +
+               m.PROCESS_TYPE_SUBMISSION + "-" + m.SUBMISSION_TYPE_SUCCEEDED);
+  Assert.ok(
+    crash.isOfType(m.PROCESS_TYPE_MAIN + "-" + m.CRASH_TYPE_CRASH + "-" +
+                   m.PROCESS_TYPE_SUBMISSION, m.SUBMISSION_TYPE_SUCCEEDED));
+
+  let crash = map.get("failure-submission");
+  Assert.ok(!!crash);
+  Assert.equal(crash.crashDate, DUMMY_DATE);
+  Assert.equal(crash.type,
+               m.PROCESS_TYPE_MAIN + "-" + m.CRASH_TYPE_CRASH + "-" +
+               m.PROCESS_TYPE_SUBMISSION + "-" + m.SUBMISSION_TYPE_FAILED);
+  Assert.ok(
+    crash.isOfType(m.PROCESS_TYPE_MAIN + "-" + m.CRASH_TYPE_CRASH + "-" +
+                   m.PROCESS_TYPE_SUBMISSION, m.SUBMISSION_TYPE_FAILED));
+
+});
+
--- a/toolkit/components/crashes/tests/xpcshell/test_crash_store.js
+++ b/toolkit/components/crashes/tests/xpcshell/test_crash_store.js
@@ -12,18 +12,21 @@ const {classes: Cc, interfaces: Ci, util
 let bsp = Cu.import("resource://gre/modules/CrashManager.jsm", this);
 Cu.import("resource://gre/modules/osfile.jsm", this);
 Cu.import("resource://gre/modules/Task.jsm", this);
 
 const {
   PROCESS_TYPE_MAIN,
   PROCESS_TYPE_CONTENT,
   PROCESS_TYPE_PLUGIN,
+  PROCESS_TYPE_SUBMISSION,
   CRASH_TYPE_CRASH,
   CRASH_TYPE_HANG,
+  SUBMISSION_TYPE_SUCCEEDED,
+  SUBMISSION_TYPE_FAILED,
 } = CrashManager.prototype;
 
 const CrashStore = bsp.CrashStore;
 
 let STORE_DIR_COUNT = 0;
 
 function getStore() {
   return Task.spawn(function* () {
@@ -268,16 +271,54 @@ add_task(function* test_add_plugin_hang(
     s.addCrash(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG, "id1", new Date())
   );
   Assert.equal(s.crashesCount, 2);
 
   let crashes = s.getCrashesOfType(PROCESS_TYPE_PLUGIN, CRASH_TYPE_HANG);
   Assert.equal(crashes.length, 2);
 });
 
+add_task(function* test_add_submission() {
+  let s = yield getStore();
+
+  Assert.ok(
+    s.addCrash(PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_CRASH + "-" +
+               PROCESS_TYPE_SUBMISSION, SUBMISSION_TYPE_SUCCEEDED,
+               "id1", new Date())
+  );
+  Assert.equal(s.crashesCount, 1);
+
+  let c = s.crashes[0];
+  Assert.ok(c.crashDate);
+  Assert.equal(c.type, PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_CRASH + "-" +
+               PROCESS_TYPE_SUBMISSION + "-" + SUBMISSION_TYPE_SUCCEEDED);
+  Assert.ok(c.isOfType(PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_CRASH + "-" +
+                       PROCESS_TYPE_SUBMISSION, SUBMISSION_TYPE_SUCCEEDED));
+
+  Assert.ok(
+    s.addCrash(PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_CRASH + "-" +
+               PROCESS_TYPE_SUBMISSION, SUBMISSION_TYPE_FAILED,
+               "id2", new Date())
+  );
+  Assert.equal(s.crashesCount, 2);
+
+  // Duplicate.
+  Assert.ok(
+    s.addCrash(PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_CRASH + "-" +
+               PROCESS_TYPE_SUBMISSION, SUBMISSION_TYPE_SUCCEEDED,
+               "id1", new Date())
+  );
+  Assert.equal(s.crashesCount, 2);
+
+  let crashes = s.getCrashesOfType(PROCESS_TYPE_MAIN + "-" + CRASH_TYPE_CRASH +
+                                   "-" + PROCESS_TYPE_SUBMISSION,
+                                   SUBMISSION_TYPE_SUCCEEDED);
+  Assert.equal(crashes.length, 1);
+});
+
 add_task(function* test_add_mixed_types() {
   let s = yield getStore();
 
   Assert.ok(
     s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_CRASH, "mcrash", new Date()) &&
     s.addCrash(PROCESS_TYPE_MAIN, CRASH_TYPE_HANG, "mhang", new Date()) &&
     s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_CRASH, "ccrash", new Date()) &&
     s.addCrash(PROCESS_TYPE_CONTENT, CRASH_TYPE_HANG, "chang", new Date()) &&
--- a/toolkit/crashreporter/client/crashreporter.cpp
+++ b/toolkit/crashreporter/client/crashreporter.cpp
@@ -27,19 +27,22 @@ using std::ostream;
 using std::ofstream;
 using std::vector;
 using std::auto_ptr;
 
 namespace CrashReporter {
 
 StringTable  gStrings;
 string       gSettingsPath;
+string       gEventsPath;
 int          gArgc;
 char**       gArgv;
 
+enum SubmissionResult {Succeeded, Failed};
+
 static auto_ptr<ofstream> gLogStream(nullptr);
 static string             gReporterDumpFile;
 static string             gExtraFile;
 
 static string kExtraDataExtension = ".extra";
 
 void UIError(const string& message)
 {
@@ -167,16 +170,63 @@ bool WriteStringsToFile(const string& pa
     success = WriteStrings(*f, header, strings, escape);
     f->close();
   }
 
   delete f;
   return success;
 }
 
+static string Basename(const string& file)
+{
+  string::size_type slashIndex = file.rfind(UI_DIR_SEPARATOR);
+  if (slashIndex != string::npos)
+    return file.substr(slashIndex + 1);
+  else
+    return file;
+}
+
+static string GetDumpLocalID()
+{
+  string localId = Basename(gReporterDumpFile);
+  string::size_type dot = localId.rfind('.');
+
+  if (dot == string::npos)
+    return "";
+
+  return localId.substr(0, dot);
+}
+
+static void WriteSubmissionEvent(SubmissionResult result,
+                                 const string& remoteId)
+{
+  if (gEventsPath.empty()) {
+    // If there is no path for writing the submission event, skip it.
+    return;
+  }
+
+  string localId = GetDumpLocalID();
+  string fpath = gEventsPath + UI_DIR_SEPARATOR + localId + "-submission";
+  ofstream* f = UIOpenWrite(fpath.c_str());
+  time_t tm;
+  time(&tm);
+
+  if (f->is_open()) {
+    *f << "crash.submission.1\n";
+    *f << tm << "\n";
+    *f << localId << "\n";
+    *f << (result == Succeeded ? "true" : "false") << "\n";
+    *f << remoteId;
+
+    f->close();
+  }
+
+  delete f;
+}
+
 void LogMessage(const std::string& message)
 {
   if (gLogStream.get()) {
     char date[64];
     time_t tm;
     time(&tm);
     if (strftime(date, sizeof(date) - 1, "%c", localtime(&tm)) == 0)
         date[0] = '\0';
@@ -213,25 +263,16 @@ static string GetExtraDataFilename(const
   int dot = filename.rfind('.');
   if (dot < 0)
     return "";
 
   filename.replace(dot, filename.length() - dot, kExtraDataExtension);
   return filename;
 }
 
-static string Basename(const string& file)
-{
-  int slashIndex = file.rfind(UI_DIR_SEPARATOR);
-  if (slashIndex >= 0)
-    return file.substr(slashIndex + 1);
-  else
-    return file;
-}
-
 static bool MoveCrashData(const string& toDir,
                           string& dumpfile,
                           string& extrafile)
 {
   if (!UIEnsurePathExists(toDir)) {
     UIError(gStrings[ST_ERROR_CREATEDUMPDIR]);
     return false;
   }
@@ -311,16 +352,17 @@ static bool AddSubmittedReport(const str
                 gStrings["CrashDetailsURL"].c_str(),
                 responseItems["ViewURL"].c_str());
     *file << buf << "\n";
   }
 
   file->close();
   delete file;
 
+  WriteSubmissionEvent(Succeeded, responseItems["CrashID"]);
   return true;
 }
 
 void DeleteDump()
 {
   const char* noDelete = getenv("MOZ_CRASHREPORTER_NO_DELETE_DUMP");
   if (!noDelete || *noDelete == '\0') {
     if (!gReporterDumpFile.empty())
@@ -338,17 +380,20 @@ void SendCompleted(bool success, const s
     }
     else {
       string directory = gReporterDumpFile;
       int slashpos = directory.find_last_of("/\\");
       if (slashpos < 2)
         return;
       directory.resize(slashpos);
       UIPruneSavedDumps(directory);
+      WriteSubmissionEvent(Failed, "");
     }
+  } else {
+    WriteSubmissionEvent(Failed, "");
   }
 }
 
 bool ShouldEnableSending()
 {
   srand(time(0));
   return ((rand() % 100) < MOZ_CRASHREPORTER_ENABLE_PERCENT);
 }
@@ -509,16 +554,33 @@ int main(int argc, char** argv)
 
     if (gSettingsPath.empty() || !UIEnsurePathExists(gSettingsPath)) {
       UIError(gStrings[ST_ERROR_NOSETTINGSPATH]);
       return 0;
     }
 
     OpenLogFile();
 
+#ifdef XP_WIN32
+    static const wchar_t kEventsDirKey[] = L"MOZ_CRASHREPORTER_EVENTS_DIRECTORY";
+    const wchar_t *eventsPath = _wgetenv(kEventsDirKey);
+    if (eventsPath && *eventsPath) {
+      gEventsPath = WideToUTF8(eventsPath);
+    }
+#else
+    static const char kEventsDirKey[] = "MOZ_CRASHREPORTER_EVENTS_DIRECTORY";
+    const char *eventsPath = getenv(kEventsDirKey);
+    if (eventsPath && *eventsPath) {
+      gEventsPath = eventsPath;
+    }
+#endif
+    else {
+      gEventsPath.clear();
+    }
+
     if (!UIFileExists(gReporterDumpFile)) {
       UIError(gStrings[ST_ERROR_DUMPFILEEXISTS]);
       return 0;
     }
 
     string pendingDir = gSettingsPath + UI_DIR_SEPARATOR + "pending";
     if (!MoveCrashData(pendingDir, gReporterDumpFile, gExtraFile)) {
       return 0;
--- a/toolkit/crashreporter/client/crashreporter.h
+++ b/toolkit/crashreporter/client/crashreporter.h
@@ -77,16 +77,17 @@ typedef std::map<std::string, std::strin
 
 //=============================================================================
 // implemented in crashreporter.cpp
 //=============================================================================
 
 namespace CrashReporter {
   extern StringTable  gStrings;
   extern std::string  gSettingsPath;
+  extern std::string  gEventsPath;
   extern int          gArgc;
   extern char**       gArgv;
 
   void UIError(const std::string& message);
 
   // The UI finished sending the report
   void SendCompleted(bool success, const std::string& serverResponse);
 
--- a/toolkit/crashreporter/nsExceptionHandler.cpp
+++ b/toolkit/crashreporter/nsExceptionHandler.cpp
@@ -158,16 +158,17 @@ static const char kCrashMainID[] = "cras
 
 static google_breakpad::ExceptionHandler* gExceptionHandler = nullptr;
 
 static XP_CHAR* pendingDirectory;
 static XP_CHAR* crashReporterPath;
 
 // Where crash events should go.
 static XP_CHAR* eventsDirectory;
+static char* eventsEnv = nullptr;
 
 // If this is false, we don't launch the crash reporter
 static bool doReport = true;
 
 // If this is true, we don't have a crash reporter
 static bool headlessClient = false;
 
 // if this is true, we pass the exception on to the OS crash reporter
@@ -2102,20 +2103,39 @@ SetCrashEventsDir(nsIFile* aDir)
   if (eventsDirectory) {
     NS_Free(eventsDirectory);
   }
 
 #ifdef XP_WIN
   nsString path;
   eventsDir->GetPath(path);
   eventsDirectory = reinterpret_cast<wchar_t*>(ToNewUnicode(path));
+
+  // Save the path in the environment for the crash reporter application.
+  nsAutoString eventsDirEnv(NS_LITERAL_STRING("MOZ_CRASHREPORTER_EVENTS_DIRECTORY="));
+  eventsDirEnv.Append(path);
+  _wputenv(eventsDirEnv.get());
 #else
   nsCString path;
   eventsDir->GetNativePath(path);
   eventsDirectory = ToNewCString(path);
+
+  // Save the path in the environment for the crash reporter application.
+  nsAutoCString eventsDirEnv("MOZ_CRASHREPORTER_EVENTS_DIRECTORY=");
+  eventsDirEnv.Append(path);
+
+  // PR_SetEnv() wants the string to be available for the lifetime
+  // of the app, so dup it here.
+  char* oldEventsEnv = eventsEnv;
+  eventsEnv = ToNewCString(eventsDirEnv);
+  PR_SetEnv(eventsEnv);
+
+  if (oldEventsEnv) {
+    NS_Free(oldEventsEnv);
+  }
 #endif
 }
 
 void
 SetProfileDirectory(nsIFile* aDir)
 {
   nsCOMPtr<nsIFile> dir;
   aDir->Clone(getter_AddRefs(dir));
--- a/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul
+++ b/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul
@@ -11,17 +11,16 @@
 <window title="Update Wizard pages: error patching, download, and errors (partial failed and download complete verification failure)"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         onload="runTestDefault();">
 <script type="application/javascript"
         src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
 <script type="application/javascript"
         src="utils.js"/>
 
-<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
 <script type="application/javascript">
 <![CDATA[
 
 const TESTS = [ {
   pageid: PAGEID_ERROR_PATCHING,
   buttonClick: "next"
 }, {
   pageid: PAGEID_DOWNLOADING,
--- a/toolkit/themes/osx/global/global.css
+++ b/toolkit/themes/osx/global/global.css
@@ -348,16 +348,17 @@ notification > button > .button-box > .b
 }
 
 .close-icon:hover:active {
   -moz-image-region: rect(0, 48px, 16px, 32px);
 }
 
 @media (min-resolution: 2dppx) {
   .close-icon > .button-icon,
+  .close-icon > .button-box > .button-icon,
   .close-icon > .toolbarbutton-icon {
     width: 16px;
   }
 
   .close-icon {
     list-style-image: url("chrome://global/skin/icons/close@2x.png");
     -moz-image-region: rect(0, 32px, 32px, 0);
   }
--- a/webapprt/content/mochitest-shared.js
+++ b/webapprt/content/mochitest-shared.js
@@ -1,16 +1,17 @@
 /* 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/. */
 
 /* Note: this script is loaded by both mochitest.js and head.js, so make sure
  * the code you put here can be evaluated by both! */
 
 Cu.import("resource://webapprt/modules/WebappRT.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
 // When WebappsHandler opens an install confirmation dialog for apps we install,
 // close it, which will be seen as the equivalent of cancelling the install.
 // This doesn't prevent us from installing those apps, as we listen for the same
 // notification as WebappsHandler and do the install ourselves.  It just
 // prevents the modal installation confirmation dialogs from hanging tests.
 Services.ww.registerNotification({
   observe: function(win, topic) {
@@ -37,53 +38,53 @@ Services.ww.registerNotification({
  * @param {Object} parameters
  *        The value to pass as the "parameters" argument to
  *        mozIDOMApplicationRegistry.install, e.g., { receipts: ... }.
  *        Use undefined to pass nothing.
  * @param {Function} onBecome
  *        The callback to call once the transmogrification is complete.
  */
 function becomeWebapp(manifestURL, parameters, onBecome) {
-  function observeInstall(subj, topic, data) {
+  let observeInstall = Task.async(function*(subj, topic, data) {
     Services.obs.removeObserver(observeInstall, "webapps-ask-install");
 
     // Step 2: Configure the runtime session to represent the app.
     // We load DOMApplicationRegistry into a local scope to avoid appearing
     // to leak it.
 
     let scope = {};
     Cu.import("resource://gre/modules/Webapps.jsm", scope);
     Cu.import("resource://webapprt/modules/Startup.jsm", scope);
-    scope.DOMApplicationRegistry.confirmInstall(JSON.parse(data));
+    yield scope.DOMApplicationRegistry.confirmInstall(JSON.parse(data));
 
     let installRecord = JSON.parse(data);
     installRecord.mm = subj;
     installRecord.registryDir = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
     WebappRT.config = installRecord;
 
     let win = Services.wm.getMostRecentWindow("webapprt:webapp");
     if (!win) {
       win = Services.ww.openWindow(null,
                                    "chrome://webapprt/content/webapp.xul",
                                    "_blank",
                                    "chrome,dialog=no,resizable,scrollbars,centerscreen",
                                    null);
     }
 
-    let promise = scope.startup(win);
-
     // During chrome tests, we use the same window to load all the tests. We
     // need to change the buildID so that the permissions for the currently
     // tested application get installed.
     Services.prefs.setCharPref("webapprt.buildID", WebappRT.config.app.manifestURL);
 
-    // During tests, the webapps registry is already loaded.
-    // The Startup module needs to be notified when the webapps registry
-    // gets loaded, so we do that now.
+    // During tests, the webapps registry is already loaded,
+    // but SystemMessageInternal expects to be notified when the registry
+    // start and then when it's ready, so we do that now.
     Services.obs.notifyObservers(this, "webapps-registry-start", null);
+    Services.obs.notifyObservers(this, "webapps-registry-ready", null);
 
-    promise.then(onBecome);
-  }
+    yield scope.startup(win);
+    onBecome();
+  });
   Services.obs.addObserver(observeInstall, "webapps-ask-install", false);
 
   // Step 1: Install the app at the URL specified by the manifest.
   navigator.mozApps.install(manifestURL, parameters);
 }
--- a/webapprt/content/mochitest.xul
+++ b/webapprt/content/mochitest.xul
@@ -1,16 +1,17 @@
 <?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://global/skin/" type="text/css"?>
 
-<window windowtype="webapprt:mochitest"
+<window id="browserTestHarness"
+        windowtype="webapprt:mochitest"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
 <script type="application/javascript" src="chrome://webapprt/content/mochitest.js"/>
 
 <description value="WebappRT Test Shim"/>
 
 </window>