merge fx-team to mozilla-central a=merge
authorCarsten "Tomcat" Book <cbook@mozilla.com>
Tue, 06 Oct 2015 11:54:45 +0200
changeset 266255 3edc8d4a1e198314f5d7ebd2967b85842beef602
parent 266218 193b383efaa4abaa9fef955b33e9df6d45d10d53 (current diff)
parent 266254 8c261f0d4d3e47e04690824c953b5f8471ddd73e (diff)
child 266256 89732fcdb0baca70e8b7a25a2725117113f0db80
push id66149
push usercbook@mozilla.com
push dateTue, 06 Oct 2015 10:21:56 +0000
treeherdermozilla-inbound@2722b65059df [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone44.0a1
first release with
nightly linux32
3edc8d4a1e19 / 44.0a1 / 20151006030231 / files
nightly linux64
3edc8d4a1e19 / 44.0a1 / 20151006030231 / files
nightly mac
3edc8d4a1e19 / 44.0a1 / 20151006030231 / files
nightly win32
3edc8d4a1e19 / 44.0a1 / 20151006030231 / files
nightly win64
3edc8d4a1e19 / 44.0a1 / 20151006030231 / 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 mozilla-central a=merge
browser/base/content/test/general/browser_notification_tab_switching.js
browser/base/content/test/general/file_dom_notifications.html
browser/themes/linux/Push-16.png
browser/themes/linux/Push-64.png
browser/themes/linux/notification-16.png
browser/themes/linux/notification-64.png
browser/themes/osx/Push-16.png
browser/themes/osx/Push-16@2x.png
browser/themes/osx/Push-64.png
browser/themes/osx/Push-64@2x.png
browser/themes/osx/notification-16.png
browser/themes/osx/notification-16@2x.png
browser/themes/osx/notification-64.png
browser/themes/osx/notification-64@2x.png
browser/themes/windows/Push-16.png
browser/themes/windows/Push-64.png
browser/themes/windows/notification-16.png
browser/themes/windows/notification-64.png
--- a/CLOBBER
+++ b/CLOBBER
@@ -17,9 +17,9 @@
 #
 # Modifying this file will now automatically clobber the buildbot machines \o/
 #
 
 # Are you updating CLOBBER because you think it's needed for your WebIDL
 # changes to stick? As of bug 928195, this shouldn't be necessary! Please
 # don't change CLOBBER for WebIDL changes any more.
 
-Bug 1182727 - Changed toolchain, needs clobber again :(
+Bug 1210755 - Android build: Switch to Android 6.0 SDK / API 23
--- a/browser/base/content/browser.xul
+++ b/browser/base/content/browser.xul
@@ -708,17 +708,16 @@
                      ontextreverted="return this.handleRevert();"
                      pageproxystate="invalid"
                      onfocus="document.getElementById('identity-box').style.MozUserFocus= 'normal'"
                      onblur="setTimeout(() => { document.getElementById('identity-box').style.MozUserFocus = ''; }, 0);">
               <box id="notification-popup-box" hidden="true" align="center">
                 <image id="default-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="identity-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="geo-notification-icon" class="notification-anchor-icon" role="button"/>
-                <image id="push-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="addons-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="indexedDB-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="login-fill-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="password-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="webapps-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="plugins-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="web-notifications-notification-icon" class="notification-anchor-icon" role="button"/>
                 <image id="webRTC-shareDevices-notification-icon" class="notification-anchor-icon" role="button"/>
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -1,16 +1,17 @@
 # to be included inside a popupset element
 
     <panel id="notification-popup"
            type="arrow"
            footertype="promobox"
            position="after_start"
            hidden="true"
            orient="vertical"
+           noautofocus="true"
            role="alert"/>
 
     <popupnotification id="webRTC-shareDevices-notification" hidden="true">
       <popupnotificationcontent id="webRTC-selectCamera" orient="vertical">
         <label value="&getUserMedia.selectCamera.label;"
                accesskey="&getUserMedia.selectCamera.accesskey;"
                control="webRTC-selectCamera-menulist"/>
         <menulist id="webRTC-selectCamera-menulist">
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/alerts/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+support-files =
+  file_dom_notifications.html
+
+[browser_notification_open_settings.js]
+[browser_notification_tab_switching.js]
+skip-if = buildapp == 'mulet' || e10s # Bug 1100662 - content access causing uncaught exception - Error: cannot ipc non-cpow object at chrome://mochitests/content/browser/browser/base/content/test/general/browser_notification_tab_switching.js:32 (or in RemoteAddonsChild.jsm)
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/alerts/browser_notification_open_settings.js
@@ -0,0 +1,18 @@
+"use strict";
+
+add_task(function* test_settingsOpen() {
+  info("Opening a dummy tab so openPreferences=>switchToTabHavingURI doesn't use the blank tab.");
+  yield BrowserTestUtils.withNewTab({
+    gBrowser,
+    url: "about:robots"
+  }, function* dummyTabTask(aBrowser) {
+    let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, "about:preferences#content");
+    info("simulate a notifications-open-settings notification");
+    let uri = NetUtil.newURI("https://example.com");
+    let principal = Services.scriptSecurityManager.createCodebasePrincipal(uri, {});
+    Services.obs.notifyObservers(principal, "notifications-open-settings", null);
+    let tab = yield tabPromise;
+    ok(tab, "The notification settings tab opened");
+    BrowserTestUtils.removeTab(tab);
+  });
+});
rename from browser/base/content/test/general/browser_notification_tab_switching.js
rename to browser/base/content/test/alerts/browser_notification_tab_switching.js
--- a/browser/base/content/test/general/browser_notification_tab_switching.js
+++ b/browser/base/content/test/alerts/browser_notification_tab_switching.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
 "use strict";
 
 var tab;
 var notification;
-var notificationURL = "http://example.org/browser/browser/base/content/test/general/file_dom_notifications.html";
+var notificationURL = "http://example.org/browser/browser/base/content/test/alerts/file_dom_notifications.html";
 var newWindowOpenedFromTab;
 
 function test () {
   waitForExplicitFinish();
 
   let pm = Services.perms;
   registerCleanupFunction(function() {
     pm.remove(makeURI(notificationURL), "desktop-notification");
@@ -53,17 +53,17 @@ function onLoad() {
     waitUntilNewWindowHasFocus();
   });
 }
 
 function onAlertShowing() {
   info("Notification alert showing");
   notification.removeEventListener("show", onAlertShowing);
 
-  let alertWindow = findChromeWindowByURI("chrome://global/content/alerts/alert.xul");
+  let alertWindow = Services.wm.getMostRecentWindow("alert:alert");
   if (!alertWindow) {
     todo(false, "Notifications don't use XUL windows on all platforms.");
     notification.close();
     newWindowOpenedFromTab.close();
     finish();
     return;
   }
   gBrowser.tabContainer.addEventListener("TabSelect", onTabSelect);
rename from browser/base/content/test/general/file_dom_notifications.html
rename to browser/base/content/test/alerts/file_dom_notifications.html
--- a/browser/base/content/test/general/file_dom_notifications.html
+++ b/browser/base/content/test/alerts/file_dom_notifications.html
@@ -1,10 +1,11 @@
 <html>
 <head>
+<meta charset="utf-8">
 <script>
 "use strict";
 
 function showNotification1() {
   var options = {
       dir: undefined,
       lang: undefined,
       body: "Test body",
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -57,17 +57,16 @@ support-files =
   file_mixedContentFromOnunload_test2.html
   file_mixedContentFramesOnHttp.html
   file_mixedPassiveContent.html
   file_bug970276_popup1.html
   file_bug970276_popup2.html
   file_bug970276_favicon1.ico
   file_bug970276_favicon2.ico
   file_documentnavigation_frameset.html
-  file_dom_notifications.html
   file_double_close_tab.html
   file_favicon_change.html
   file_favicon_change_not_in_document.html
   file_fullscreen-window-open.html
   get_user_media.html
   head.js
   healthreport_pingData.js
   healthreport_testRemoteCommands.html
@@ -333,18 +332,16 @@ skip-if = os == "linux" # Linux: Intermi
 [browser_locationBarExternalLoad.js]
 [browser_menuButtonFitts.js]
 skip-if = os != "win" # The Fitts Law menu button is only supported on Windows (bug 969376)
 [browser_middleMouse_noJSPaste.js]
 [browser_minimize.js]
 skip-if = e10s # Bug 1100664 - test directly access content docShells (TypeError: gBrowser.docShell is null)
 [browser_mixedcontent_securityflags.js]
 tags = mcb
-[browser_notification_tab_switching.js]
-skip-if = buildapp == 'mulet' || e10s # Bug 1100662 - content access causing uncaught exception - Error: cannot ipc non-cpow object at chrome://mochitests/content/browser/browser/base/content/test/general/browser_notification_tab_switching.js:32 (or in RemoteAddonsChild.jsm)
 [browser_offlineQuotaNotification.js]
 skip-if = buildapp == 'mulet' || e10s # Bug 1093603 - test breaks with PopupNotifications.panel.firstElementChild is null
 [browser_overflowScroll.js]
 [browser_pageInfo.js]
 skip-if = buildapp == 'mulet'
 [browser_page_style_menu.js]
 [browser_parsable_css.js]
 [browser_parsable_script.js]
--- a/browser/base/content/test/general/head.js
+++ b/browser/base/content/test/general/head.js
@@ -54,26 +54,16 @@ function whenDelayedStartupFinished(aWin
   Services.obs.addObserver(function observer(aSubject, aTopic) {
     if (aWindow == aSubject) {
       Services.obs.removeObserver(observer, aTopic);
       executeSoon(aCallback);
     }
   }, "browser-delayed-startup-finished", false);
 }
 
-function findChromeWindowByURI(aURI) {
-  let windows = Services.wm.getEnumerator(null);
-  while (windows.hasMoreElements()) {
-    let win = windows.getNext();
-    if (win.location.href == aURI)
-      return win;
-  }
-  return null;
-}
-
 function updateTabContextMenu(tab) {
   let menu = document.getElementById("tabContextMenu");
   if (!tab)
     tab = gBrowser.selectedTab;
   var evt = new Event("");
   tab.dispatchEvent(evt);
   menu.openPopup(tab, "end_after", 0, 0, true, false, evt);
   is(TabContextMenu.contextTab, tab, "TabContextMenu context is the expected tab");
--- a/browser/base/moz.build
+++ b/browser/base/moz.build
@@ -10,16 +10,17 @@ MOCHITEST_MANIFESTS += [
     'content/test/general/mochitest.ini',
 ]
 
 MOCHITEST_CHROME_MANIFESTS += [
     'content/test/chrome/chrome.ini',
 ]
 
 BROWSER_CHROME_MANIFESTS += [
+    'content/test/alerts/browser.ini',
     'content/test/chat/browser.ini',
     'content/test/general/browser.ini',
     'content/test/newtab/browser.ini',
     'content/test/plugins/browser.ini',
     'content/test/popupNotifications/browser.ini',
     'content/test/referrer/browser.ini',
     'content/test/social/browser.ini',
 ]
--- a/browser/components/loop/content/js/conversationAppStore.js
+++ b/browser/components/loop/content/js/conversationAppStore.js
@@ -142,17 +142,18 @@ loop.store.ConversationAppStore = (funct
      */
     LoopHangupNowHandler: function() {
       switch (this.getStoreState().windowType) {
         case "incoming":
         case "outgoing":
           this._dispatcher.dispatch(new loop.shared.actions.HangupCall());
           break;
         case "room":
-          if (this._activeRoomStore.getStoreState().used) {
+          if (this._activeRoomStore.getStoreState().used &&
+              !this._storeState.showFeedbackForm) {
             this._dispatcher.dispatch(new loop.shared.actions.LeaveRoom());
           } else {
             loop.shared.mixins.WindowCloseMixin.closeWindow();
           }
           break;
         default:
           loop.shared.mixins.WindowCloseMixin.closeWindow();
           break;
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -399,16 +399,18 @@ loop.conversationViews = (function(mozL1
           }
           return mozL10n.get("generic_contact_unavailable_title");
         case FAILURE_DETAILS.NO_MEDIA:
         case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
           return mozL10n.get("no_media_failure_message");
         case FAILURE_DETAILS.TOS_FAILURE:
           return mozL10n.get("tos_failure_message",
             { clientShortname: mozL10n.get("clientShortname2") });
+        case FAILURE_DETAILS.ICE_FAILED:
+          return mozL10n.get("ice_failure_message");
         default:
           return mozL10n.get("generic_failure_message");
       }
     },
 
     _renderExtraMessage: function() {
       if (this.props.extraMessage) {
         return React.createElement("p", {className: "failure-info-extra"}, this.props.extraMessage);
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -399,16 +399,18 @@ loop.conversationViews = (function(mozL1
           }
           return mozL10n.get("generic_contact_unavailable_title");
         case FAILURE_DETAILS.NO_MEDIA:
         case FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA:
           return mozL10n.get("no_media_failure_message");
         case FAILURE_DETAILS.TOS_FAILURE:
           return mozL10n.get("tos_failure_message",
             { clientShortname: mozL10n.get("clientShortname2") });
+        case FAILURE_DETAILS.ICE_FAILED:
+          return mozL10n.get("ice_failure_message");
         default:
           return mozL10n.get("generic_failure_message");
       }
     },
 
     _renderExtraMessage: function() {
       if (this.props.extraMessage) {
         return <p className="failure-info-extra">{this.props.extraMessage}</p>;
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -3,16 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var loop = loop || {};
 loop.roomViews = (function(mozL10n) {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
 
   /**
    * ActiveRoomStore mixin.
    * @type {Object}
@@ -94,24 +95,32 @@ loop.roomViews = (function(mozL10n) {
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     render: function() {
       var settingsMenuItems = [
         { id: "feedback" },
         { id: "help" }
       ];
+
+      var btnTitle;
+      if (this.props.failureReason === FAILURE_DETAILS.ICE_FAILED) {
+        btnTitle = mozL10n.get("retry_call_button");
+      } else {
+        btnTitle = mozL10n.get("rejoin_button");
+      }
+
       return (
         React.createElement("div", {className: "room-failure"}, 
           React.createElement(loop.conversationViews.FailureInfoView, {
             failureReason: this.props.failureReason}), 
           React.createElement("div", {className: "btn-group call-action-group"}, 
             React.createElement("button", {className: "btn btn-info btn-rejoin", 
                     onClick: this.handleRejoinCall}, 
-              mozL10n.get("rejoin_button")
+              btnTitle
             )
           ), 
           React.createElement(loop.shared.views.SettingsControlButton, {
             menuBelow: true, 
             menuItems: settingsMenuItems, 
             mozLoop: this.props.mozLoop})
         )
       );
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -3,16 +3,17 @@
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var loop = loop || {};
 loop.roomViews = (function(mozL10n) {
   "use strict";
 
   var ROOM_STATES = loop.store.ROOM_STATES;
   var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
+  var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
   var sharedActions = loop.shared.actions;
   var sharedMixins = loop.shared.mixins;
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
 
   /**
    * ActiveRoomStore mixin.
    * @type {Object}
@@ -94,24 +95,32 @@ loop.roomViews = (function(mozL10n) {
       this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
     },
 
     render: function() {
       var settingsMenuItems = [
         { id: "feedback" },
         { id: "help" }
       ];
+
+      var btnTitle;
+      if (this.props.failureReason === FAILURE_DETAILS.ICE_FAILED) {
+        btnTitle = mozL10n.get("retry_call_button");
+      } else {
+        btnTitle = mozL10n.get("rejoin_button");
+      }
+
       return (
         <div className="room-failure">
           <loop.conversationViews.FailureInfoView
             failureReason={this.props.failureReason} />
           <div className="btn-group call-action-group">
             <button className="btn btn-info btn-rejoin"
                     onClick={this.handleRejoinCall}>
-              {mozL10n.get("rejoin_button")}
+              {btnTitle}
             </button>
           </div>
           <loop.shared.views.SettingsControlButton
             menuBelow={true}
             menuItems={settingsMenuItems}
             mozLoop={this.props.mozLoop} />
         </div>
       );
--- a/browser/components/loop/content/shared/js/otSdkDriver.js
+++ b/browser/components/loop/content/shared/js/otSdkDriver.js
@@ -923,16 +923,23 @@ loop.OTSdkDriver = (function() {
         reason: FAILURE_DETAILS.MEDIA_DENIED
       }));
 
       delete this._mockPublisherEl;
     },
 
     _onOTException: function(event) {
       switch (event.code) {
+        case OT.ExceptionCodes.PUBLISHER_ICE_WORKFLOW_FAILED:
+        case OT.ExceptionCodes.SUBSCRIBER_ICE_WORKFLOW_FAILED:
+          this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
+            reason: FAILURE_DETAILS.ICE_FAILED
+          }));
+          this._notifyMetricsEvent("sdk.exception." + event.code);
+          break;
         case OT.ExceptionCodes.UNABLE_TO_PUBLISH:
           if (event.message === "GetUserMedia") {
             // We free up the publisher here in case the store wants to try
             // grabbing the media again.
             if (this.publisher) {
               this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
               this.publisher.destroy();
               delete this.publisher;
--- a/browser/components/loop/content/shared/js/utils.js
+++ b/browser/components/loop/content/shared/js/utils.js
@@ -79,17 +79,18 @@ var inChrome = typeof Components != "und
     UNABLE_TO_PUBLISH_MEDIA: "unable-to-publish-media",
     USER_UNAVAILABLE: "reason-user-unavailable",
     COULD_NOT_CONNECT: "reason-could-not-connect",
     NETWORK_DISCONNECTED: "reason-network-disconnected",
     EXPIRED_OR_INVALID: "reason-expired-or-invalid",
     // TOS_FAILURE reflects the sdk error code 1026:
     // https://tokbox.com/developer/sdks/js/reference/ExceptionEvent.html
     TOS_FAILURE: "reason-tos-failure",
-    UNKNOWN: "reason-unknown"
+    UNKNOWN: "reason-unknown",
+    ICE_FAILED: "reason-ice-failed"
   };
 
   var ROOM_INFO_FAILURES = {
     // There's no data available from the server.
     NO_DATA: "no_data",
     // WebCrypto is unsupported in this browser.
     WEB_CRYPTO_UNSUPPORTED: "web_crypto_unsupported",
     // The room is missing the crypto key information.
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -153,16 +153,18 @@ loop.standaloneRoomViews = (function(moz
         // XXX Bug 1166824 should provide a better string for this.
         case FAILURE_DETAILS.NO_MEDIA:
           return mozL10n.get("rooms_media_denied_message");
         case FAILURE_DETAILS.EXPIRED_OR_INVALID:
           return mozL10n.get("rooms_unavailable_notification_message");
         case FAILURE_DETAILS.TOS_FAILURE:
           return mozL10n.get("tos_failure_message",
             { clientShortname: mozL10n.get("clientShortname2") });
+        case FAILURE_DETAILS.ICE_FAILED:
+          return mozL10n.get("rooms_ice_failure_message");
         default:
           return mozL10n.get("status_error");
       }
     },
 
     /**
      * This renders a retry button if one is necessary.
      */
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -153,16 +153,18 @@ loop.standaloneRoomViews = (function(moz
         // XXX Bug 1166824 should provide a better string for this.
         case FAILURE_DETAILS.NO_MEDIA:
           return mozL10n.get("rooms_media_denied_message");
         case FAILURE_DETAILS.EXPIRED_OR_INVALID:
           return mozL10n.get("rooms_unavailable_notification_message");
         case FAILURE_DETAILS.TOS_FAILURE:
           return mozL10n.get("tos_failure_message",
             { clientShortname: mozL10n.get("clientShortname2") });
+        case FAILURE_DETAILS.ICE_FAILED:
+          return mozL10n.get("rooms_ice_failure_message");
         default:
           return mozL10n.get("status_error");
       }
     },
 
     /**
      * This renders a retry button if one is necessary.
      */
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -67,16 +67,17 @@ rooms_room_joined_label=Someone has join
 rooms_room_join_label=Join the conversation
 rooms_room_joined_own_conversation_label=Enjoy your conversation
 rooms_already_joined=You're already in this conversation.
 rooms_display_name_guest=Guest
 rooms_unavailable_notification_message=Sorry, you cannot join this conversation. The link may be expired or invalid.
 rooms_media_denied_message=We could not get access to your microphone or camera. Please reload the page to try again.
 room_information_failure_not_available=No information about this conversation is available. Please request a new link from the person who sent it to you.
 room_information_failure_unsupported_browser=Your browser cannot access any information about this conversation. Please make sure you're using the latest version.
+rooms_ice_failure_message=Connection failed. Your firewall may be blocking calls.
 
 ## LOCALIZATION_NOTE(rooms_read_while_wait_offer): This string is followed by a
 # tile/offer image and title that are provided by a separate service that has
 # localized content.
 rooms_read_while_wait_offer=Want something to read while you wait?
 
 ## LOCALIZATION_NOTE(standalone_title_with_room_name): {{roomName}} will be replaced
 ## by the name of the conversation and {{clientShortname}} will be
--- a/browser/components/loop/test/desktop-local/conversationAppStore_test.js
+++ b/browser/components/loop/test/desktop-local/conversationAppStore_test.js
@@ -245,16 +245,29 @@ describe("loop.store.ConversationAppStor
 
         store.LoopHangupNowHandler();
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch, new sharedActions.LeaveRoom());
         sinon.assert.notCalled(loop.shared.mixins.WindowCloseMixin.closeWindow);
       });
 
+      it("should close the window when a room was used and it showed feedback", function() {
+        store.setStoreState({
+          showFeedbackForm: true,
+          windowType: "room"
+        });
+        roomUsed = true;
+
+        store.LoopHangupNowHandler();
+
+        sinon.assert.notCalled(dispatcher.dispatch);
+        sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
+      });
+
       it("should close the window when a room was not used", function() {
         store.setStoreState({ windowType: "room" });
 
         store.LoopHangupNowHandler();
 
         sinon.assert.notCalled(dispatcher.dispatch);
         sinon.assert.calledOnce(loop.shared.mixins.WindowCloseMixin.closeWindow);
       });
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -346,16 +346,26 @@ describe("loop.conversationViews", funct
         extraFailureMessage: "Fake failure message",
         failureReason: FAILURE_DETAILS.UNKNOWN
       });
 
       var extraFailureMessage = view.getDOMNode().querySelector(".failure-info-extra-failure");
 
       expect(extraFailureMessage.textContent).eql("Fake failure message");
     });
+
+    it("should display an ICE failure message", function() {
+      view = mountTestComponent({
+        failureReason: FAILURE_DETAILS.ICE_FAILED
+      });
+
+      var message = view.getDOMNode().querySelector(".failure-info-message");
+
+      expect(message.textContent).eql("ice_failure_message");
+    });
   });
 
   describe("DirectCallFailureView", function() {
     var fakeAudio, composeCallUrlEmail;
 
     var fakeContact = {email: [{value: "test@test.tld"}]};
 
     function mountTestComponent(options) {
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -137,17 +137,17 @@ describe("loop.roomViews", function () {
   });
 
   describe("RoomFailureView", function() {
     var fakeAudio;
 
     function mountTestComponent(props) {
       props = _.extend({
         dispatcher: dispatcher,
-        failureReason: FAILURE_DETAILS.UNKNOWN,
+        failureReason: props && props.failureReason || FAILURE_DETAILS.UNKNOWN,
         mozLoop: fakeMozLoop
       });
       return TestUtils.renderIntoDocument(
         React.createElement(loop.roomViews.RoomFailureView, props));
     }
 
     beforeEach(function() {
       fakeAudio = {
@@ -172,16 +172,26 @@ describe("loop.roomViews", function () {
 
       React.addons.TestUtils.Simulate.click(rejoinBtn);
 
       sinon.assert.calledOnce(dispatcher.dispatch);
       sinon.assert.calledWithExactly(dispatcher.dispatch,
         new sharedActions.JoinRoom());
     });
 
+    it("should render retry button when an ice failure is dispatched", function() {
+      view = mountTestComponent({
+        failureReason: FAILURE_DETAILS.ICE_FAILED
+      });
+
+      var retryBtn = view.getDOMNode().querySelector(".btn-rejoin");
+
+      expect(retryBtn.textContent).eql("retry_call_button");
+    });
+
     it("should play a failure sound, once", function() {
       view = mountTestComponent();
 
       sinon.assert.calledOnce(fakeMozLoop.getAudioBlob);
       sinon.assert.calledWithExactly(fakeMozLoop.getAudioBlob,
                                      "failure", sinon.match.func);
       sinon.assert.calledOnce(fakeAudio.play);
       expect(fakeAudio.loop).to.equal(false);
--- a/browser/components/loop/test/shared/otSdkDriver_test.js
+++ b/browser/components/loop/test/shared/otSdkDriver_test.js
@@ -1656,16 +1656,61 @@ describe("loop.OTSdkDriver", function ()
               event: "sdk.exception." + OT.ExceptionCodes.TERMS_OF_SERVICE_FAILURE,
               state: "starting",
               connections: 0,
               sendStreams: 0,
               recvStreams: 0
             }));
         });
       });
+
+      describe("ICE failed", function() {
+        it("should dispatch a ConnectionFailure action (Publisher)", function() {
+          sdk.trigger("exception", {
+            code: OT.ExceptionCodes.PUBLISHER_ICE_WORKFLOW_FAILED,
+            message: "ICE failed"
+          });
+
+          sinon.assert.calledTwice(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.ConnectionFailure({
+              reason: FAILURE_DETAILS.ICE_FAILED
+            }));
+        });
+
+        it("should dispatch a ConnectionFailure action (Subscriber)", function() {
+          sdk.trigger("exception", {
+            code: OT.ExceptionCodes.SUBSCRIBER_ICE_WORKFLOW_FAILED,
+            message: "ICE failed"
+          });
+
+          sinon.assert.calledTwice(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.ConnectionFailure({
+              reason: FAILURE_DETAILS.ICE_FAILED
+            }));
+        });
+
+        it("should notify metrics", function() {
+          sdk.trigger("exception", {
+            code: OT.ExceptionCodes.PUBLISHER_ICE_WORKFLOW_FAILED,
+            message: "ICE failed"
+          });
+
+          sinon.assert.calledTwice(dispatcher.dispatch);
+          sinon.assert.calledWithExactly(dispatcher.dispatch,
+            new sharedActions.ConnectionStatus({
+              event: "sdk.exception." + OT.ExceptionCodes.PUBLISHER_ICE_WORKFLOW_FAILED,
+              state: "starting",
+              connections: 0,
+              sendStreams: 0,
+              recvStreams: 0
+            }));
+        });
+      });
     });
   });
 
   describe("Events: screenshare:", function() {
     var videoElement;
 
     beforeEach(function() {
       driver.connectSession(sessionData);
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -647,16 +647,27 @@ describe("loop.standaloneRoomViews", fun
 
       describe("Failed room message", function() {
         it("should display the StandaloneRoomFailureView", function() {
           activeRoomStore.setStoreState({ roomState: ROOM_STATES.FAILED });
 
           TestUtils.findRenderedComponentWithType(view,
             loop.standaloneRoomViews.StandaloneRoomFailureView);
         });
+
+        it("should display ICE failure message", function() {
+          activeRoomStore.setStoreState({
+            roomState: ROOM_STATES.FAILED,
+            failureReason: FAILURE_DETAILS.ICE_FAILED
+          });
+
+          var ice_failed_message = view.getDOMNode().querySelector(".failed-room-message").textContent;
+          expect(ice_failed_message).eql("rooms_ice_failure_message");
+          expect(view.getDOMNode().querySelector(".btn-info")).not.eql(null);
+        });
       });
 
       describe("Join button", function() {
         function getJoinButton(elem) {
           return elem.getDOMNode().querySelector(".btn-join");
         }
 
         it("should render the Join button when room isn't active", function() {
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -271,16 +271,19 @@ BrowserGlue.prototype = {
     Cu.import("resource://services-sync/main.js");
     Weave.Service.scheduler.delayedAutoConnect(delay);
   },
 #endif
 
   // nsIObserver implementation
   observe: function BG_observe(subject, topic, data) {
     switch (topic) {
+      case "notifications-open-settings":
+        this._openPreferences("content");
+        break;
       case "prefservice:after-app-defaults":
         this._onAppDefaults();
         break;
       case "final-ui-startup":
         this._finalUIStartup();
         break;
       case "browser-delayed-startup-finished":
         this._onFirstWindowLoaded(subject);
@@ -586,16 +589,17 @@ BrowserGlue.prototype = {
     if (Services.search.isInitialized) {
       Services.search.defaultEngine = Services.search.currentEngine;
     }
   },
 
   // initialization (called on application startup) 
   _init: function BG__init() {
     let os = Services.obs;
+    os.addObserver(this, "notifications-open-settings", false);
     os.addObserver(this, "prefservice:after-app-defaults", false);
     os.addObserver(this, "final-ui-startup", false);
     os.addObserver(this, "browser-delayed-startup-finished", false);
     os.addObserver(this, "sessionstore-windows-restored", false);
     os.addObserver(this, "browser:purge-session-history", false);
     os.addObserver(this, "quit-application-requested", false);
     os.addObserver(this, "quit-application-granted", false);
 #ifdef OBSERVE_LASTWINDOW_CLOSE_TOPICS
@@ -634,16 +638,17 @@ BrowserGlue.prototype = {
     ExtensionManagement.registerScript("chrome://browser/content/ext-windows.js");
 
     this._flashHangCount = 0;
   },
 
   // cleanup (called on application shutdown)
   _dispose: function BG__dispose() {
     let os = Services.obs;
+    os.removeObserver(this, "notifications-open-settings");
     os.removeObserver(this, "prefservice:after-app-defaults");
     os.removeObserver(this, "final-ui-startup");
     os.removeObserver(this, "sessionstore-windows-restored");
     os.removeObserver(this, "browser:purge-session-history");
     os.removeObserver(this, "quit-application-requested");
     os.removeObserver(this, "quit-application-granted");
     os.removeObserver(this, "restart-in-safe-mode");
 #ifdef OBSERVE_LASTWINDOW_CLOSE_TOPICS
@@ -2356,16 +2361,29 @@ BrowserGlue.prototype = {
     } catch(ex) {
       Cu.reportError(ex);
     } finally {
       Services.prefs.setIntPref(SMART_BOOKMARKS_PREF, SMART_BOOKMARKS_VERSION);
       Services.prefs.savePrefFile(null);
     }
   }),
 
+  /**
+   * Open preferences even if there are no open windows.
+   */
+  _openPreferences(...args) {
+    if (Services.appShell.hiddenDOMWindow.openPreferences) {
+      Services.appShell.hiddenDOMWindow.openPreferences(...args);
+      return;
+    }
+
+    let chromeWindow = RecentWindow.getMostRecentBrowserWindow();
+    chromeWindow.openPreferences(...args);
+  },
+
 #ifdef MOZ_SERVICES_SYNC
   /**
    * Called as an observer when Sync's "display URI" notification is fired.
    *
    * We open the received URI in a background tab.
    *
    * Eventually, this will likely be replaced by a more robust tab syncing
    * feature. This functionality is considered somewhat evil by UX because it
@@ -2529,48 +2547,16 @@ ContentPermissionPrompt.prototype = {
     if (!aOptions)
       aOptions = {};
     aOptions.displayURI = requestPrincipal.URI;
 
     return chromeWin.PopupNotifications.show(browser, aNotificationId, aMessage, aAnchorId,
                                              mainAction, secondaryActions, aOptions);
   },
 
-  _promptPush : function(aRequest) {
-    var message = gBrowserBundle.GetStringFromName("push.enablePush2");
-
-    var actions = [
-    {
-      stringId: "push.alwaysAllow",
-      action: Ci.nsIPermissionManager.ALLOW_ACTION,
-      expireType: null,
-      callback: function() {}
-    },
-    {
-      stringId: "push.allowForSession",
-      action: Ci.nsIPermissionManager.ALLOW_ACTION,
-      expireType: Ci.nsIPermissionManager.EXPIRE_SESSION,
-      callback: function() {}
-    },
-    {
-      stringId: "push.alwaysBlock",
-      action: Ci.nsIPermissionManager.DENY_ACTION,
-      expireType: null,
-      callback: function() {}
-    }]
-
-    var options = {
-      learnMoreURL: Services.urlFormatter.formatURLPref("browser.push.warning.infoURL"),
-    };
-
-    this._showPrompt(aRequest, message, "push", actions, "push",
-                     "push-notification-icon", options);
-
-  },
-
   _promptGeo : function(aRequest) {
     var secHistogram = Services.telemetry.getHistogramById("SECURITY_UI");
 
     var message;
 
     // Share location action.
     var actions = [{
       stringId: "geolocation.shareLocation",
@@ -2616,38 +2602,36 @@ ContentPermissionPrompt.prototype = {
                      "geo-notification-icon", options);
   },
 
   _promptWebNotifications : function(aRequest) {
     var message = gBrowserBundle.GetStringFromName("webNotifications.showFromSite2");
 
     var actions = [
       {
-        stringId: "webNotifications.showForSession",
-        action: Ci.nsIPermissionManager.ALLOW_ACTION,
-        expireType: Ci.nsIPermissionManager.EXPIRE_SESSION,
-        callback: function() {},
-      },
-      {
         stringId: "webNotifications.alwaysShow",
         action: Ci.nsIPermissionManager.ALLOW_ACTION,
         expireType: null,
         callback: function() {},
       },
       {
         stringId: "webNotifications.neverShow",
         action: Ci.nsIPermissionManager.DENY_ACTION,
         expireType: null,
         callback: function() {},
       },
     ];
 
+    var options = {
+      learnMoreURL: Services.urlFormatter.formatURLPref("browser.push.warning.infoURL"),
+    };
+
     this._showPrompt(aRequest, message, "desktop-notification", actions,
                      "web-notifications",
-                     "web-notifications-notification-icon", null);
+                     "web-notifications-notification-icon", options);
   },
 
   _promptPointerLock: function CPP_promtPointerLock(aRequest, autoAllow) {
     let message = gBrowserBundle.GetStringFromName(autoAllow ?
                                   "pointerLock.autoLock.title3" : "pointerLock.title3");
 
     // If this is an autoAllow info prompt, offer no actions.
     // _showPrompt() will allow the request when it's dismissed.
@@ -2708,17 +2692,16 @@ ContentPermissionPrompt.prototype = {
       request.cancel();
       return;
     }
     let perm = types.queryElementAt(0, Ci.nsIContentPermissionType);
 
     const kFeatureKeys = { "geolocation" : "geo",
                            "desktop-notification" : "desktop-notification",
                            "pointerLock" : "pointerLock",
-                           "push" : "push"
                          };
 
     // Make sure that we support the request.
     if (!(perm.type in kFeatureKeys)) {
       return;
     }
 
     var requestingPrincipal = request.principal;
@@ -2759,19 +2742,16 @@ ContentPermissionPrompt.prototype = {
       this._promptGeo(request);
       break;
     case "desktop-notification":
       this._promptWebNotifications(request);
       break;
     case "pointerLock":
       this._promptPointerLock(request, autoAllow);
       break;
-    case "push":
-      this._promptPush(request);
-      break;
     }
   },
 
 };
 
 var DefaultBrowserCheck = {
   get OPTIONPOPUP() { return "defaultBrowserNotificationPopup" },
   _setAsDefaultTimer: null,
--- a/browser/components/preferences/aboutPermissions.js
+++ b/browser/components/preferences/aboutPermissions.js
@@ -41,17 +41,17 @@ var gVisitStmt = gPlacesDatabase.createA
                   "SELECT SUM(visit_count) AS count " +
                   "FROM moz_places " +
                   "WHERE rev_host = :rev_host");
 
 /**
  * Permission types that should be tested with testExactPermission, as opposed
  * to testPermission. This is based on what consumers use to test these permissions.
  */
-var TEST_EXACT_PERM_TYPES = ["geo", "camera", "microphone"];
+var TEST_EXACT_PERM_TYPES = ["geo", "camera", "microphone", "desktop-notification"];
 
 /**
  * Site object represents a single site, uniquely identified by a principal.
  */
 function Site(principal) {
   this.principal = principal;
   this.listitem = null;
 }
@@ -316,26 +316,19 @@ var PermissionDefaults = {
     }
     return this.ALLOW;
   },
   set popup(aValue) {
     let value = (aValue == this.DENY);
     Services.prefs.setBoolPref("dom.disable_open_during_load", value);
   },
 
-  get push() {
-    if (!Services.prefs.getBoolPref("dom.push.enabled")) {
-      return this.DENY;
-    }
+  get ["desktop-notification"]() {
     return this.UNKNOWN;
   },
-  set push(aValue) {
-    let value = (aValue != this.DENY);
-    Services.prefs.setBoolPref("dom.push.enabled", value);
-  },
   get camera() {
     return this.UNKNOWN;
   },
   get microphone() {
     return this.UNKNOWN;
   }
 };
 
@@ -379,27 +372,28 @@ var AboutPermissions = {
   /**
    * This reflects the permissions that we expose in the UI. These correspond
    * to permission type strings in the permission manager, PermissionDefaults,
    * and element ids in aboutPermissions.xul.
    *
    * Potential future additions: "sts/use", "sts/subd"
    */
   _supportedPermissions: ["password", "cookie", "geo", "indexedDB", "popup",
-                          "camera", "microphone", "push"],
+                          "camera", "microphone", "desktop-notification"],
 
   /**
    * Permissions that don't have a global "Allow" option.
    */
-  _noGlobalAllow: ["geo", "indexedDB", "camera", "microphone", "push"],
+  _noGlobalAllow: ["geo", "indexedDB", "camera", "microphone",
+                   "desktop-notification"],
 
   /**
    * Permissions that don't have a global "Deny" option.
    */
-  _noGlobalDeny: ["camera", "microphone"],
+  _noGlobalDeny: ["camera", "microphone", "desktop-notification"],
 
   _stringBundle: Services.strings.
                  createBundle("chrome://browser/locale/preferences/aboutPermissions.properties"),
 
   /**
    * Called on page load.
    */
   init: function() {
--- a/browser/components/preferences/aboutPermissions.xul
+++ b/browser/components/preferences/aboutPermissions.xul
@@ -234,31 +234,31 @@
                   <menuitem id="indexedDB-1" value="1" label="&permission.allow;"/>
                   <menuitem id="indexedDB-2" value="2" label="&permission.block;"/>
                 </menupopup>
               </menulist>
             </hbox>
           </vbox>
         </hbox>
 
-        <!-- Push Notifications -->
-        <hbox id="push-pref-item"
+        <!-- Notifications -->
+        <hbox id="desktop-notification-pref-item"
               class="pref-item" align="top">
-          <image class="pref-icon" type="push"/>
+          <image class="pref-icon" type="desktop-notification"/>
           <vbox>
-            <label class="pref-title" value="&push.label;"/>
+            <label class="pref-title" value="&desktop-notification.label;"/>
             <hbox align="center">
-              <menulist id="push-menulist"
+              <menulist id="desktop-notification-menulist"
                         class="pref-menulist"
-                        type="push"
+                        type="desktop-notification"
                         oncommand="AboutPermissions.onPermissionCommand(event);">
                 <menupopup>
-                  <menuitem id="push-0" value="0" label="&permission.alwaysAsk;"/>
-                  <menuitem id="push-1" value="1" label="&permission.allow;"/>
-                  <menuitem id="push-2" value="2" label="&permission.block;"/>
+                  <menuitem id="desktop-notification-0" value="0" label="&permission.alwaysAsk;"/>
+                  <menuitem id="desktop-notification-1" value="1" label="&permission.allow;"/>
+                  <menuitem id="desktop-notification-2" value="2" label="&permission.block;"/>
                 </menupopup>
               </menulist>
             </hbox>
           </vbox>
         </hbox>
       </vbox>
     </vbox>
   </hbox>
--- a/browser/components/preferences/in-content/tests/browser_permissions.js
+++ b/browser/components/preferences/in-content/tests/browser_permissions.js
@@ -23,17 +23,17 @@ const PERM_DENY = 2;
 // cookie specific permissions
 const PERM_FIRST_PARTY_ONLY = 9;
 
 // used to set permissions on test sites
 const TEST_PERMS = {
   "password": PERM_ALLOW,
   "cookie": PERM_ALLOW,
   "geo": PERM_UNKNOWN,
-  "push": PERM_DENY,
+  "desktop-notification": PERM_UNKNOWN,
   "indexedDB": PERM_UNKNOWN,
   "popup": PERM_DENY,
   "camera": PERM_UNKNOWN,
   "microphone": PERM_UNKNOWN
 };
 
 const NO_GLOBAL_ALLOW = [
   "geo",
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -114,19 +114,16 @@
           "anonid", "searchbar-stringbundle");</field>
       <field name="_textboxInitialized">false</field>
       <field name="_textbox">document.getAnonymousElementByAttribute(this,
           "anonid", "searchbar-textbox");</field>
       <field name="_engines">null</field>
       <field name="FormHistory" readonly="true">
         (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
       </field>
-      <field name="PlacesUtils" readonly="true">
-        (Components.utils.import("resource://gre/modules/PlacesUtils.jsm", {})).PlacesUtils;
-      </field>
 
       <property name="engines" readonly="true">
         <getter><![CDATA[
           if (!this._engines)
             this._engines = Services.search.getVisibleEngines();
           return this._engines;
         ]]></getter>
       </property>
@@ -1440,17 +1437,17 @@
           // Make the target button of the context menu reflect the current
           // search engine first. Doing this as opposed to rebuilding all the
           // one-off buttons avoids flicker.
           let button = document.getElementById("searchbar-engine-one-off-item-" +
             this._contextEngine.name.replace(/ /g, '-'));
           button.id = "searchbar-engine-one-off-item-" + currentEngine.name.replace(/ /g, '-');
           let uri = "chrome://browser/skin/search-engine-placeholder.png";
           if (currentEngine.iconURI)
-            uri = PlacesUtils.getImageURLForResolution(window, currentEngine.iconURI.spec);
+            uri = currentEngine.iconURI.spec;
           button.setAttribute("image", uri);
           button.setAttribute("tooltiptext", currentEngine.name);
           button.engine = currentEngine;
 
           Services.search.currentEngine = this._contextEngine;
         }
       ]]></handler>
 
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -370,33 +370,22 @@ geolocation.shareLocation=Share Location
 geolocation.shareLocation.accesskey=a
 geolocation.alwaysShareLocation=Always Share Location
 geolocation.alwaysShareLocation.accesskey=A
 geolocation.neverShareLocation=Never Share Location
 geolocation.neverShareLocation.accesskey=N
 geolocation.shareWithSite2=Would you like to share your location with this site?
 geolocation.shareWithFile2=Would you like to share your location with this file?
 
-webNotifications.showForSession=Show for this session
-webNotifications.showForSession.accesskey=s
 webNotifications.alwaysShow=Always Show Notifications
 webNotifications.alwaysShow.accesskey=A
 webNotifications.neverShow=Always Block Notifications
 webNotifications.neverShow.accesskey=N
 webNotifications.showFromSite2=Would you like to show notifications from this site?
 
-# Push Notifications
-push.allowForSession=Allow for Session
-push.allowForSession.accesskey=S
-push.alwaysAllow=Always Allow Push Notifications
-push.alwaysAllow.accesskey=A
-push.alwaysBlock=Always Block Push Notifications
-push.alwaysBlock.accesskey=B
-push.enablePush2=Would you like to allow Push Notifications for this site?
-
 # Pointer lock UI
 
 pointerLock.allow2=Hide pointer
 pointerLock.allow2.accesskey=H
 pointerLock.alwaysAllow=Always allow hiding
 pointerLock.alwaysAllow.accesskey=A
 pointerLock.neverAllow=Never allow hiding
 pointerLock.neverAllow.accesskey=N
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -293,16 +293,17 @@ call_timeout_notification_text=Your call
 retry_call_button=Retry
 cancel_button=Cancel
 rejoin_button=Rejoin Conversation
 
 cannot_start_call_session_not_ready=Can't start call, session is not ready.
 network_disconnected=The network connection terminated abruptly.
 connection_error_see_console_notification=Call failed; see console for details.
 no_media_failure_message=No camera or microphone found.
+ice_failure_message=Connection failed. Your firewall may be blocking calls.
 
 ## LOCALIZATION NOTE (legal_text_and_links3): In this item, don't translate the
 ## parts between {{..}} because these will be replaced with links with the labels
 ## from legal_text_tos and legal_text_privacy. clientShortname will be replaced
 ## by the brand name.
 legal_text_and_links3=By using {{clientShortname}} you agree to the {{terms_of_use}} \
   and {{privacy_notice}}.
 legal_text_tos = Terms of Use
--- a/browser/locales/en-US/chrome/browser/preferences/aboutPermissions.dtd
+++ b/browser/locales/en-US/chrome/browser/preferences/aboutPermissions.dtd
@@ -36,13 +36,13 @@
 <!ENTITY plugins.label                   "Plugins">
 
 <!-- LOCALIZATION NOTE (indexedDB.label): This is describing indexedDB storage
      using the same language used for the permIndexedDB string in browser/pageInfo.dtd -->
 <!ENTITY indexedDB.label                 "Maintain Offline Storage">
 
 <!ENTITY popup.label                     "Open Pop-up Windows">
 
-<!ENTITY push.label                      "Receive Push Notifications">
+<!ENTITY desktop-notification.label      "Show Notifications">
 <!ENTITY camera.label                    "Use the Camera">
 <!ENTITY microphone.label                "Use the Microphone">
 
 <!ENTITY focusSearch.key                 "f">
--- a/browser/locales/en-US/chrome/browser/sitePermissions.properties
+++ b/browser/locales/en-US/chrome/browser/sitePermissions.properties
@@ -12,9 +12,8 @@ permission.desktop-notification.label = 
 permission.image.label = Load Images
 permission.camera.label = Use the Camera
 permission.microphone.label = Use the Microphone
 permission.install.label = Install Add-ons
 permission.popup.label = Open Pop-up Windows
 permission.geo.label = Access Your Location
 permission.indexedDB.label = Maintain Offline Storage
 permission.pointerLock.label = Hide the Mouse Pointer
-permission.push.label = Receive Push Notifications
--- a/browser/modules/SitePermissions.jsm
+++ b/browser/modules/SitePermissions.jsm
@@ -156,17 +156,19 @@ var gPermissionObject = {
 
       if (Services.prefs.getIntPref("network.cookie.lifetimePolicy") == 2)
         return SitePermissions.SESSION;
 
       return SitePermissions.ALLOW;
     }
   },
 
-  "desktop-notification": {},
+  "desktop-notification": {
+    exactHostMatch: true
+  },
 
   "camera": {},
   "microphone": {},
 
   "popup": {
     getDefault: function () {
       return Services.prefs.getBoolPref("dom.disable_open_during_load") ?
                SitePermissions.BLOCK : SitePermissions.ALLOW;
@@ -183,14 +185,10 @@ var gPermissionObject = {
   "geo": {
     exactHostMatch: true
   },
 
   "indexedDB": {},
 
   "pointerLock": {
     exactHostMatch: true
-  },
-
-  "push": {
-    exactHostMatch: true
   }
 };
--- a/browser/modules/test/xpcshell/test_SitePermissions.js
+++ b/browser/modules/test/xpcshell/test_SitePermissions.js
@@ -3,12 +3,11 @@
  */
 "use strict";
 
 Components.utils.import("resource:///modules/SitePermissions.jsm");
 
 add_task(function* testPermissionsListing() {
   Assert.deepEqual(SitePermissions.listPermissions().sort(),
     ["camera","cookie","desktop-notification","geo","image",
-     "indexedDB","install","microphone","pointerLock","popup",
-     "push"],
+     "indexedDB","install","microphone","pointerLock","popup"],
     "Correct list of all permissions");
 });
deleted file mode 100644
index 082b177811eb58303ee74b744e924adaf7223ade..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 6e09ab9c32d021d9f1f4c937943b31be08cb87c8..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/linux/jar.mn
+++ b/browser/themes/linux/jar.mn
@@ -18,33 +18,29 @@ browser.jar:
   skin/classic/browser/actionicon-tab.png
 * skin/classic/browser/browser.css
 * skin/classic/browser/devedition.css
 * skin/classic/browser/browser-lightweightTheme.css
   skin/classic/browser/click-to-play-warning-stripes.png
   skin/classic/browser/content-contextmenu.svg
   skin/classic/browser/Geolocation-16.png
   skin/classic/browser/Geolocation-64.png
-  skin/classic/browser/Push-16.png
-  skin/classic/browser/Push-64.png
   skin/classic/browser/Info.png
   skin/classic/browser/menuPanel.png
   skin/classic/browser/menuPanel@2x.png
   skin/classic/browser/menuPanel-customize.png
   skin/classic/browser/menuPanel-customize@2x.png
   skin/classic/browser/menuPanel-exit.png
   skin/classic/browser/menuPanel-exit@2x.png
   skin/classic/browser/menuPanel-help.png
   skin/classic/browser/menuPanel-help@2x.png
   skin/classic/browser/menuPanel-small.png
   skin/classic/browser/menuPanel-small@2x.png
   skin/classic/browser/monitor.png
   skin/classic/browser/monitor_16-10.png
-  skin/classic/browser/notification-16.png
-  skin/classic/browser/notification-64.png
 * skin/classic/browser/pageInfo.css
   skin/classic/browser/pageInfo.png
   skin/classic/browser/page-livemarks.png
   skin/classic/browser/pointerLock-16.png
   skin/classic/browser/pointerLock-64.png
   skin/classic/browser/Privacy-16.png
   skin/classic/browser/privatebrowsing-mask.png
   skin/classic/browser/reload-stop-go.png
deleted file mode 100644
index 6b2df734137b83b58f899252907e52ebde657bd6..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index a01d0ab7764fd0766d4b38208e7e50ce996aba3a..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/linux/preferences/aboutPermissions.css
+++ b/browser/themes/linux/preferences/aboutPermissions.css
@@ -89,18 +89,18 @@
   list-style-image: url(chrome://mozapps/skin/passwordmgr/key-64.png);
 }
 .pref-icon[type="cookie"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 .pref-icon[type="geo"] {
   list-style-image: url(chrome://browser/skin/Geolocation-64.png);
 }
-.pref-icon[type="push"] {
-  list-style-image: url(chrome://browser/skin/Push-64.png);
+.pref-icon[type="desktop-notification"] {
+  list-style-image: url(chrome://browser/skin/web-notifications-icon.svg);
 }
 .pref-icon[type="indexedDB"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 .pref-icon[type="install"] {
   list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png);
 }
 .pref-icon[type="popup"] {
deleted file mode 100644
index 54ef8f8eae7221735fe774a2d6cfc4c68c1e342e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 314089368457780f64af9b7107edfda1d8f37ebc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 099b9c76f355237c3ae163f5c17845bb2cf36c6c..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 411db1a5a455401d5df6a5fd3bca39bd3b3bca77..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/osx/jar.mn
+++ b/browser/themes/osx/jar.mn
@@ -20,31 +20,23 @@ browser.jar:
 * skin/classic/browser/devedition.css
 * skin/classic/browser/browser-lightweightTheme.css
   skin/classic/browser/click-to-play-warning-stripes.png
   skin/classic/browser/content-contextmenu.svg
   skin/classic/browser/Geolocation-16.png
   skin/classic/browser/Geolocation-16@2x.png
   skin/classic/browser/Geolocation-64.png
   skin/classic/browser/Geolocation-64@2x.png
-  skin/classic/browser/Push-16.png
-  skin/classic/browser/Push-16@2x.png
-  skin/classic/browser/Push-64.png
-  skin/classic/browser/Push-64@2x.png
   skin/classic/browser/Info.png
   skin/classic/browser/keyhole-circle.png
   skin/classic/browser/keyhole-circle@2x.png
   skin/classic/browser/KUI-background.png
   skin/classic/browser/subtle-pattern.png
   skin/classic/browser/menu-back.png
   skin/classic/browser/menu-forward.png
-  skin/classic/browser/notification-16.png
-  skin/classic/browser/notification-16@2x.png
-  skin/classic/browser/notification-64.png
-  skin/classic/browser/notification-64@2x.png
   skin/classic/browser/menuPanel.png
   skin/classic/browser/menuPanel@2x.png
   skin/classic/browser/menuPanel-customize.png
   skin/classic/browser/menuPanel-customize@2x.png
   skin/classic/browser/menuPanel-exit.png
   skin/classic/browser/menuPanel-exit@2x.png
   skin/classic/browser/menuPanel-help.png
   skin/classic/browser/menuPanel-help@2x.png
deleted file mode 100644
index 6b2df734137b83b58f899252907e52ebde657bd6..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 21818c847e9a55288e39556fb5f35c7d94c7d18f..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index a01d0ab7764fd0766d4b38208e7e50ce996aba3a..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 9adb75fbffca924af685b9f14af4a2ef3519b30b..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/osx/preferences/aboutPermissions.css
+++ b/browser/themes/osx/preferences/aboutPermissions.css
@@ -99,18 +99,18 @@
   list-style-image: url(chrome://mozapps/skin/passwordmgr/key-64.png);
 }
 .pref-icon[type="cookie"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 .pref-icon[type="geo"] {
   list-style-image: url(chrome://browser/skin/Geolocation-64.png);
 }
-.pref-icon[type="push"] {
-  list-style-image: url(chrome://browser/skin/Push-64.png);
+.pref-icon[type="desktop-notification"] {
+  list-style-image: url(chrome://browser/skin/web-notifications-icon.svg);
 }
 .pref-icon[type="indexedDB"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 .pref-icon[type="install"] {
   list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png);
 }
 .pref-icon[type="popup"] {
--- a/browser/themes/shared/jar.inc.mn
+++ b/browser/themes/shared/jar.inc.mn
@@ -122,16 +122,18 @@
   skin/classic/browser/reader-tour@2x.png                      (../shared/reader/reader-tour@2x.png)
   skin/classic/browser/readerMode.svg                          (../shared/reader/readerMode.svg)
   skin/classic/browser/notification-pluginNormal.png           (../shared/plugins/notification-pluginNormal.png)
   skin/classic/browser/notification-pluginNormal@2x.png        (../shared/plugins/notification-pluginNormal@2x.png)
   skin/classic/browser/notification-pluginAlert.png            (../shared/plugins/notification-pluginAlert.png)
   skin/classic/browser/notification-pluginAlert@2x.png         (../shared/plugins/notification-pluginAlert@2x.png)
   skin/classic/browser/notification-pluginBlocked.png          (../shared/plugins/notification-pluginBlocked.png)
   skin/classic/browser/notification-pluginBlocked@2x.png       (../shared/plugins/notification-pluginBlocked@2x.png)
+  skin/classic/browser/web-notifications-icon.svg              (../shared/web-notifications-icon.svg)
+  skin/classic/browser/web-notifications-tray.svg              (../shared/web-notifications-tray.svg)
   skin/classic/browser/webRTC-shareDevice-16.png               (../shared/webrtc/webRTC-shareDevice-16.png)
   skin/classic/browser/webRTC-shareDevice-16@2x.png            (../shared/webrtc/webRTC-shareDevice-16@2x.png)
   skin/classic/browser/webRTC-shareDevice-64.png               (../shared/webrtc/webRTC-shareDevice-64.png)
   skin/classic/browser/webRTC-shareDevice-64@2x.png            (../shared/webrtc/webRTC-shareDevice-64@2x.png)
   skin/classic/browser/webRTC-sharingDevice-16.png             (../shared/webrtc/webRTC-sharingDevice-16.png)
   skin/classic/browser/webRTC-sharingDevice-16@2x.png          (../shared/webrtc/webRTC-sharingDevice-16@2x.png)
   skin/classic/browser/webRTC-shareMicrophone-16.png           (../shared/webrtc/webRTC-shareMicrophone-16.png)
   skin/classic/browser/webRTC-shareMicrophone-16@2x.png        (../shared/webrtc/webRTC-shareMicrophone-16@2x.png)
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -9,20 +9,16 @@
   height: 64px;
   -moz-margin-end: 10px;
 }
 
 .popup-notification-icon[popupid="geolocation"] {
   list-style-image: url(chrome://browser/skin/Geolocation-64.png);
 }
 
-.popup-notification-icon[popupid="push"] {
-  list-style-image: url(chrome://browser/skin/Push-64.png);
-}
-
 .popup-notification-icon[popupid="xpinstall-disabled"],
 .popup-notification-icon[popupid="addon-install-blocked"],
 .popup-notification-icon[popupid="addon-install-origin-blocked"] {
   list-style-image: url(chrome://browser/skin/addons/addon-install-blocked.svg);
 }
 
 .popup-notification-icon[popupid="addon-progress"] {
   list-style-image: url(chrome://browser/skin/addons/addon-install-downloading.svg);
@@ -48,17 +44,17 @@
   list-style-image: url(chrome://browser/skin/addons/addon-install-restart.svg);
 }
 
 .popup-notification-icon[popupid="click-to-play-plugins"] {
   list-style-image: url(chrome://mozapps/skin/plugins/pluginBlocked-64.png);
 }
 
 .popup-notification-icon[popupid="web-notifications"] {
-  list-style-image: url(chrome://browser/skin/notification-64.png);
+  list-style-image: url(chrome://browser/skin/web-notifications-icon.svg);
 }
 
 .popup-notification-icon[popupid="indexedDB-permissions-prompt"],
 .popup-notification-icon[popupid*="offline-app-requested"],
 .popup-notification-icon[popupid="offline-app-usage"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 
@@ -137,20 +133,16 @@
   /* XXX: need HiDPI version */
 }
 
 .geo-notification-icon,
 #geo-notification-icon {
   list-style-image: url(chrome://browser/skin/Geolocation-16.png);
 }
 
-#push-notification-icon {
-  list-style-image: url(chrome://browser/skin/Push-16.png);
-}
-
 #addons-notification-icon {
   list-style-image: url(chrome://browser/skin/addons/addon-install-anchor.svg#default);
 }
 
 #addons-notification-icon:hover {
   list-style-image: url(chrome://browser/skin/addons/addon-install-anchor.svg#hover);
 }
 
@@ -249,17 +241,28 @@
 
 .webRTC-sharingScreen-notification-icon,
 #webRTC-sharingScreen-notification-icon {
   list-style-image: url(chrome://browser/skin/webRTC-sharingScreen-16.png);
 }
 
 .web-notifications-notification-icon,
 #web-notifications-notification-icon {
-  list-style-image: url(chrome://browser/skin/notification-16.png);
+  list-style-image: url(chrome://browser/skin/web-notifications-tray.svg);
+  -moz-image-region: rect(0, 16px, 16px, 0);
+}
+
+.web-notifications-notification-icon:hover,
+#web-notifications-notification-icon:hover {
+  -moz-image-region: rect(0, 32px, 16px, 16px);
+}
+
+.web-notifications-notification-icon:hover:active,
+#web-notifications-notification-icon:hover:active {
+  -moz-image-region: rect(0, 48px, 16px, 32px);
 }
 
 .pointerLock-notification-icon,
 #pointerLock-notification-icon {
   list-style-image: url(chrome://browser/skin/pointerLock-16.png);
 }
 
 .translate-notification-icon,
@@ -364,20 +367,16 @@
 
 %ifdef XP_MACOSX
 /* OSX only until we have icons for Windows and Linux */
   .geo-notification-icon,
   #geo-notification-icon {
     list-style-image: url(chrome://browser/skin/Geolocation-16@2x.png);
   }
 
-  #push-notification-icon {
-    list-style-image: url(chrome://browser/skin/Push-16@2x.png);
-  }
-
   .indexedDB-notification-icon,
   #indexedDB-notification-icon {
     list-style-image: url(chrome://global/skin/icons/question-32.png);
   }
 
   #login-fill-notification-icon,
   #password-notification-icon {
     list-style-image: url(chrome://mozapps/skin/passwordmgr/key-16@2x.png);
@@ -407,21 +406,16 @@
   #plugins-notification-icon:hover {
     -moz-image-region: rect(0, 64px, 32px, 32px);
   }
 
   #plugins-notification-icon:active {
     -moz-image-region: rect(0, 96px, 32px, 64px);
   }
 
-  .web-notifications-notification-icon,
-  #web-notifications-notification-icon {
-    list-style-image: url(chrome://browser/skin/notification-16@2x.png);
-  }
-
   .pointerLock-notification-icon,
   #pointerLock-notification-icon {
     list-style-image: url(chrome://browser/skin/pointerLock-16@2x.png);
   }
 
   .translate-notification-icon,
   #translate-notification-icon {
     list-style-image: url(chrome://browser/skin/translation-16@2x.png);
@@ -433,24 +427,16 @@
     list-style-image: url(chrome://browser/skin/translation-16@2x.png);
     -moz-image-region: rect(0px, 64px, 32px, 32px);
   }
 
   .popup-notification-icon[popupid="geolocation"] {
     list-style-image: url(chrome://browser/skin/Geolocation-64@2x.png);
   }
 
-  .popup-notification-icon[popupid="push"] {
-    list-style-image: url(chrome://browser/skin/Push-64@2x.png);
-  }
-
-  .popup-notification-icon[popupid="web-notifications"] {
-    list-style-image: url(chrome://browser/skin/notification-64@2x.png);
-  }
-
   .popup-notification-icon[popupid="pointerLock"] {
     list-style-image: url(chrome://browser/skin/pointerLock-64@2x.png);
   }
 
   .popup-notification-icon[popupid="servicesInstall"] {
     list-style-image: url(chrome://browser/skin/social/services-64@2x.png);
   }
 
--- a/browser/themes/shared/social/chat.inc.css
+++ b/browser/themes/shared/social/chat.inc.css
@@ -79,17 +79,17 @@
 .chat-minimize-button {
   list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize");
 }
 
 .chat-minimize-button:hover {
   list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-hover");
 }
 
-:hover,:hover:active) {
+.chat-minimize-button:hover:active {
   list-style-image: url("chrome://browser/skin/social/chat-icons.svg#minimize-active");
 }
 
 .chat-swap-button {
   list-style-image: url("chrome://browser/skin/social/chat-icons.svg#expand");
   transform: rotate(180deg);
 }
 
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/web-notifications-icon.svg
@@ -0,0 +1,15 @@
+<?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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" width="64" height="64" viewBox="0 0 64 64">
+  <defs>
+    <style>
+      .icon {
+        fill: #a6a6a6;
+        fill-rule: evenodd;
+      }
+    </style>
+  </defs>
+  <path d="M57,48 L46,48 L46,60.016 L32.482,48 L7,48 C5.343,48 4,46.657 4,45 L4,11.031 C4,9.374 5.343,8.031 7,8.031 L57,8.031 C58.657,8.031 60,9.374 60,11.031 L60,45 C60,46.657 58.657,48 57,48 ZM36,16.031 C36,14.927 35.105,14.031 34,14.031 L30,14.031 C28.895,14.031 28,14.927 28,16.031 L28,30.031 C28,31.136 28.895,32.031 30,32.031 L34,32.031 C35.105,32.031 36,31.136 36,30.031 L36,16.031 ZM36,37.5 C36,36.672 35.328,36 34.5,36 L29.5,36 C28.672,36 28,36.672 28,37.5 L28,40.5 C28,41.328 28.672,42 29.5,42 L34.5,42 C35.328,42 36,41.328 36,40.5 L36,37.5 Z" class="icon"/>
+</svg>
new file mode 100644
--- /dev/null
+++ b/browser/themes/shared/web-notifications-tray.svg
@@ -0,0 +1,23 @@
+<?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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="16" viewBox="0 0 96 32">
+  <defs>
+    <style>
+      .style-icon-notification {
+        fill: #666666;
+      }
+      .style-icon-notification.hover {
+        fill: #808080;
+      }
+      .style-icon-notification.active {
+        fill: #4d4d4d;
+      }
+    </style>
+    <path id="shape-notifcations-push" d="M27,23.969 L24,23.969 L24,29.977 L17.241,23.969 L5,23.969 C3.343,23.969 2,22.626 2,20.969 L2,6.969 C2,5.312 3.343,3.969 5,3.969 L27,3.969 C28.657,3.969 30,5.312 30,6.969 L30,20.969 C30,22.626 28.657,23.969 27,23.969 ZM18,8.969 C18,7.864 17.105,6.969 16,6.969 C14.895,6.969 14,7.864 14,8.969 L14,13.969 C14,15.073 14.895,15.969 16,15.969 C17.105,15.969 18,15.073 18,13.969 L18,8.969 ZM16.5,17.969 L15.5,17.969 C14.672,17.969 14,18.640 14,19.469 C14,20.297 14.672,20.969 15.5,20.969 L16.5,20.969 C17.328,20.969 18,20.297 18,19.469 C18,18.640 17.328,17.969 16.5,17.969 Z"/>
+  </defs>
+  <use xlink:href="#shape-notifcations-push" class="style-icon-notification"/>
+  <use xlink:href="#shape-notifcations-push" transform="translate(32)" class="style-icon-notification hover"/>
+  <use xlink:href="#shape-notifcations-push" transform="translate(64)" class="style-icon-notification active"/>
+</svg>
deleted file mode 100644
index d710e7336dbc445d36ee4868257d9139c50f8919..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index 27fecb8588f939fbb33347518c1627fc8e9b8af8..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/windows/jar.mn
+++ b/browser/themes/windows/jar.mn
@@ -20,18 +20,16 @@ browser.jar:
 * skin/classic/browser/browser.css
 * skin/classic/browser/devedition.css
 * skin/classic/browser/browser-lightweightTheme.css
   skin/classic/browser/caption-buttons.svg
   skin/classic/browser/click-to-play-warning-stripes.png
   skin/classic/browser/content-contextmenu.svg
   skin/classic/browser/Geolocation-16.png
   skin/classic/browser/Geolocation-64.png
-  skin/classic/browser/Push-16.png
-  skin/classic/browser/Push-64.png
   skin/classic/browser/Info.png
   skin/classic/browser/Info-XP.png
   skin/classic/browser/keyhole-forward-mask.svg
   skin/classic/browser/KUI-background.png
   skin/classic/browser/livemark-folder.png
   skin/classic/browser/livemark-folder-XP.png
   skin/classic/browser/menu-back.png
   skin/classic/browser/menu-back-XP.png
@@ -48,18 +46,16 @@ browser.jar:
   skin/classic/browser/menuPanel-help.png
   skin/classic/browser/menuPanel-help@2x.png
   skin/classic/browser/menuPanel-small.png
   skin/classic/browser/menuPanel-small@2x.png
   skin/classic/browser/menuPanel-small-aero.png
   skin/classic/browser/menuPanel-small-aero@2x.png
   skin/classic/browser/monitor.png
   skin/classic/browser/monitor_16-10.png
-  skin/classic/browser/notification-16.png
-  skin/classic/browser/notification-64.png
   skin/classic/browser/pageInfo.css
   skin/classic/browser/pageInfo.png
   skin/classic/browser/pageInfo-XP.png
   skin/classic/browser/pointerLock-16.png
   skin/classic/browser/pointerLock-64.png
   skin/classic/browser/Privacy-16.png
   skin/classic/browser/Privacy-16-XP.png
   skin/classic/browser/privatebrowsing-mask-tabstrip.png
deleted file mode 100644
index 281fba16d54cca16c18e8b883ebecd78edaf6efd..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
deleted file mode 100644
index cedd80d70eec1819625f04b57ff8217577e0e5a3..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
GIT binary patch
literal 0
Hc$@<O00001
--- a/browser/themes/windows/preferences/aboutPermissions.css
+++ b/browser/themes/windows/preferences/aboutPermissions.css
@@ -93,18 +93,18 @@
   list-style-image: url(chrome://mozapps/skin/passwordmgr/key-64.png);
 }
 .pref-icon[type="cookie"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 .pref-icon[type="geo"] {
   list-style-image: url(chrome://browser/skin/Geolocation-64.png);
 }
-.pref-icon[type="push"] {
-  list-style-image: url(chrome://browser/skin/Push-64.png);
+.pref-icon[type="desktop-notification"] {
+  list-style-image: url(chrome://browser/skin/web-notifications-icon.svg);
 }
 .pref-icon[type="indexedDB"] {
   list-style-image: url(chrome://global/skin/icons/question-64.png);
 }
 .pref-icon[type="install"] {
   list-style-image: url(chrome://mozapps/skin/extensions/extensionGeneric.png);
 }
 .pref-icon[type="popup"] {
--- a/build/autoconf/android.m4
+++ b/build/autoconf/android.m4
@@ -290,18 +290,19 @@ AC_DEFUN([MOZ_ANDROID_AAR],[
 ])
 
 AC_DEFUN([MOZ_ANDROID_GOOGLE_PLAY_SERVICES],
 [
 
 if test -n "$MOZ_NATIVE_DEVICES" ; then
     AC_SUBST(MOZ_NATIVE_DEVICES)
 
-    MOZ_ANDROID_AAR(play-services-base, 7.8.0, google, com/google/android/gms)
-    MOZ_ANDROID_AAR(play-services-cast, 7.8.0, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-base, 8.1.0, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-basement, 8.1.0, google, com/google/android/gms)
+    MOZ_ANDROID_AAR(play-services-cast, 8.1.0, google, com/google/android/gms)
     MOZ_ANDROID_AAR(mediarouter-v7, 22.2.1, android, com/android/support, REQUIRED_INTERNAL_IMPL)
 fi
 
 ])
 
 dnl Configure an Android SDK.
 dnl Arg 1: target SDK version, like 22.
 dnl Arg 2: build tools version, like 22.0.1.
--- a/configure.in
+++ b/configure.in
@@ -4090,17 +4090,17 @@ if test -z "$gonkdir" ; then
             :
             ;;
         *)
             AC_MSG_ERROR([You must specify --target=arm-linux-androideabi (or some other valid android target) when building with --enable-application=mobile/android or --enable-application=mobile/android/b2gdroid.
              See https://wiki.mozilla.org/Mobile/Fennec/Android#Setup_Fennec_mozconfig for more information about the necessary options])
             ;;
         esac
 
-        MOZ_ANDROID_SDK(22, 22.0.1)
+        MOZ_ANDROID_SDK(23, 23.0.1)
         ;;
     esac
 fi
 
 dnl ========================================================
 dnl =
 dnl = Toolkit Options
 dnl =
--- a/devtools/client/webide/content/runtimedetails.js
+++ b/devtools/client/webide/content/runtimedetails.js
@@ -5,17 +5,17 @@
 const Cu = Components.utils;
 const {Services} = Cu.import("resource://gre/modules/Services.jsm");
 const {require} = Cu.import("resource://gre/modules/devtools/shared/Loader.jsm", {});
 const {AppManager} = require("devtools/client/webide/modules/app-manager");
 const {Connection} = require("devtools/shared/client/connection-manager");
 const {RuntimeTypes} = require("devtools/client/webide/modules/runtimes");
 const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
 
-const UNRESTRICTED_HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE#Unrestricted_app_debugging_%28including_certified_apps.2C_main_process.2C_etc.%29";
+const UNRESTRICTED_HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Running_and_debugging_apps#Unrestricted_app_debugging_%28including_certified_apps_main_process_etc.%29";
 
 window.addEventListener("load", function onLoad() {
   window.removeEventListener("load", onLoad);
   document.querySelector("#close").onclick = CloseUI;
   document.querySelector("#devtools-check button").onclick = EnableCertApps;
   document.querySelector("#adb-check button").onclick = RootADB;
   document.querySelector("#unrestricted-privileges").onclick = function() {
     window.parent.UI.openInBrowser(UNRESTRICTED_HELP_URL);
--- a/dom/notification/Notification.cpp
+++ b/dom/notification/Notification.cpp
@@ -1156,16 +1156,25 @@ NotificationObserver::Observe(nsISupport
   if (!strcmp("alertdisablecallback", aTopic)) {
     nsCOMPtr<nsIPermissionManager> permissionManager =
       mozilla::services::GetPermissionManager();
     if (!permissionManager) {
       return NS_ERROR_FAILURE;
     }
     permissionManager->RemoveFromPrincipal(mPrincipal, "desktop-notification");
     return NS_OK;
+  } else if (!strcmp("alertsettingscallback", aTopic)) {
+    nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+    if (!obs) {
+      return NS_ERROR_FAILURE;
+    }
+
+    // Notify other observers so they can show settings UI.
+    obs->NotifyObservers(mPrincipal, "notifications-open-settings", nullptr);
+    return NS_OK;
   }
 
   return mObserver->Observe(aSubject, aTopic, aData);
 }
 
 NS_IMETHODIMP
 MainThreadNotificationObserver::Observe(nsISupports* aSubject, const char* aTopic,
                                         const char16_t* aData)
@@ -1625,19 +1634,19 @@ Notification::GetPermissionInternal(nsIP
     }
   }
 
   uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
 
   nsCOMPtr<nsIPermissionManager> permissionManager =
     services::GetPermissionManager();
 
-  permissionManager->TestPermissionFromPrincipal(aPrincipal,
-                                                 "desktop-notification",
-                                                 &permission);
+  permissionManager->TestExactPermissionFromPrincipal(aPrincipal,
+                                                      "desktop-notification",
+                                                      &permission);
 
   // Convert the result to one of the enum types.
   switch (permission) {
   case nsIPermissionManager::ALLOW_ACTION:
     return NotificationPermission::Granted;
   case nsIPermissionManager::DENY_ACTION:
     return NotificationPermission::Denied;
   default:
--- a/dom/permission/PermissionUtils.cpp
+++ b/dom/permission/PermissionUtils.cpp
@@ -7,17 +7,18 @@
 #include "PermissionUtils.h"
 
 namespace mozilla {
 namespace dom {
 
 const char* kPermissionTypes[] = {
   "geo",
   "desktop-notification",
-  "push",
+  // Alias `push` to `desktop-notification`.
+  "desktop-notification",
   "midi"
 };
 
 // `-1` for the last null entry.
 const size_t kPermissionNameCount =
   MOZ_ARRAY_LENGTH(PermissionNameValues::strings) - 1;
 
 static_assert(MOZ_ARRAY_LENGTH(kPermissionTypes) == kPermissionNameCount,
--- a/dom/permission/tests/test_permissions_api.html
+++ b/dom/permission/tests/test_permissions_api.html
@@ -18,17 +18,17 @@
 let { UNKNOWN_ACTION, PROMPT_ACTION, ALLOW_ACTION, DENY_ACTION } =
   SpecialPowers.Ci.nsIPermissionManager;
 
 SimpleTest.waitForExplicitFinish();
 
 const PERMISSIONS = [
   { name: 'geolocation', perm: 'geo' },
   { name: 'notifications', perm: 'desktop-notification' },
-  { name: 'push', perm: 'push' },
+  { name: 'push', perm: 'desktop-notification' },
 ];
 
 const UNSUPPORTED_PERMISSIONS = [
   'midi',
 ];
 
 function setup() {
   return new Promise((resolve, reject) => {
--- a/dom/push/Push.js
+++ b/dom/push/Push.js
@@ -62,45 +62,45 @@ Push.prototype = {
   setScope: function(scope){
     debug('setScope ' + scope);
     this._scope = scope;
   },
 
   askPermission: function (aAllowCallback, aCancelCallback) {
     debug("askPermission");
 
-    let principal = this._window.document.nodePrincipal;
-    let type = "push";
-    let permValue =
-      Services.perms.testExactPermissionFromPrincipal(principal, type);
+    let permValue = Services.perms.testExactPermissionFromPrincipal(
+      this._principal,
+      "desktop-notification"
+    );
 
     if (permValue == Ci.nsIPermissionManager.ALLOW_ACTION) {
-        aAllowCallback();
+      aAllowCallback();
       return;
     }
 
     if (permValue == Ci.nsIPermissionManager.DENY_ACTION) {
       aCancelCallback();
       return;
     }
 
     // Create an array with a single nsIContentPermissionType element.
-    type = {
-      type: "push",
+    let type = {
+      type: "desktop-notification",
       access: null,
       options: [],
       QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionType])
     };
     let typeArray = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
     typeArray.appendElement(type, false);
 
     // create a nsIContentPermissionRequest
     let request = {
       types: typeArray,
-      principal: principal,
+      principal: this._principal,
       QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionRequest]),
       allow: function() {
         let histogram = Services.telemetry.getHistogramById("PUSH_API_PERMISSION_GRANTED");
         histogram.add();
         aAllowCallback();
       },
       cancel: function() {
         let histogram = Services.telemetry.getHistogramById("PUSH_API_PERMISSION_DENIED");
@@ -177,21 +177,18 @@ Push.prototype = {
 
   permissionState: function() {
     debug("permissionState()" + this._scope);
 
     let p = this.createPromise((resolve, reject) => {
       let permission = Ci.nsIPermissionManager.DENY_ACTION;
 
       try {
-        let permissionManager = Cc["@mozilla.org/permissionmanager;1"]
-                                .getService(Ci.nsIPermissionManager);
-        permission =
-          permissionManager.testExactPermissionFromPrincipal(this._principal,
-                                                             "push");
+        permission = Services.perms.testExactPermissionFromPrincipal(
+          this._principal, "desktop-notification");
       } catch(e) {
         reject();
         return;
       }
 
       let pushPermissionStatus = "prompt";
       if (permission == Ci.nsIPermissionManager.ALLOW_ACTION) {
         pushPermissionStatus = "granted";
--- a/dom/push/PushManager.cpp
+++ b/dom/push/PushManager.cpp
@@ -587,17 +587,17 @@ public:
       return NS_OK;
     }
 
     nsCOMPtr<nsIPrincipal> principal = mProxy->GetWorkerPrivate()->GetPrincipal();
 
     uint32_t permission = nsIPermissionManager::DENY_ACTION;
     nsresult rv = permManager->TestExactPermissionFromPrincipal(
                     principal,
-                    "push",
+                    "desktop-notification",
                     &permission);
 
     if (NS_WARN_IF(NS_FAILED(rv)) || permission != nsIPermissionManager::ALLOW_ACTION) {
       callback->OnPushEndpoint(NS_ERROR_FAILURE, EmptyString(), 0, nullptr);
       return NS_OK;
     }
 
     nsCOMPtr<nsIPushClient> client =
@@ -727,38 +727,30 @@ public:
     if (mProxy->CleanedUp()) {
       return NS_OK;
     }
 
     nsCOMPtr<nsIPermissionManager> permManager =
       mozilla::services::GetPermissionManager();
 
     nsresult rv = NS_ERROR_FAILURE;
-    PushPermissionState state = PushPermissionState::Denied;
+    PushPermissionState state = PushPermissionState::Prompt;
 
     if (permManager) {
-      uint32_t permission = nsIPermissionManager::DENY_ACTION;
+      uint32_t permission = nsIPermissionManager::UNKNOWN_ACTION;
       rv = permManager->TestExactPermissionFromPrincipal(
              mProxy->GetWorkerPrivate()->GetPrincipal(),
-             "push",
+             "desktop-notification",
              &permission);
 
       if (NS_SUCCEEDED(rv)) {
-        switch (permission) {
-          case nsIPermissionManager::ALLOW_ACTION:
-            state = PushPermissionState::Granted;
-            break;
-          case nsIPermissionManager::DENY_ACTION:
-            state = PushPermissionState::Denied;
-            break;
-          case nsIPermissionManager::PROMPT_ACTION:
-            state = PushPermissionState::Prompt;
-            break;
-          default:
-            MOZ_CRASH("Unexpected case!");
+        if (permission == nsIPermissionManager::ALLOW_ACTION) {
+          state = PushPermissionState::Granted;
+        } else if (permission == nsIPermissionManager::DENY_ACTION) {
+          state = PushPermissionState::Denied;
         }
       }
     }
 
     AutoJSAPI jsapi;
     jsapi.Init();
     nsRefPtr<PermissionResultRunnable> r =
       new PermissionResultRunnable(mProxy, rv, state);
--- a/dom/push/PushRecord.jsm
+++ b/dom/push/PushRecord.jsm
@@ -156,30 +156,22 @@ PushRecord.prototype = {
           return true;
         }
       }
     }
     return false;
   },
 
   /**
-   * Returns the push permission state for the principal associated with
-   * this registration.
-   */
-  pushPermission() {
-    return Services.perms.testExactPermissionFromPrincipal(
-           this.principal, "push");
-  },
-
-  /**
    * Indicates whether the registration can deliver push messages to its
    * associated service worker.
    */
   hasPermission() {
-    let permission = this.pushPermission();
+    let permission = Services.perms.testExactPermissionFromPrincipal(
+      this.principal, "desktop-notification");
     return permission == Ci.nsIPermissionManager.ALLOW_ACTION;
   },
 
   quotaApplies() {
     return Number.isFinite(this.quota);
   },
 
   isExpired() {
--- a/dom/push/test/test_data.html
+++ b/dom/push/test/test_data.html
@@ -174,13 +174,13 @@ http://creativecommons.org/licenses/publ
   }
 
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.push.enabled", true],
     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
     ["dom.serviceWorkers.enabled", true],
     ["dom.serviceWorkers.testing.enabled", true]
     ]}, runTest);
-  SpecialPowers.addPermission('push', true, document);
+  SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_has_permissions.html
+++ b/dom/push/test/test_has_permissions.html
@@ -60,17 +60,17 @@ http://creativecommons.org/licenses/publ
     start()
     .then(hasPermission)
     .then(unregister)
     .catch(function(e) {
       ok(false, "Some test failed with error " + e);
     }).then(SimpleTest.finish);
   }
 
-  SpecialPowers.addPermission('push', false, document);
+  SpecialPowers.addPermission("desktop-notification", false, document);
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.push.enabled", true],
     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
     ["dom.serviceWorkers.enabled", true],
     ["dom.serviceWorkers.testing.enabled", true]
     ]}, runTest);
   SimpleTest.waitForExplicitFinish();
 
--- a/dom/push/test/test_multiple_register.html
+++ b/dom/push/test/test_multiple_register.html
@@ -120,13 +120,13 @@ http://creativecommons.org/licenses/publ
 
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.push.enabled", true],
     ["dom.push.debug", true],
     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
     ["dom.serviceWorkers.enabled", true],
     ["dom.serviceWorkers.testing.enabled", true]
     ]}, runTest);
-  SpecialPowers.addPermission('push', true, document);
+  SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_multiple_register_different_scope.html
+++ b/dom/push/test/test_multiple_register_different_scope.html
@@ -114,13 +114,13 @@ http://creativecommons.org/licenses/publ
   }
 
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.push.enabled", true],
     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
     ["dom.serviceWorkers.enabled", true],
     ["dom.serviceWorkers.testing.enabled", true]
     ]}, runTest);
-  SpecialPowers.addPermission('push', true, document);
+  SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_multiple_register_during_service_activation.html
+++ b/dom/push/test/test_multiple_register_during_service_activation.html
@@ -102,13 +102,13 @@ var defaultServerURL = SpecialPowers.get
 
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.push.enabled", true],
     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
     ["dom.serviceWorkers.enabled", true],
     ["dom.serviceWorkers.testing.enabled", true],
     ["dom.push.serverURL", "wss://something.org"]
     ]}, runTest);
-  SpecialPowers.addPermission('push', true, document);
+  SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_permissions.html
+++ b/dom/push/test/test_permissions.html
@@ -66,37 +66,57 @@ http://creativecommons.org/licenses/publ
           res(swr);
         }
         );
     });
     return p;
   }
 
   function checkPermissionState(swr) {
-    return swr.pushManager.permissionState().then(function(state) {
-      ok(state === "denied", "permissionState() should resolve to denied.");
-      return swr;
-    }).catch(function(e) {
-      ok(false, "permissionState() should resolve to denied.");
-      return swr;
-    });
+    var permissionManager = SpecialPowers.Ci.nsIPermissionManager;
+    var tests = [{
+      action: permissionManager.ALLOW_ACTION,
+      state: "granted",
+    }, {
+      action: permissionManager.DENY_ACTION,
+      state: "denied",
+    }, {
+      action: permissionManager.PROMPT_ACTION,
+      state: "prompt",
+    }, {
+      action: permissionManager.UNKNOWN_ACTION,
+      state: "prompt",
+    }];
+    return tests.reduce((promise, test) => {
+      return promise.then(function() {
+        if (test.action == permissionManager.UNKNOWN_ACTION) {
+          SpecialPowers.removePermission("desktop-notification", document);
+        } else {
+          SpecialPowers.addPermission("desktop-notification",
+            test.action, document);
+        }
+        return swr.pushManager.permissionState().then(state => {
+          is(state, test.state, JSON.stringify(test));
+        });
+      });
+    }, Promise.resolve());
   }
 
   function runTest() {
     start()
     .then(setupPushNotification)
     .then(getEndpoint)
     .then(checkPermissionState)
     .then(unregister)
     .catch(function(e) {
       ok(false, "Some test failed with error " + e);
     }).then(SimpleTest.finish);
   }
 
-  SpecialPowers.addPermission('push', false, document);
+  SpecialPowers.addPermission("desktop-notification", false, document);
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.push.enabled", true],
     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
     ["dom.serviceWorkers.enabled", true],
     ["dom.serviceWorkers.testing.enabled", true]
     ]}, runTest);
   SimpleTest.waitForExplicitFinish();
 
--- a/dom/push/test/test_register.html
+++ b/dom/push/test/test_register.html
@@ -126,13 +126,13 @@ http://creativecommons.org/licenses/publ
   }
 
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.push.enabled", true],
     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
     ["dom.serviceWorkers.enabled", true],
     ["dom.serviceWorkers.testing.enabled", true]
     ]}, runTest);
-  SpecialPowers.addPermission('push', true, document);
+  SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_try_registering_offline_disabled.html
+++ b/dom/push/test/test_try_registering_offline_disabled.html
@@ -288,13 +288,13 @@ http://creativecommons.org/licenses/publ
   }
 
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.push.enabled", true],
     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
     ["dom.serviceWorkers.enabled", true],
     ["dom.serviceWorkers.testing.enabled", true]
     ]}, runTest);
-  SpecialPowers.addPermission('push', true, document);
+  SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
--- a/dom/push/test/test_unregister.html
+++ b/dom/push/test/test_unregister.html
@@ -80,14 +80,14 @@ http://creativecommons.org/licenses/publ
   }
 
   SpecialPowers.pushPrefEnv({"set": [
     ["dom.push.enabled", true],
     ["dom.serviceWorkers.exemptFromPerDomainMax", true],
     ["dom.serviceWorkers.enabled", true],
     ["dom.serviceWorkers.testing.enabled", true]
     ]}, runTest);
-  SpecialPowers.addPermission('push', true, document);
+  SpecialPowers.addPermission("desktop-notification", true, document);
   SimpleTest.waitForExplicitFinish();
 </script>
 </body>
 </html>
 
--- a/mobile/android/base/Makefile.in
+++ b/mobile/android/base/Makefile.in
@@ -61,16 +61,17 @@ JAVA_CLASSPATH += \
     $(ANDROID_RECYCLERVIEW_V7_AAR_LIB) \
     $(NULL)
 
 # If native devices are enabled, add Google Play Services and some of the v7
 # compat libraries.
 ifdef MOZ_NATIVE_DEVICES
     JAVA_CLASSPATH += \
         $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
+        $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
         $(ANDROID_PLAY_SERVICES_CAST_AAR_LIB) \
         $(ANDROID_MEDIAROUTER_V7_AAR_LIB) \
         $(ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB) \
         $(NULL)
 endif
 
 JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH)))
 
@@ -81,16 +82,17 @@ java_bundled_libs := \
     $(ANDROID_SUPPORT_V4_AAR_INTERNAL_LIB) \
     $(ANDROID_APPCOMPAT_V7_AAR_LIB) \
     $(ANDROID_RECYCLERVIEW_V7_AAR_LIB) \
     $(NULL)
 
 ifdef MOZ_NATIVE_DEVICES
     java_bundled_libs += \
         $(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
+        $(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
         $(ANDROID_PLAY_SERVICES_CAST_AAR_LIB) \
         $(ANDROID_MEDIAROUTER_V7_AAR_LIB) \
         $(ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB) \
         $(NULL)
 endif
 
 java_bundled_libs := $(subst $(NULL) ,:,$(strip $(java_bundled_libs)))
 
@@ -367,16 +369,17 @@ endif
 generated/org/mozilla/gecko/R.java: .aapt.deps ;
 
 # If native devices are enabled, add Google Play Services, build their resources
 generated/android/support/v4/R.java: .aapt.deps ;
 generated/android/support/v7/appcompat/R.java: .aapt.deps ;
 generated/android/support/v7/mediarouter/R.java: .aapt.deps ;
 generated/android/support/v7/recyclerview/R.java: .aapt.deps ;
 generated/com/google/android/gms/R.java: .aapt.deps ;
+generated/com/google/android/gms/base/R.java: .aapt.deps ;
 generated/com/google/android/gms/cast/R.java: .aapt.deps ;
 
 gecko.ap_: .aapt.deps ;
 R.txt: .aapt.deps ;
 
 # [Comment 2/3] This tom-foolery provides a target that forces a
 # rebuild of gecko.ap_.  This is used during packaging to ensure that
 # resources are fresh.  The alternative would be complicated; see
--- a/mobile/android/base/home/BrowserSearch.java
+++ b/mobile/android/base/home/BrowserSearch.java
@@ -9,37 +9,40 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Locale;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
+import org.mozilla.gecko.AppConstants;
 import org.mozilla.gecko.annotation.RobocopTarget;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
 import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.PrefsHelper;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.SuggestClient;
 import org.mozilla.gecko.Tab;
 import org.mozilla.gecko.Tabs;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserContract.History;
 import org.mozilla.gecko.db.BrowserContract.URLColumns;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.home.SearchLoader.SearchCursorLoader;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.util.GeckoEventListener;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 
 import android.app.Activity;
+import android.content.ContentResolver;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
 import android.support.v4.app.LoaderManager.LoaderCallbacks;
 import android.support.v4.content.AsyncTaskLoader;
 import android.support.v4.content.Loader;
 import android.text.TextUtils;
@@ -89,16 +92,17 @@ public class BrowserSearch extends HomeF
     // Logging tag name
     private static final String LOGTAG = "GeckoBrowserSearch";
 
     // Cursor loader ID for search query
     private static final int LOADER_ID_SEARCH = 0;
 
     // AsyncTask loader ID for suggestion query
     private static final int LOADER_ID_SUGGESTION = 1;
+    private static final int LOADER_ID_SAVED_SUGGESTION = 2;
 
     // Timeout for the suggestion client to respond
     private static final int SUGGESTION_TIMEOUT = 3000;
 
     // Maximum number of results returned by the suggestion client
     private static final int SUGGESTION_MAX = 3;
 
     // The maximum number of rows deep in a search we'll dig
@@ -132,28 +136,32 @@ public class BrowserSearch extends HomeF
     @RobocopTarget
     public volatile SuggestClient mSuggestClient;
 
     // List of search engines from Gecko.
     // Do not mutate this list.
     // Access to this member must only occur from the UI thread.
     private List<SearchEngine> mSearchEngines;
 
+    // Search history suggestions
+    private ArrayList<String> mSearchHistorySuggestions;
+
     // Track the locale that was last in use when we filled mSearchEngines.
     // Access to this member must only occur from the UI thread.
     private Locale mLastLocale;
 
     // Whether search suggestions are enabled or not
     private boolean mSuggestionsEnabled;
 
     // Callbacks used for the search loader
     private CursorLoaderCallbacks mCursorLoaderCallbacks;
 
     // Callbacks used for the search suggestion loader
-    private SuggestionLoaderCallbacks mSuggestionLoaderCallbacks;
+    private SearchEngineSuggestionLoaderCallbacks mSearchEngineSuggestionLoaderCallbacks;
+    private SearchHistorySuggestionLoaderCallbacks mSearchHistorySuggestionLoaderCallback;
 
     // Autocomplete handler used when filtering results
     private AutocompleteHandler mAutocompleteHandler;
 
     // On search listener
     private OnSearchListener mSearchListener;
 
     // On edit suggestion listener
@@ -215,16 +223,17 @@ public class BrowserSearch extends HomeF
         mEditSuggestionListener = null;
     }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
 
         mSearchEngines = new ArrayList<SearchEngine>();
+        mSearchHistorySuggestions = new ArrayList<>();
     }
 
     @Override
     public void onDestroy() {
         super.onDestroy();
 
         mSearchEngines = null;
     }
@@ -350,17 +359,18 @@ public class BrowserSearch extends HomeF
     public void onActivityCreated(Bundle savedInstanceState) {
         super.onActivityCreated(savedInstanceState);
 
         // Initialize the search adapter
         mAdapter = new SearchAdapter(getActivity());
         mList.setAdapter(mAdapter);
 
         // Only create an instance when we need it
-        mSuggestionLoaderCallbacks = null;
+        mSearchEngineSuggestionLoaderCallbacks = null;
+        mSearchHistorySuggestionLoaderCallback = null;
 
         // Create callbacks before the initial loader is started
         mCursorLoaderCallbacks = new CursorLoaderCallbacks();
         loadIfVisible();
     }
 
     @Override
     public void handleMessage(String event, final JSONObject message) {
@@ -514,30 +524,46 @@ public class BrowserSearch extends HomeF
         mSearchEngineBar.scrollToPosition(0);
     }
 
     private void filterSuggestions() {
         if (mSuggestClient == null || !mSuggestionsEnabled) {
             return;
         }
 
-        if (mSuggestionLoaderCallbacks == null) {
-            mSuggestionLoaderCallbacks = new SuggestionLoaderCallbacks();
+        // Suggestions from search engine
+        if (mSearchEngineSuggestionLoaderCallbacks == null) {
+            mSearchEngineSuggestionLoaderCallbacks = new SearchEngineSuggestionLoaderCallbacks();
         }
+        getLoaderManager().restartLoader(LOADER_ID_SUGGESTION, null, mSearchEngineSuggestionLoaderCallbacks);
 
-        getLoaderManager().restartLoader(LOADER_ID_SUGGESTION, null, mSuggestionLoaderCallbacks);
+        // Start search history suggestions query only in nightly. Bug 1201325
+        if (AppConstants.NIGHTLY_BUILD) {
+            // Saved suggestions
+            if (mSearchHistorySuggestionLoaderCallback == null) {
+                mSearchHistorySuggestionLoaderCallback = new SearchHistorySuggestionLoaderCallbacks();
+            }
+            getLoaderManager().restartLoader(LOADER_ID_SAVED_SUGGESTION, null, mSearchHistorySuggestionLoaderCallback);
+        }
     }
 
     private void setSuggestions(ArrayList<String> suggestions) {
         ThreadUtils.assertOnUiThread();
 
         mSearchEngines.get(0).setSuggestions(suggestions);
         mAdapter.notifyDataSetChanged();
     }
 
+    private void setSavedSuggestions(ArrayList<String> savedSuggestions) {
+        ThreadUtils.assertOnUiThread();
+
+        mSearchHistorySuggestions = savedSuggestions;
+        mAdapter.notifyDataSetChanged();
+    }
+
     private void setSearchEngines(JSONObject data) {
         ThreadUtils.assertOnUiThread();
 
         // This method is called via a Runnable posted from the Gecko thread, so
         // it's possible the fragment and/or its view has been destroyed by the
         // time we get here. If so, just abort.
         if (mView == null) {
             return;
@@ -783,33 +809,26 @@ public class BrowserSearch extends HomeF
                 // The search term hasn't changed, simply reuse any existing
                 // loader for the current search term. This will ensure autocompletion
                 // is consistently triggered (see bug 933739).
                 initSearchLoader();
             }
         }
     }
 
-    private static class SuggestionAsyncLoader extends AsyncTaskLoader<ArrayList<String>> {
-        private final SuggestClient mSuggestClient;
-        private final String mSearchTerm;
+    abstract private static class SuggestionAsyncLoader extends AsyncTaskLoader<ArrayList<String>> {
+        protected final String mSearchTerm;
         private ArrayList<String> mSuggestions;
 
-        public SuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) {
+        public SuggestionAsyncLoader(Context context, String searchTerm) {
             super(context);
-            mSuggestClient = suggestClient;
             mSearchTerm = searchTerm;
         }
 
         @Override
-        public ArrayList<String> loadInBackground() {
-            return mSuggestClient.query(mSearchTerm);
-        }
-
-        @Override
         public void deliverResult(ArrayList<String> suggestions) {
             mSuggestions = suggestions;
 
             if (isStarted()) {
                 super.deliverResult(mSuggestions);
             }
         }
 
@@ -833,16 +852,68 @@ public class BrowserSearch extends HomeF
         protected void onReset() {
             super.onReset();
 
             onStopLoading();
             mSuggestions = null;
         }
     }
 
+    private static class SearchEngineSuggestionAsyncLoader extends SuggestionAsyncLoader {
+        private final SuggestClient mSuggestClient;
+
+        public SearchEngineSuggestionAsyncLoader(Context context, SuggestClient suggestClient, String searchTerm) {
+            super(context, searchTerm);
+            mSuggestClient = suggestClient;
+        }
+
+        @Override
+        public ArrayList<String> loadInBackground() {
+            return mSuggestClient.query(mSearchTerm);
+        }
+    }
+
+    private static class SearchHistorySuggestionAsyncLoader extends SuggestionAsyncLoader {
+        public SearchHistorySuggestionAsyncLoader(Context context, String searchTerm) {
+            super(context, searchTerm);
+        }
+
+        @Override
+        public ArrayList<String> loadInBackground() {
+            final ContentResolver cr = getContext().getContentResolver();
+
+            String[] columns = new String[] { BrowserContract.SearchHistory.QUERY };
+            String actualQuery = BrowserContract.SearchHistory.QUERY + " LIKE ?";
+            String[] queryArgs = new String[] { '%' + mSearchTerm + '%' };
+
+            final int maxSavedSuggestions = getContext().getResources().getInteger(R.integer.max_saved_suggestions);
+            final String sortOrderAndLimit = BrowserContract.SearchHistory.DATE +" DESC LIMIT " + maxSavedSuggestions;
+            final Cursor result =  cr.query(BrowserContract.SearchHistory.CONTENT_URI, columns, actualQuery, queryArgs, sortOrderAndLimit);
+
+            if (result == null) {
+                return new ArrayList<>();
+            }
+
+            final ArrayList<String> savedSuggestions = new ArrayList<>();
+            try {
+                if (result.moveToFirst()) {
+                    final int searchColumn = result.getColumnIndexOrThrow(BrowserContract.SearchHistory.QUERY);
+                    do {
+                        final String savedSearch = result.getString(searchColumn);
+                        savedSuggestions.add(savedSearch);
+                    } while (result.moveToNext());
+                }
+            } finally {
+                result.close();
+            }
+
+            return savedSuggestions;
+        }
+    }
+
     private class SearchAdapter extends MultiTypeCursorAdapter {
         private static final int ROW_SEARCH = 0;
         private static final int ROW_STANDARD = 1;
         private static final int ROW_SUGGEST = 2;
 
         public SearchAdapter(Context context) {
             super(context, null, new int[] { ROW_STANDARD,
                                              ROW_SEARCH,
@@ -910,18 +981,19 @@ public class BrowserSearch extends HomeF
             if (type == ROW_SEARCH || type == ROW_SUGGEST) {
                 final SearchEngineRow row = (SearchEngineRow) view;
                 row.setOnUrlOpenListener(mUrlOpenListener);
                 row.setOnSearchListener(mSearchListener);
                 row.setOnEditSuggestionListener(mEditSuggestionListener);
                 row.setSearchTerm(mSearchTerm);
 
                 final SearchEngine engine = mSearchEngines.get(position);
-                final boolean animate = (mAnimateSuggestions && engine.hasSuggestions());
-                row.updateSuggestions(mSuggestionsEnabled, engine, mSearchTerm, animate);
+                final boolean haveSuggestions = (engine.hasSuggestions() || !mSearchHistorySuggestions.isEmpty());
+                final boolean animate = (mAnimateSuggestions && haveSuggestions);
+                row.updateSuggestions(mSuggestionsEnabled, engine, mSearchHistorySuggestions, animate);
                 if (animate) {
                     // Only animate suggestions the first time they are shown
                     mAnimateSuggestions = false;
                 }
             } else {
                 // Account for the search engines
                 position -= getPrimaryEngineCount();
 
@@ -950,36 +1022,56 @@ public class BrowserSearch extends HomeF
         }
 
         @Override
         public void onLoaderReset(Loader<Cursor> loader) {
             mAdapter.swapCursor(null);
         }
     }
 
-    private class SuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> {
+    private class SearchEngineSuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> {
         @Override
         public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) {
             // mSuggestClient is set to null in onDestroyView(), so using it
             // safely here relies on the fact that onCreateLoader() is called
             // synchronously in restartLoader().
-            return new SuggestionAsyncLoader(getActivity(), mSuggestClient, mSearchTerm);
+            return new SearchEngineSuggestionAsyncLoader(getActivity(), mSuggestClient, mSearchTerm);
         }
 
         @Override
         public void onLoadFinished(Loader<ArrayList<String>> loader, ArrayList<String> suggestions) {
             setSuggestions(suggestions);
         }
 
         @Override
         public void onLoaderReset(Loader<ArrayList<String>> loader) {
             setSuggestions(new ArrayList<String>());
         }
     }
 
+    private class SearchHistorySuggestionLoaderCallbacks implements LoaderCallbacks<ArrayList<String>> {
+        @Override
+        public Loader<ArrayList<String>> onCreateLoader(int id, Bundle args) {
+            // mSuggestClient is set to null in onDestroyView(), so using it
+            // safely here relies on the fact that onCreateLoader() is called
+            // synchronously in restartLoader().
+            return new SearchHistorySuggestionAsyncLoader(getActivity(), mSearchTerm);
+        }
+
+        @Override
+        public void onLoadFinished(Loader<ArrayList<String>> loader, ArrayList<String> suggestions) {
+            setSavedSuggestions(suggestions);
+        }
+
+        @Override
+        public void onLoaderReset(Loader<ArrayList<String>> loader) {
+            setSavedSuggestions(new ArrayList<String>());
+        }
+    }
+
     private static class ListSelectionListener implements View.OnFocusChangeListener,
                                                           AdapterView.OnItemSelectedListener {
         private SearchEngineRow mSelectedEngineRow;
 
         @Override
         public void onFocusChange(View v, boolean hasFocus) {
             if (hasFocus) {
                 View selectedRow = ((ListView) v).getSelectedView();
--- a/mobile/android/base/home/SearchEngineRow.java
+++ b/mobile/android/base/home/SearchEngineRow.java
@@ -1,47 +1,45 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 package org.mozilla.gecko.home;
 
 import org.mozilla.gecko.AppConstants;
-import org.mozilla.gecko.db.BrowserContract.SearchHistory;
 import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.home.BrowserSearch.OnEditSuggestionListener;
 import org.mozilla.gecko.home.BrowserSearch.OnSearchListener;
 import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
 import org.mozilla.gecko.preferences.GeckoPreferences;
 import org.mozilla.gecko.util.DrawableUtil;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.HardwareUtils;
 import org.mozilla.gecko.widget.AnimatedHeightLayout;
 import org.mozilla.gecko.widget.FaviconView;
 import org.mozilla.gecko.widget.FlowLayout;
 
-import android.database.Cursor;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.SharedPreferences;
 import android.graphics.drawable.Drawable;
 import android.util.AttributeSet;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.animation.AlphaAnimation;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import java.util.EnumSet;
+import java.util.List;
 
 class SearchEngineRow extends AnimatedHeightLayout {
     // Duration for fade-in animation
     private static final int ANIMATION_DURATION = 250;
 
     // Inner views
     private final FlowLayout mSuggestionView;
     private final FaviconView mIconView;
@@ -237,63 +235,34 @@ class SearchEngineRow extends AnimatedHe
         for (int i = lastVisibleChildIndex + 1; i < recycledSuggestionCount; ++i) {
             mSuggestionView.getChildAt(i).setVisibility(View.GONE);
         }
     }
 
     /**
      * Displays search suggestions from previous searches.
      *
-     * @param c The Cursor to iterate over for saved search suggestion to display
+     * @param savedSuggestions The List to iterate over for saved search suggestions to display
      * @param suggestionCounter global index of where to start adding suggestion "buttons" in the search engine row
      * @param animate whether or not to animate suggestions for visual polish
      * @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls
      */
-    private void updateFromSavedSearches(Cursor c, boolean animate, int suggestionCounter, int recycledSuggestionCount) {
-        if (c == null) {
+    private void updateFromSavedSearches(List<String> savedSuggestions, boolean animate, int suggestionCounter, int recycledSuggestionCount) {
+        if (savedSuggestions == null || savedSuggestions.isEmpty()) {
             return;
         }
-        try {
-            if (c.moveToFirst()) {
-                final int searchColumn = c.getColumnIndexOrThrow(SearchHistory.QUERY);
-                final int historyStartIndex = suggestionCounter;
-                do {
-                    final String savedSearch = c.getString(searchColumn);
-                    // suggestionCounter counts all suggestions (from history and the search engine)
-                    // but we want the relative position of the history item in telemetry
-                    String telemetryTag = "history." + (suggestionCounter - historyStartIndex);
-                    bindSuggestionView(savedSearch, animate, recycledSuggestionCount, suggestionCounter, true, telemetryTag);
-                    ++suggestionCounter;
-                } while (c.moveToNext());
-            }
-        } finally {
-            c.close();
-        }
-        hideRecycledSuggestions(suggestionCounter, recycledSuggestionCount);
-    }
 
-    /**
-     * Gets matching suggestions from search history.
-     *
-     * @param searchTerm the string with which to look for matches in the saved searches
-     * @return matching prior searches that contain searchTerm
-     */
-    private Cursor getSavedSearches(String searchTerm) {
-        if (!AppConstants.NIGHTLY_BUILD) {
-            return null;
+        final int historyStartIndex = suggestionCounter;
+        for (String suggestion : savedSuggestions) {
+            String telemetryTag = "history." + (suggestionCounter - historyStartIndex);
+            bindSuggestionView(suggestion, animate, recycledSuggestionCount, suggestionCounter, true, telemetryTag);
+            ++suggestionCounter;
         }
 
-        final ContentResolver cr = getContext().getContentResolver();
-
-        String[] columns = new String[] { SearchHistory.QUERY };
-        String actualQuery = SearchHistory.QUERY + " LIKE ?";
-        String[] queryArgs = new String[] { '%' + searchTerm + '%' };
-
-        String sortOrderAndLimit = SearchHistory.DATE +" DESC LIMIT " + mMaxSavedSuggestions;
-        return cr.query(SearchHistory.CONTENT_URI, columns, actualQuery, queryArgs, sortOrderAndLimit);
+        hideRecycledSuggestions(suggestionCounter, recycledSuggestionCount);
     }
 
     /**
      * Displays suggestions supplied by the search engine, relative to number of suggestions from search history.
      *
      * @param animate whether or not to animate suggestions for visual polish
      * @param recycledSuggestionCount How many suggestion "button" views we could recycle from previous calls
      * @param savedSuggestionCount how many saved searches this searchTerm has
@@ -336,20 +305,20 @@ class SearchEngineRow extends AnimatedHe
      * Updates the whole suggestions UI, the search engine UI, suggestions from the default search engine,
      * and suggestions from search history.
      *
      * This can be called before the opt-in permission prompt is shown or set.
      * Even if both suggestion types are disabled, we need to update the search engine, its image, and the content description.
      *
      * @param searchSuggestionsEnabled whether or not suggestions from the default search engine are enabled
      * @param searchEngine the search engine to use throughout the SearchEngineRow class
-     * @param searchTerm the text from the url to get suggestions on
+     * @param searchHistorySuggestions search history suggestions
      * @param animate whether or not to use animations
      **/
-    public void updateSuggestions(boolean searchSuggestionsEnabled, SearchEngine searchEngine, String searchTerm, boolean animate) {
+    public void updateSuggestions(boolean searchSuggestionsEnabled, SearchEngine searchEngine, List<String> searchHistorySuggestions, boolean animate) {
         mSearchEngine = searchEngine;
         // Set the search engine icon (e.g., Google) for the row.
         mIconView.updateAndScaleImage(mSearchEngine.getIcon(), mSearchEngine.getEngineIdentifier());
         // Set the initial content description.
         setDescriptionOnSuggestion(mUserEnteredTextView, mUserEnteredTextView.getText().toString());
 
         if (!AppConstants.NIGHTLY_BUILD) {
             if (searchSuggestionsEnabled) {
@@ -358,35 +327,22 @@ class SearchEngineRow extends AnimatedHe
             return;
         }
 
         final int recycledSuggestionCount = mSuggestionView.getChildCount();
         final SharedPreferences prefs = GeckoSharedPrefs.forApp(getContext());
         final boolean savedSearchesEnabled = prefs.getBoolean(GeckoPreferences.PREFS_HISTORY_SAVED_SEARCH, true);
 
         if (searchSuggestionsEnabled && savedSearchesEnabled) {
-            final Cursor c = getSavedSearches(searchTerm);
-            try {
-                final int savedSearchCount = (c != null) ? c.getCount() : 0;
-                final int suggestionViewCount = updateFromSearchEngine(animate, recycledSuggestionCount, savedSearchCount);
-                updateFromSavedSearches(c, animate, suggestionViewCount, recycledSuggestionCount);
-            } finally {
-                if (c != null) {
-                    c.close();
-                }
-            }
+            final int savedSearchCount = (searchHistorySuggestions != null) ? searchHistorySuggestions.size() : 0;
+            final int suggestionViewCount = updateFromSearchEngine(animate, recycledSuggestionCount, savedSearchCount);
+            updateFromSavedSearches(searchHistorySuggestions, animate, suggestionViewCount, recycledSuggestionCount);
+
         } else if (savedSearchesEnabled) {
-            final Cursor c = getSavedSearches(searchTerm);
-            try {
-                updateFromSavedSearches(c, animate, 0, recycledSuggestionCount);
-            } finally {
-                if (c != null) {
-                    c.close();
-                }
-            }
+            updateFromSavedSearches(searchHistorySuggestions, animate, 0, recycledSuggestionCount);
         } else if (searchSuggestionsEnabled) {
             updateFromSearchEngine(animate, recycledSuggestionCount, 0);
         }
     }
 
     @Override
     public boolean onKeyDown(int keyCode, android.view.KeyEvent event) {
         final View suggestion = mSuggestionView.getChildAt(mSelectedView);
--- a/mobile/android/base/locales/en-US/android_strings.dtd
+++ b/mobile/android/base/locales/en-US/android_strings.dtd
@@ -220,16 +220,20 @@
 <!ENTITY pref_tap_to_load_images_title "Tap-to-load images">
 <!ENTITY pref_tap_to_load_images_summary2 "Load images only when you long press them">
 
 <!ENTITY pref_tracking_protection_title "Tracking protection">
 <!ENTITY pref_tracking_protection_summary3 "Enabled in Private Browsing">
 <!ENTITY pref_donottrack_title "Do not track">
 <!ENTITY pref_donottrack_summary "&brandShortName; will tell sites that you do not want to be tracked">
 
+<!ENTITY pref_tracking_protection_enabled "Enabled">
+<!ENTITY pref_tracking_protection_enabled_pb "Enabled in Private Browsing">
+<!ENTITY pref_tracking_protection_disabled "Disabled">
+
 <!ENTITY tracking_protection_prompt_title "Now with Tracking Protection">
 <!ENTITY tracking_protection_prompt_text "Actively block tracking elements so you don\'t have to worry.">
 <!ENTITY tracking_protection_prompt_tip_text "Visit Privacy settings to learn more">
 <!ENTITY tracking_protection_prompt_action_button "Got it!">
 
 <!ENTITY tab_queue_toast_message3 "Tab saved in &brandShortName;">
 <!ENTITY tab_queue_toast_action "Open now">
 <!ENTITY tab_queue_prompt_title "Opening multiple links?">
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -649,16 +649,17 @@ gbjar.extra_jars += [
     'gecko-util.jar',
     'sync-thirdparty.jar',
 ]
 
 moz_native_devices_jars = [
     CONFIG['ANDROID_MEDIAROUTER_V7_AAR_LIB'],
     CONFIG['ANDROID_MEDIAROUTER_V7_AAR_INTERNAL_LIB'],
     CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_LIB'],
+    CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'],
     CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_LIB'],
 ]
 moz_native_devices_sources = [
     'ChromeCast.java',
     'GeckoMediaPlayer.java',
     'MediaPlayerManager.java',
     'PresentationMediaPlayerManager.java',
 ]
@@ -667,18 +668,23 @@ if CONFIG['MOZ_NATIVE_DEVICES']:
     gbjar.sources += moz_native_devices_sources
 
     if CONFIG['ANDROID_MEDIAROUTER_V7_AAR']:
         ANDROID_EXTRA_PACKAGES += ['android.support.v7.mediarouter']
         ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_MEDIAROUTER_V7_AAR_RES']]
         resjar.generated_sources += ['android/support/v7/mediarouter/R.java']
 
     if CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR']:
+        ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.base']
+        ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_RES']]
+        resjar.generated_sources += ['com/google/android/gms/base/R.java']
+
+    if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']:
         ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
-        ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_RES']]
+        ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']]
         resjar.generated_sources += ['com/google/android/gms/R.java']
 
     if CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR']:
         ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.cast']
         ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_RES']]
         resjar.generated_sources += ['com/google/android/gms/cast/R.java']
 
 gbjar.extra_jars += [CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB']]
--- a/mobile/android/base/preferences/GeckoPreferences.java
+++ b/mobile/android/base/preferences/GeckoPreferences.java
@@ -123,16 +123,18 @@ OnSharedPreferenceChangeListener
     private static final String PREFS_UPDATER_URL = "app.update.url.android";
     private static final String PREFS_GEO_REPORTING = NON_PREF_PREFIX + "app.geo.reportdata";
     private static final String PREFS_GEO_LEARN_MORE = NON_PREF_PREFIX + "geo.learn_more";
     private static final String PREFS_HEALTHREPORT_LINK = NON_PREF_PREFIX + "healthreport.link";
     private static final String PREFS_DEVTOOLS_REMOTE_USB_ENABLED = "devtools.remote.usb.enabled";
     private static final String PREFS_DEVTOOLS_REMOTE_WIFI_ENABLED = "devtools.remote.wifi.enabled";
     private static final String PREFS_DISPLAY_TITLEBAR_MODE = "browser.chrome.titlebarMode";
     private static final String PREFS_SYNC = NON_PREF_PREFIX + "sync";
+    private static final String PREFS_TRACKING_PROTECTION = "privacy.trackingprotection.state";
+    private static final String PREFS_TRACKING_PROTECTION_PB = "privacy.trackingprotection.pbmode.enabled";
     public static final String PREFS_OPEN_URLS_IN_PRIVATE = NON_PREF_PREFIX + "openExternalURLsPrivately";
     public static final String PREFS_VOICE_INPUT_ENABLED = NON_PREF_PREFIX + "voice_input_enabled";
     public static final String PREFS_QRCODE_ENABLED = NON_PREF_PREFIX + "qrcode_enabled";
     private static final String PREFS_DEVTOOLS = NON_PREF_PREFIX + "devtools.enabled";
     private static final String PREFS_DISPLAY = NON_PREF_PREFIX + "display.enabled";
     private static final String PREFS_CUSTOMIZE_HOME = NON_PREF_PREFIX + "customize_home";
     private static final String PREFS_CUSTOMIZE_IMAGE_BLOCKING = "browser.image_blocking.enabled";
     private static final String PREFS_TRACKING_PROTECTION_PRIVATE_BROWSING = "privacy.trackingprotection.pbmode.enabled";
@@ -767,16 +769,30 @@ OnSharedPreferenceChangeListener
                     }
                 } else if (PREFS_HISTORY_SAVED_SEARCH.equals(key)) {
                     // Remove settings UI if not on Nightly
                     if (!AppConstants.NIGHTLY_BUILD) {
                         preferences.removePreference(pref);
                         i--;
                         continue;
                     }
+                } else if (PREFS_TRACKING_PROTECTION.equals(key)) {
+                    // Remove UI for global TP pref in non-Nightly builds.
+                    if (!AppConstants.NIGHTLY_BUILD) {
+                        preferences.removePreference(pref);
+                        i--;
+                        continue;
+                    }
+                } else if (PREFS_TRACKING_PROTECTION_PB.equals(key)) {
+                    // Remove UI for private-browsing-only TP pref in Nightly builds.
+                    if (AppConstants.NIGHTLY_BUILD) {
+                        preferences.removePreference(pref);
+                        i--;
+                        continue;
+                    }
                 } else if (PREFS_TELEMETRY_ENABLED.equals(key)) {
                     if (!AppConstants.MOZ_TELEMETRY_REPORTING) {
                         preferences.removePreference(pref);
                         i--;
                         continue;
                     }
                 } else if (PREFS_HEALTHREPORT_UPLOAD_ENABLED.equals(key) ||
                            PREFS_HEALTHREPORT_LINK.equals(key)) {
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/drawable/home_history_clear_button_bg.xml
@@ -0,0 +1,23 @@
+<?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/.
+  -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
+    <item>
+        <shape android:shape="rectangle" >
+            <stroke android:width="1dp"
+                    android:color="@color/divider_light" />
+            <padding android:top="1dp" />
+        </shape>
+    </item>
+    <item>
+        <selector>
+            <item android:state_pressed="true"
+                  android:drawable="@color/toolbar_grey_pressed" />
+            <item android:drawable="@color/toolbar_grey"/>
+        </selector>
+    </item>
+</layer-list>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/resources/layout/home_history_clear_button.xml
@@ -0,0 +1,10 @@
+<?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/. -->
+
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+        android:id="@+id/clear_history_button"
+        style="@style/Widget.Home.ActionButton"
+        android:text="@string/home_clear_history_button"
+        android:visibility="gone" />
--- a/mobile/android/base/resources/layout/home_history_panel.xml
+++ b/mobile/android/base/resources/layout/home_history_panel.xml
@@ -5,21 +5,11 @@
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
               android:layout_width="match_parent"
               android:layout_height="match_parent"
               android:orientation="vertical">
 
     <include layout="@layout/home_list"/>
 
-    <LinearLayout android:layout_width="match_parent"
-                  android:layout_height="wrap_content"
-                  android:background="@color/home_button_bar_bg">
-
-        <Button android:id="@+id/clear_history_button"
-                style="@style/Widget.Home.ActionButton"
-                android:text="@string/home_clear_history_button"
-                android:gravity="center"
-                android:visibility="gone"/>
-
-    </LinearLayout>
+    <include layout="@layout/home_history_clear_button"/>
 
 </LinearLayout>
--- a/mobile/android/base/resources/layout/home_history_split_pane_panel.xml
+++ b/mobile/android/base/resources/layout/home_history_split_pane_panel.xml
@@ -36,18 +36,11 @@
             android:id="@+id/list"
             style="@style/Widget.HistoryListView"
             android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_weight="@dimen/split_plane_right_pane_weight"/>
 
     </LinearLayout>
 
-    <Button android:id="@+id/clear_history_button"
-            style="@style/Widget.Home.ActionButton"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            android:background="@color/home_button_bar_bg"
-            android:text="@string/home_clear_history_button"
-            android:gravity="center"
-            android:visibility="gone"/>
+    <include layout="@layout/home_history_clear_button"/>
 
 </LinearLayout>
--- a/mobile/android/base/resources/values-v16/styles.xml
+++ b/mobile/android/base/resources/values-v16/styles.xml
@@ -8,20 +8,16 @@
     <style name="TextAppearance.EmptyMessage" parent="TextAppearance.Large">
         <item name="android:fontFamily">sans-serif-light</item>
     </style>
 
     <style name="TextAppearance.Widget.Home.ItemTitle" parent="TextAppearance.Medium">
         <item name="android:fontFamily">sans-serif-light</item>
     </style>
 
-    <style name="TextAppearance.Widget.Home.PageTitle" parent="TextAppearance.Medium">
-        <item name="android:fontFamily">sans-serif-light</item>
-    </style>
-
     <style name="TextAppearance.FirstrunTextLight">
         <item name="android:fontFamily">sans-serif-light</item>
     </style>
 
     <style name="TextAppearance.FirstrunTextRegular">
         <item name="android:fontFamily">sans-serif</item>
     </style>
 
--- a/mobile/android/base/resources/values/arrays.xml
+++ b/mobile/android/base/resources/values/arrays.xml
@@ -45,16 +45,26 @@
         <item>true</item>
         <item>false</item>
     </string-array>
     <string-array name="pref_cookies_entries">
         <item>@string/pref_cookies_accept_all</item>
         <item>@string/pref_cookies_not_accept_foreign</item>
         <item>@string/pref_cookies_disabled</item>
     </string-array>
+    <string-array name="pref_tracking_protection_values">
+        <item>2</item>
+        <item>1</item>
+        <item>0</item>
+    </string-array>
+    <string-array name="pref_tracking_protection_entries">
+        <item>@string/pref_tracking_protection_enabled</item>
+        <item>@string/pref_tracking_protection_enabled_pb</item>
+        <item>@string/pref_tracking_protection_disabled</item>
+    </string-array>
     <string-array name="pref_cookies_values">
         <item>0</item>
         <item>1</item>
         <item>2</item>
     </string-array>
     <string-array name="pref_import_android_entries">
         <item>@string/bookmarks_title</item>
         <item>@string/history_title</item>
--- a/mobile/android/base/resources/values/styles.xml
+++ b/mobile/android/base/resources/values/styles.xml
@@ -253,31 +253,28 @@
         <item name="android:textAppearance">@style/TextAppearance.Widget.Home.Header</item>
         <item name="android:background">@color/about_page_header_grey</item>
         <item name="android:focusable">false</item>
         <item name="android:gravity">center|left</item>
         <item name="android:paddingLeft">10dip</item>
         <item name="android:paddingRight">10dip</item>
     </style>
 
-    <style name="Widget.Home.PageButton">
+    <style name="Widget.Home.ActionButton">
         <item name="android:layout_width">match_parent</item>
-        <item name="android:layout_height">40dip</item>
-        <item name="android:textAppearance">@style/TextAppearance.Widget.Home.PageTitle</item>
-        <item name="android:background">@drawable/action_bar_button</item>
+        <item name="android:layout_height">48dp</item>
+        <item name="android:textColor">@color/text_and_tabs_tray_grey</item>
+        <item name="android:textSize">14sp</item>
+        <item name="android:background">@drawable/home_history_clear_button_bg</item>
         <item name="android:focusable">true</item>
-        <item name="android:gravity">center|left</item>
+        <item name="android:gravity">center</item>
         <item name="android:paddingLeft">10dip</item>
         <item name="android:paddingRight">10dip</item>
     </style>
 
-    <style name="Widget.Home.ActionButton" parent="Widget.Home.PageButton">
-        <item name="android:textAppearance">@style/TextAppearance.Widget.Home.PageAction</item>
-    </style>
-
     <style name="Widget.Home.ActionItem">
         <item name="android:layout_width">fill_parent</item>
         <item name="android:layout_height">40dip</item>
         <item name="android:textColor">#000000</item>
         <item name="android:gravity">center</item>
     </style>
 
     <style name="Widget.Firstrun.Button" parent="Widget.BaseButton">
@@ -396,22 +393,16 @@
     </style>
 
     <style name="TextAppearance.Widget.Home" />
 
     <style name="TextAppearance.Widget.Home.Header" parent="TextAppearance.Small">
         <item name="android:textColor">?android:attr/textColorPrimary</item>
     </style>
 
-    <style name="TextAppearance.Widget.Home.PageTitle" parent="TextAppearance.Medium" />
-
-    <style name="TextAppearance.Widget.Home.PageAction" parent="TextAppearance.Small">
-        <item name="android:textColor">#00ACFF</item>
-    </style>
-
     <style name="TextAppearance.Widget.Home.ItemTitle" parent="TextAppearance.Medium"/>
 
     <style name="TextAppearance.Widget.Home.ItemDescription" parent="TextAppearance.Micro">
         <item name="android:textColor">@color/tabs_tray_icon_grey</item>
     </style>
 
     <style name="TextAppearance.Widget.HomeBanner" parent="TextAppearance.Small">
         <item name="android:textColor">?android:attr/textColorHint</item>
--- a/mobile/android/base/resources/xml/preferences_privacy.xml
+++ b/mobile/android/base/resources/xml/preferences_privacy.xml
@@ -8,16 +8,22 @@
                   android:title="@string/pref_category_privacy_short"
                   android:enabled="false">
 
     <CheckBoxPreference android:key="privacy.trackingprotection.pbmode.enabled"
                         android:title="@string/pref_tracking_protection_title"
                         android:summary="@string/pref_tracking_protection_summary"
                         android:persistent="false" />
 
+    <ListPreference android:key="privacy.trackingprotection.state"
+                    android:title="@string/pref_tracking_protection_title"
+                    android:entries="@array/pref_tracking_protection_entries"
+                    android:entryValues="@array/pref_tracking_protection_values"
+                    android:persistent="false" />
+
     <org.mozilla.gecko.preferences.AlignRightLinkPreference
             android:key="android.not_a_preference.trackingprotection.learn_more"
             android:title="@string/pref_learn_more"
             android:persistent="false"
             url="https://support.mozilla.org/kb/firefox-android-tracking-protection" />
 
     <CheckBoxPreference android:key="privacy.donottrackheader.enabled"
                         android:title="@string/pref_donottrack_title"
--- a/mobile/android/base/strings.xml.in
+++ b/mobile/android/base/strings.xml.in
@@ -211,16 +211,20 @@
   <string name="pref_tap_to_load_images_title">&pref_tap_to_load_images_title;</string>
   <string name="pref_tap_to_load_images_summary2">&pref_tap_to_load_images_summary2;</string>
 
   <string name="pref_tracking_protection_title">&pref_tracking_protection_title;</string>
   <string name="pref_tracking_protection_summary">&pref_tracking_protection_summary3;</string>
   <string name="pref_donottrack_title">&pref_donottrack_title;</string>
   <string name="pref_donottrack_summary">&pref_donottrack_summary;</string>
 
+  <string name="pref_tracking_protection_enabled">&pref_tracking_protection_enabled;</string>
+  <string name="pref_tracking_protection_enabled_pb">&pref_tracking_protection_enabled_pb;</string>
+  <string name="pref_tracking_protection_disabled">&pref_tracking_protection_disabled;</string>
+
   <string name="pref_char_encoding">&pref_char_encoding;</string>
   <string name="pref_char_encoding_on">&pref_char_encoding_on;</string>
   <string name="pref_char_encoding_off">&pref_char_encoding_off;</string>
   <string name="pref_clear_private_data">&pref_clear_private_data2;</string>
   <string name="pref_clear_private_data_category">&pref_clear_private_data_category;</string>
   <string name="pref_clear_on_exit_title">&pref_clear_on_exit_title2;</string>
   <string name="pref_clear_on_exit_summary2">&pref_clear_on_exit_summary2;</string>
   <string name="pref_clear_on_exit_dialog_title">&pref_clear_on_exit_dialog_title;</string>
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -1409,16 +1409,21 @@ var BrowserApp = {
   },
 
   notifyPrefObservers: function(aPref) {
     this._prefObservers[aPref].forEach(function(aRequestId) {
       this.getPreferences(aRequestId, [aPref], 1);
     }, this);
   },
 
+  // These values come from pref_tracking_protection_entries in arrays.xml.
+  PREF_TRACKING_PROTECTION_ENABLED: "2",
+  PREF_TRACKING_PROTECTION_ENABLED_PB: "1",
+  PREF_TRACKING_PROTECTION_DISABLED: "0",
+
   handlePreferencesRequest: function handlePreferencesRequest(aRequestId,
                                                               aPrefNames,
                                                               aListen) {
 
     let prefs = [];
 
     for (let prefName of aPrefNames) {
       let pref = {
@@ -1447,16 +1452,28 @@ var BrowserApp = {
           prefs.push(pref);
           continue;
         // Handle master password
         case "privacy.masterpassword.enabled":
           pref.type = "bool";
           pref.value = MasterPassword.enabled;
           prefs.push(pref);
           continue;
+        case "privacy.trackingprotection.state": {
+          pref.type = "string";
+          if (Services.prefs.getBoolPref("privacy.trackingprotection.enabled")) {
+            pref.value = this.PREF_TRACKING_PROTECTION_ENABLED;
+          } else if (Services.prefs.getBoolPref("privacy.trackingprotection.pbmode.enabled")) {
+            pref.value = this.PREF_TRACKING_PROTECTION_ENABLED_PB;
+          } else {
+            pref.value = this.PREF_TRACKING_PROTECTION_DISABLED;
+          }
+          prefs.push(pref);
+          continue;
+        }
         // Crash reporter submit pref must be fetched from nsICrashReporter service.
         case "datareporting.crashreporter.submitEnabled":
           let crashReporterBuilt = "nsICrashReporter" in Ci && Services.appinfo instanceof Ci.nsICrashReporter;
           if (crashReporterBuilt) {
             pref.type = "bool";
             pref.value = Services.appinfo.submitReports;
             prefs.push(pref);
           }
@@ -1529,16 +1546,39 @@ var BrowserApp = {
       // MasterPassword pref is not real, we just need take action and leave
       case "privacy.masterpassword.enabled":
         if (MasterPassword.enabled)
           MasterPassword.removePassword(json.value);
         else
           MasterPassword.setPassword(json.value);
         return;
 
+      // "privacy.trackingprotection.state" is not a "real" pref name, but it's used in the setting menu.
+      // By default "privacy.trackingprotection.pbmode.enabled" is true,
+      // and "privacy.trackingprotection.enabled" is false.
+      case "privacy.trackingprotection.state": {
+        switch (json.value) {
+          // Tracking protection disabled.
+          case this.PREF_TRACKING_PROTECTION_DISABLED:
+            Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", false);
+            Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
+            break;
+          // Tracking protection only in private browsing,
+          case this.PREF_TRACKING_PROTECTION_ENABLED_PB:
+            Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
+            Services.prefs.setBoolPref("privacy.trackingprotection.enabled", false);
+            break;
+          // Tracking protection everywhere.
+          case this.PREF_TRACKING_PROTECTION_ENABLED:
+            Services.prefs.setBoolPref("privacy.trackingprotection.pbmode.enabled", true);
+            Services.prefs.setBoolPref("privacy.trackingprotection.enabled", true);
+            break;
+        }
+        return;
+      }
       // Enabling or disabling suggestions will prevent future prompts
       case SearchEngines.PREF_SUGGEST_ENABLED:
         Services.prefs.setBoolPref(SearchEngines.PREF_SUGGEST_PROMPTED, true);
         break;
 
       // Crash reporter preference is in a service; set and return.
       case "datareporting.crashreporter.submitEnabled":
         let crashReporterBuilt = "nsICrashReporter" in Ci && Services.appinfo instanceof Ci.nsICrashReporter;
--- a/mobile/android/gradle/app/build.gradle
+++ b/mobile/android/gradle/app/build.gradle
@@ -1,13 +1,13 @@
 apply plugin: 'com.android.application'
 
 android {
-    compileSdkVersion 22
-    buildToolsVersion "22.0.1"
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 22
         minSdkVersion 9
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
--- a/mobile/android/gradle/base/build.gradle
+++ b/mobile/android/gradle/base/build.gradle
@@ -1,13 +1,13 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 22
-    buildToolsVersion "22.0.1"
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 22
         minSdkVersion 9
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
@@ -71,18 +71,19 @@ android {
 
 dependencies {
     compile 'com.android.support:support-v4:22.2.1'
     compile 'com.android.support:appcompat-v7:22.2.1'
     compile 'com.android.support:recyclerview-v7:22.2.1'
 
     if (mozconfig.substs.MOZ_NATIVE_DEVICES) {
         compile 'com.android.support:mediarouter-v7:22.2.1'
-        compile 'com.google.android.gms:play-services-base:7.8.0'
-        compile 'com.google.android.gms:play-services-cast:7.8.0'
+        compile 'com.google.android.gms:play-services-basement:8.1.0'
+        compile 'com.google.android.gms:play-services-base:8.1.0'
+        compile 'com.google.android.gms:play-services-cast:8.1.0'
     }
 
     compile project(':branding')
     compile project(':preprocessed_code')
     compile project(':preprocessed_resources')
     compile project(':thirdparty')
 
     testCompile 'junit:junit:4.12'
--- a/mobile/android/gradle/branding/build.gradle
+++ b/mobile/android/gradle/branding/build.gradle
@@ -1,13 +1,13 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 22
-    buildToolsVersion "22.0.1"
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 22
         minSdkVersion 9
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
--- a/mobile/android/gradle/preprocessed_code/build.gradle
+++ b/mobile/android/gradle/preprocessed_code/build.gradle
@@ -1,13 +1,13 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 22
-    buildToolsVersion "22.0.1"
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 22
         minSdkVersion 9
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
--- a/mobile/android/gradle/preprocessed_resources/build.gradle
+++ b/mobile/android/gradle/preprocessed_resources/build.gradle
@@ -1,13 +1,13 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 22
-    buildToolsVersion "22.0.1"
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 22
         minSdkVersion 9
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
--- a/mobile/android/gradle/thirdparty/build.gradle
+++ b/mobile/android/gradle/thirdparty/build.gradle
@@ -1,13 +1,13 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 22
-    buildToolsVersion "22.0.1"
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 22
         minSdkVersion 9
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
--- a/mobile/android/gradle/thirdparty_adjust_sdk/build.gradle
+++ b/mobile/android/gradle/thirdparty_adjust_sdk/build.gradle
@@ -1,13 +1,13 @@
 apply plugin: 'com.android.library'
 
 android {
-    compileSdkVersion 22
-    buildToolsVersion "22.0.1"
+    compileSdkVersion 23
+    buildToolsVersion "23.0.1"
 
     defaultConfig {
         targetSdkVersion 22
         minSdkVersion 9
     }
 
     compileOptions {
         sourceCompatibility JavaVersion.VERSION_1_7
--- a/python/mozboot/mozboot/android.py
+++ b/python/mozboot/mozboot/android.py
@@ -9,18 +9,18 @@ import errno
 import os
 import stat
 import subprocess
 
 
 # These are the platform and build-tools versions for building
 # mobile/android, respectively. Try to keep these in synch with the
 # build system and Mozilla's automation.
-ANDROID_TARGET_SDK = '22'
-ANDROID_BUILD_TOOLS_VERSION = '22.0.1'
+ANDROID_TARGET_SDK = '23'
+ANDROID_BUILD_TOOLS_VERSION = '23.0.1'
 
 # These are the "Android packages" needed for building Firefox for Android.
 # Use |android list sdk --extended| to see these identifiers.
 ANDROID_PACKAGES = [
     'tools',
     'platform-tools-preview', # Temporarily, tools depends on platform-tools-preview.
     'build-tools-%s' % ANDROID_BUILD_TOOLS_VERSION,
     'android-%s' % ANDROID_TARGET_SDK,
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -9033,17 +9033,17 @@
   },
   "FENNEC_TABQUEUE_ENABLED": {
     "expires_in_version": "never",
     "kind": "boolean",
     "description": "Has the tab queue functionality been enabled."
   },
   "FENNEC_OPEN_URLS_IN_PRIVATE": {
     "alert_emails": ["margaret@mozilla.com"],
-    "expires_in_version": "44",
+    "expires_in_version": "45",
     "kind": "flag",
     "description": "Reports the state of the open external links in private tabs preference"
   },
   "VIDEO_EME_DISABLED": {
     "alert_emails": ["edwin@mozilla.com"],
     "expires_in_version": "45",
     "kind": "boolean",
     "description": "Set if media.eme.enabled is false, in a build that supports the Adobe Primetime Content Decryption Module."
--- a/toolkit/components/telemetry/TelemetryController.jsm
+++ b/toolkit/components/telemetry/TelemetryController.jsm
@@ -904,19 +904,35 @@ var Impl = {
    * the preferences panel), this triggers sending the deletion ping.
    */
   _onUploadPrefChange: function() {
     const uploadEnabled = Preferences.get(PREF_FHR_UPLOAD_ENABLED, false);
     if (uploadEnabled) {
       // There's nothing we should do if we are enabling upload.
       return;
     }
-    // Send the deletion ping.
-    this._log.trace("_onUploadPrefChange - Sending deletion ping.");
-    this.submitExternalPing(PING_TYPE_DELETION, {}, { addClientId: true });
+
+    let p = Task.spawn(function*() {
+      try {
+        // Clear the current pings.
+        yield TelemetrySend.clearCurrentPings();
+
+        // Remove all the pending pings, but not the deletion ping.
+        yield TelemetryStorage.runRemovePendingPingsTask();
+      } catch (e) {
+        this._log.error("_onUploadPrefChange - error clearing pending pings", e);
+      } finally {
+        // Always send the deletion ping.
+        this._log.trace("_onUploadPrefChange - Sending deletion ping.");
+        this.submitExternalPing(PING_TYPE_DELETION, {}, { addClientId: true });
+      }
+    }.bind(this));
+
+    this._shutdownBarrier.client.addBlocker(
+      "TelemetryController: removing pending pings after data upload was disabled", p);
   },
 
   _attachObservers: function() {
     if (IS_UNIFIED_TELEMETRY) {
       // Watch the FHR upload setting to trigger deletion pings.
       Preferences.observe(PREF_FHR_UPLOAD_ENABLED, this._onUploadPrefChange, this);
     }
   },
--- a/toolkit/components/telemetry/TelemetrySend.jsm
+++ b/toolkit/components/telemetry/TelemetrySend.jsm
@@ -230,16 +230,23 @@ this.TelemetrySend = {
   /**
    * Only used in tests.
    */
   setServer: function(server) {
     return TelemetrySendImpl.setServer(server);
   },
 
   /**
+   * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests.
+   */
+  clearCurrentPings: function() {
+    return TelemetrySendImpl.clearCurrentPings();
+  },
+
+  /**
    * Only used in tests to wait on outgoing pending pings.
    */
   testWaitOnOutgoingPings: function() {
     return TelemetrySendImpl.promisePendingPingActivity();
   },
 
   /**
    * Test-only - this allows overriding behavior to enable ping sending in debug builds.
@@ -321,26 +328,29 @@ var SendScheduler = {
 
   shutdown: function() {
     this._log.trace("shutdown");
     this._shutdown = true;
     CancellableTimeout.cancelTimeout();
     return Promise.resolve(this._sendTask);
   },
 
+  start: function() {
+    this._log.trace("start");
+    this._sendsFailed = false;
+    this._backoffDelay = SEND_TICK_DELAY;
+    this._shutdown = false;
+  },
+
   /**
    * Only used for testing, resets the state to emulate a restart.
    */
   reset: function() {
     this._log.trace("reset");
-    return this.shutdown().then(() => {
-      this._sendsFailed = false;
-      this._backoffDelay = SEND_TICK_DELAY;
-      this._shutdown = false;
-    });
+    return this.shutdown().then(() => this.start());
   },
 
   /**
    * Notify the scheduler of a failure in sending out pings that warrants retrying.
    * This will trigger the exponential backoff timer behavior on the next tick.
    */
   notifySendsFailed: function() {
     this._log.trace("notifySendsFailed");
@@ -516,16 +526,18 @@ var SendScheduler = {
       sendTaskState: this._sendTaskState,
       backoffDelay: this._backoffDelay,
     };
   },
  };
 
 var TelemetrySendImpl = {
   _sendingEnabled: false,
+  // Tracks the shutdown state.
+  _shutdown: false,
   _logger: null,
   // This tracks all pending ping requests to the server.
   _pendingPingRequests: new Map(),
   // This tracks all the pending async ping activity.
   _pendingPingActivity: new Set(),
   // This is true when running in the test infrastructure.
   _testMode: false,
   // This holds pings that we currently try and haven't persisted yet.
@@ -611,16 +623,18 @@ var TelemetrySendImpl = {
     for (let pingInfo of infos) {
       const ageInDays =
         Utils.millisecondsToDays(Math.abs(now.getTime() - pingInfo.lastModificationDate));
       Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_AGE").add(ageInDays);
     }
    }),
 
   shutdown: Task.async(function*() {
+    this._shutdown = true;
+
     for (let topic of this.OBSERVER_TOPICS) {
       try {
         Services.obs.removeObserver(this, topic);
       } catch (ex) {
         this._log.error("shutdown - failed to remove observer for " + topic, ex);
       }
     }
 
@@ -638,18 +652,18 @@ var TelemetrySendImpl = {
 
     // Save any outstanding pending pings to disk.
     yield this._persistCurrentPings();
   }),
 
   reset: function() {
     this._log.trace("reset");
 
+    this._shutdown = false;
     this._currentPings = new Map();
-
     this._overduePingCount = 0;
 
     const histograms = [
       "TELEMETRY_SUCCESS",
       "TELEMETRY_SEND",
       "TELEMETRY_PING",
     ];
 
@@ -701,16 +715,48 @@ var TelemetrySendImpl = {
   /**
    * Only used in tests.
    */
   setServer: function (server) {
     this._log.trace("setServer", server);
     this._server = server;
   },
 
+  /**
+   * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests.
+   */
+  clearCurrentPings: Task.async(function*() {
+    if (this._shutdown) {
+      this._log.trace("clearCurrentPings - in shutdown, bailing out");
+      return;
+    }
+
+    // Temporarily disable the scheduler. It must not try to reschedule ping sending
+    // while we're deleting them.
+    yield SendScheduler.shutdown();
+
+    // Now that the ping activity has settled, abort outstanding ping requests.
+    this._cancelOutgoingRequests();
+
+    // Also, purge current pings.
+    this._currentPings.clear();
+
+    // We might have been interrupted and shutdown could have been started.
+    // We need to bail out in that case to avoid triggering send activity etc.
+    // at unexpected times.
+    if (this._shutdown) {
+      this._log.trace("clearCurrentPings - in shutdown, not spinning SendScheduler up again");
+      return;
+    }
+
+    // Enable the scheduler again and spin the send task.
+    SendScheduler.start();
+    SendScheduler.triggerSendingPings(true);
+  }),
+
   _cancelOutgoingRequests: function() {
     // Abort any pending ping XHRs.
     for (let [id, request] of this._pendingPingRequests) {
       this._log.trace("_cancelOutgoingRequests - aborting ping request for id " + id);
       try {
         request.abort();
       } catch (e) {
         this._log.error("_cancelOutgoingRequests - failed to abort request for id " + id, e);
--- a/toolkit/components/telemetry/TelemetryStorage.jsm
+++ b/toolkit/components/telemetry/TelemetryStorage.jsm
@@ -111,32 +111,19 @@ var Policy = {
                                 : PENDING_PINGS_QUOTA_BYTES_DESKTOP,
 };
 
 /**
  * Wait for all promises in iterable to resolve or reject. This function
  * always resolves its promise with undefined, and never rejects.
  */
 function waitForAll(it) {
-  let list = Array.from(it);
-  let pending = list.length;
-  if (pending == 0) {
-    return Promise.resolve();
-  }
-  return new Promise(function(resolve, reject) {
-    let rfunc = () => {
-      --pending;
-      if (pending == 0) {
-        resolve();
-      }
-    };
-    for (let p of list) {
-      p.then(rfunc, rfunc);
-    }
-  });
+  let dummy = () => {};
+  let promises = [for (p of it) p.catch(dummy)];
+  return Promise.all(promises);
 }
 
 this.TelemetryStorage = {
   get pingDirectoryPath() {
     return OS.Path.join(OS.Constants.Path.profileDir, "saved-telemetry-pings");
   },
 
   /**
@@ -200,16 +187,25 @@ this.TelemetryStorage = {
    *
    * @return {Promise} Resolved when the cleanup task completes.
    */
   runEnforcePendingPingsQuotaTask: function() {
     return TelemetryStorageImpl.runEnforcePendingPingsQuotaTask();
   },
 
   /**
+   * Run the task to remove all the pending pings (except the deletion ping).
+   *
+   * @return {Promise} Resolved when the pings are removed.
+   */
+  runRemovePendingPingsTask: function() {
+    return TelemetryStorageImpl.runRemovePendingPingsTask();
+  },
+
+  /**
    * Reset the storage state in tests.
    */
   reset: function() {
     return TelemetryStorageImpl.reset();
   },
 
   /**
    * Test method that allows waiting on the archive clean task to finish.
@@ -531,16 +527,22 @@ var TelemetryStorageImpl = {
   _activelyArchiving: new Set(),
   // Track the archive loading task to prevent multiple tasks from being executed.
   _scanArchiveTask: null,
   // Track the archive cleanup task.
   _cleanArchiveTask: null,
   // Whether we already scanned the archived pings on disk.
   _scannedArchiveDirectory: false,
 
+  // Track the pending ping removal task.
+  _removePendingPingsTask: null,
+
+  // This tracks all the pending async ping save activity.
+  _activePendingPingSaves: new Set(),
+
   // Tracks the pending pings in a Map of (id -> {timestampCreated, type}).
   // We use this to cache info on pending pings to avoid scanning the disk more than once.
   _pendingPings: new Map(),
 
   // Track the pending pings enforce quota task.
   _enforcePendingPingsQuotaTask: null,
 
   // Track the shutdown process to bail out of the clean up task quickly.
@@ -556,22 +558,50 @@ var TelemetryStorageImpl = {
 
   /**
    * Shutdown & block on any outstanding async activity in this module.
    *
    * @return {Promise} Promise that is resolved when shutdown is complete.
    */
   shutdown: Task.async(function*() {
     this._shutdown = true;
-    yield this._abortedSessionSerializer.flushTasks();
-    yield this._deletionPingSerializer.flushTasks();
-    // If the tasks for archive cleaning or pending ping quota are still running, block on
-    // them. They will bail out as soon as possible.
-    yield this._cleanArchiveTask;
-    yield this._enforcePendingPingsQuotaTask;
+
+    // If the following tasks are still running, block on them. They will bail out as soon
+    // as possible.
+    yield this._abortedSessionSerializer.flushTasks().catch(ex => {
+      this._log.error("shutdown - failed to flush aborted-session writes", ex);
+    });
+
+    yield this._deletionPingSerializer.flushTasks().catch(ex => {
+      this._log.error("shutdown - failed to flush deletion ping writes", ex);
+    });
+
+    if (this._cleanArchiveTask) {
+      yield this._cleanArchiveTask.catch(ex => {
+        this._log.error("shutdown - the archive cleaning task failed", ex);
+      });
+    }
+
+    if (this._enforcePendingPingsQuotaTask) {
+      yield this._enforcePendingPingsQuotaTask.catch(ex => {
+        this._log.error("shutdown - the pending pings quota task failed", ex);
+      });
+    }
+
+    if (this._removePendingPingsTask) {
+      yield this._removePendingPingsTask.catch(ex => {
+        this._log.error("shutdown - the pending pings removal task failed", ex);
+      });
+    }
+
+    // Wait on pending pings still being saved. While OS.File should have shutdown
+    // blockers in place, we a) have seen weird errors being reported that might
+    // indicate a bad shutdown path and b) might have completion handlers hanging
+    // off the save operations that don't expect to be late in shutdown.
+    yield this.promisePendingPingSaves();
   }),
 
   /**
    * Save an archived ping to disk.
    *
    * @param {object} ping The ping data to archive.
    * @return {promise} Promise that is resolved when the ping is successfully archived.
    */
@@ -1166,23 +1196,25 @@ var TelemetryStorageImpl = {
    * @param {object} ping The ping.
    * @returns {promise}
    */
   cleanupPingFile: function(ping) {
     return OS.File.remove(pingFilePath(ping));
   },
 
   savePendingPing: function(ping) {
-    return this.savePing(ping, true).then((path) => {
+    let p = this.savePing(ping, true).then((path) => {
       this._pendingPings.set(ping.id, {
         path: path,
         lastModificationDate: Policy.now().getTime(),
       });
       this._log.trace("savePendingPing - saved ping with id " + ping.id);
     });
+    this._trackPendingPingSaveTask(p);
+    return p;
   },
 
   loadPendingPing: Task.async(function*(id) {
     this._log.trace("loadPendingPing - id: " + id);
     let info = this._pendingPings.get(id);
     if (!info) {
       this._log.trace("loadPendingPing - unknown id " + id);
       throw new Error("TelemetryStorage.loadPendingPing - no ping with id " + id);
@@ -1234,16 +1266,90 @@ var TelemetryStorageImpl = {
 
     this._log.trace("removePendingPing - deleting ping with id: " + id +
                     ", path: " + info.path);
     this._pendingPings.delete(id);
     return OS.File.remove(info.path).catch((ex) =>
       this._log.error("removePendingPing - failed to remove ping", ex));
   },
 
+  /**
+   * Track any pending ping save tasks through the promise passed here.
+   * This is needed to block on any outstanding ping save activity.
+   *
+   * @param {Object<Promise>} The save promise to track.
+   */
+  _trackPendingPingSaveTask: function (promise) {
+    let clear = () => this._activePendingPingSaves.delete(promise);
+    promise.then(clear, clear);
+    this._activePendingPingSaves.add(promise);
+  },
+
+  /**
+   * Return a promise that allows to wait on pending pings being saved.
+   * @return {Object<Promise>} A promise resolved when all the pending pings save promises
+   *         are resolved.
+   */
+  promisePendingPingSaves: function () {
+    // Make sure to wait for all the promises, even if they reject. We don't need to log
+    // the failures here, as they are already logged elsewhere.
+    return waitForAll(this._activePendingPingSaves);
+  },
+
+  /**
+   * Run the task to remove all the pending pings (except the deletion ping).
+   *
+   * @return {Promise} Resolved when the pings are removed.
+   */
+  runRemovePendingPingsTask: Task.async(function*() {
+    // If we already have a pending pings removal task active, return that.
+    if (this._removePendingPingsTask) {
+      return this._removePendingPingsTask;
+    }
+
+    // Start the task to remove all pending pings. Also make sure to clear the task once done.
+    try {
+      this._removePendingPingsTask = this.removePendingPings();
+      yield this._removePendingPingsTask;
+    } finally {
+      this._removePendingPingsTask = null;
+    }
+  }),
+
+  removePendingPings: Task.async(function*() {
+    this._log.trace("removePendingPings - removing all pending pings");
+
+    // Wait on pending pings still being saved, so so we don't miss removing them.
+    yield this.promisePendingPingSaves();
+
+    // Individually remove existing pings, so we don't interfere with operations expecting
+    // the pending pings directory to exist.
+    const directory = TelemetryStorage.pingDirectoryPath;
+    let iter = new OS.File.DirectoryIterator(directory);
+
+    try {
+      if (!(yield iter.exists())) {
+        this._log.trace("removePendingPings - the pending pings directory doesn't exist");
+        return;
+      }
+
+      let files = (yield iter.nextBatch()).filter(e => !e.isDir);
+      for (let file of files) {
+        try {
+          yield OS.File.remove(file.path);
+        } catch (ex) {
+          this._log.error("removePendingPings - failed to remove file " + file.path, ex);
+          continue;
+        }
+      }
+    } finally {
+      yield iter.close();
+    }
+  }),
+
   loadPendingPingList: function() {
     // If we already have a pending scanning task active, return that.
     if (this._scanPendingPingsTask) {
       return this._scanPendingPingsTask;
     }
 
     if (this._scannedPendingDirectory) {
       this._log.trace("loadPendingPingList - Pending already scanned, hitting cache.");
@@ -1485,18 +1591,20 @@ var TelemetryStorageImpl = {
    * Save the deletion ping.
    * @param ping The deletion ping.
    * @return {Promise} Resolved when the ping is saved.
    */
   saveDeletionPing: Task.async(function*(ping) {
     this._log.trace("saveDeletionPing - ping path: " + gDeletionPingFilePath);
     yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true });
 
-    return this._deletionPingSerializer.enqueueTask(() =>
+    let p = this._deletionPingSerializer.enqueueTask(() =>
       this.savePingToFile(ping, gDeletionPingFilePath, true));
+    this._trackPendingPingSaveTask(p);
+    return p;
   }),
 
   /**
    * Remove the deletion ping.
    * @return {Promise} Resolved when the ping is deleted from the disk.
    */
   removeDeletionPing: Task.async(function*() {
     return this._deletionPingSerializer.enqueueTask(Task.async(function*() {
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -138,17 +138,17 @@ add_task(function* test_simplePing() {
   // Make sure the version in the query string matches the new ping format version.
   let params = request.queryString.split("&");
   Assert.ok(params.find(p => p == ("v=" + PING_FORMAT_VERSION)));
 
   let ping = decodeRequestPayload(request);
   checkPingFormat(ping, TEST_PING_TYPE, false, false);
 });
 
-add_task(function* test_deletionPing() {
+add_task(function* test_disableDataUpload() {
   const isUnified = Preferences.get(PREF_UNIFIED, false);
   if (!isUnified) {
     // Skipping the test if unified telemetry is off, as no deletion ping will
     // be generated.
     return;
   }
 
   const PREF_TELEMETRY_SERVER = "toolkit.telemetry.server";
@@ -161,22 +161,36 @@ add_task(function* test_deletionPing() {
   // Wait on ping activity to settle.
   yield TelemetrySend.testWaitOnOutgoingPings();
 
   // Restore FHR Upload.
   Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
 
   // Simulate a failure in sending the deletion ping by disabling the HTTP server.
   yield PingServer.stop();
+
+  // Try to send a ping. It will be saved as pending  and get deleted when disabling upload.
+  TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
   // Disable FHR upload to send a deletion ping again.
   Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
-  // Wait for the send task to terminate, flagging it to do so at the next opportunity and
-  // cancelling any timeouts.
+
+  // Wait on sending activity to settle, as |TelemetryController.reset()| doesn't do that.
+  yield TelemetrySend.testWaitOnOutgoingPings();
+  // Wait for the pending pings to be deleted. Resetting TelemetryController doesn't
+  // trigger the shutdown, so we need to call it ourselves.
+  yield TelemetryStorage.shutdown();
+  // Simulate a restart, and spin the send task.
   yield TelemetryController.reset();
 
+  // Disabling Telemetry upload must clear out all the pending pings.
+  let pendingPings = yield TelemetryStorage.loadPendingPingList();
+  Assert.equal(pendingPings.length, 1,
+               "All the pending pings but the deletion ping should have been deleted");
+
   // Enable the ping server again.
   PingServer.start();
   // We set the new server using the pref, otherwise it would get reset with
   // |TelemetryController.reset|.
   Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port);
 
   // Reset the controller to spin the ping sending task.
   yield TelemetryController.reset();
@@ -264,17 +278,17 @@ add_task(function* test_archivePings() {
   let promise = TelemetryArchive.promiseArchivedPingById(pingId);
   Assert.ok((yield promiseRejects(promise)),
     "TelemetryController should not archive pings if the archive pref is disabled.");
 
   // Enable archiving and the upload so that pings get sent and archived again.
   Preferences.set(uploadPref, true);
   Preferences.set(PREF_ARCHIVE_ENABLED, true);
 
-  now = new Date(2014, 06, 18, 22, 0, 0);
+  now = new Date(2014, 6, 18, 22, 0, 0);
   fakeNow(now);
   // Restore the non asserting ping handler.
   PingServer.resetPingHandler();
   pingId = yield sendPing(true, true);
 
   // Check that we archive pings when successfully sending them.
   yield PingServer.promiseNextPing();
   ping = yield TelemetryArchive.promiseArchivedPingById(pingId);
--- a/toolkit/locales/en-US/chrome/alerts/alert.properties
+++ b/toolkit/locales/en-US/chrome/alerts/alert.properties
@@ -4,8 +4,9 @@
 
 # LOCALIZATION NOTE(closeButton.title): Used as the close button text for web notifications on OS X.
 # This should ideally match the string that OS X uses for the close button on alert-type
 # notifications. OS X will truncate the value if it's too long.
 closeButton.title = Close
 # LOCALIZATION NOTE(actionButton.label): Used as the button label to provide more actions on OS X notifications. OS X will truncate this if it's too long.
 actionButton.label = …
 webActions.disable.label = Disable notifications from this site
+webActions.settings.label = Notification settings
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -542,17 +542,16 @@ PopupNotifications.prototype = {
         gNotificationParents.set(popupnotification, popupnotification.parentNode);
       else
         popupnotification = doc.createElementNS(XUL_NS, "popupnotification");
 
       popupnotification.setAttribute("label", n.message);
       popupnotification.setAttribute("id", popupnotificationID);
       popupnotification.setAttribute("popupid", n.id);
       popupnotification.setAttribute("closebuttoncommand", "PopupNotifications._dismiss();");
-      popupnotification.setAttribute("noautofocus", "true");
       if (n.mainAction) {
         popupnotification.setAttribute("buttonlabel", n.mainAction.label);
         popupnotification.setAttribute("buttonaccesskey", n.mainAction.accessKey);
         popupnotification.setAttribute("buttoncommand", "PopupNotifications._onButtonCommand(event);");
         popupnotification.setAttribute("menucommand", "PopupNotifications._onMenuCommand(event);");
         popupnotification.setAttribute("closeitemcommand", "PopupNotifications._dismiss();event.stopPropagation();");
       } else {
         popupnotification.removeAttribute("buttonlabel");
@@ -864,16 +863,19 @@ PopupNotifications.prototype = {
     }
 
     // If the panel is not closed, and the anchor is different, immediately mark all
     // active notifications for the previous anchor as dismissed
     if (this.panel.state != "closed" && anchor != this._currentAnchorElement) {
       this._dismissOrRemoveCurrentNotifications();
     }
 
+    // Ensure we move focus into the panel because it's opened through user interaction:
+    this.panel.removeAttribute("noautofocus", "true");
+
     this._reshowNotifications(anchor);
   },
 
   _reshowNotifications: function PopupNotifications_reshowNotifications(anchor, browser) {
     // Mark notifications anchored to this anchor as un-dismissed
     let notifications = this._getNotificationsForBrowser(browser || this.tabbrowser.selectedBrowser);
     notifications.forEach(function (n) {
       if (n.anchorElement == anchor)
@@ -944,16 +946,20 @@ PopupNotifications.prototype = {
     if (event.target != this.panel || this._ignoreDismissal) {
       if (this._ignoreDismissal) {
         this._ignoreDismissal.resolve();
         this._ignoreDismissal = null;
       }
       return;
     }
 
+    // Ensure that when the panel comes up without user interaction,
+    // we don't autofocus it.
+    this.panel.setAttribute("noautofocus", "true");
+
     this._dismissOrRemoveCurrentNotifications();
 
     this._clearPanel();
 
     this._update();
   },
 
   _dismissOrRemoveCurrentNotifications: function() {
--- a/widget/cocoa/OSXNotificationCenter.mm
+++ b/widget/cocoa/OSXNotificationCenter.mm
@@ -134,17 +134,18 @@ enum {
   mOSXNC->CloseAlertCocoaString(name);
 }
 
 @end
 
 namespace mozilla {
 
 enum {
-  OSXNotificationActionDisable = 0
+  OSXNotificationActionDisable = 0,
+  OSXNotificationActionSettings = 1,
 };
 
 class OSXNotificationInfo {
 private:
   ~OSXNotificationInfo();
 
 public:
   NS_INLINE_DECL_REFCOUNTING(OSXNotificationInfo)
@@ -248,31 +249,34 @@ OSXNotificationCenter::ShowAlertNotifica
 
   // If this is not an application/extension alert, show additional actions dealing with permissions.
   if (aPrincipal && !nsContentUtils::IsSystemOrExpandedPrincipal(aPrincipal)
       && !aPrincipal->GetIsNullPrincipal()) {
     nsCOMPtr<nsIStringBundleService> sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID);
     nsCOMPtr<nsIStringBundle> bundle;
     nsresult rv = sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle));
     if (NS_SUCCEEDED(rv)) {
-      nsXPIDLString closeButtonTitle, actionButtonTitle, disableButtonTitle;
+      nsXPIDLString closeButtonTitle, actionButtonTitle, disableButtonTitle, settingsButtonTitle;
       bundle->GetStringFromName(NS_LITERAL_STRING("closeButton.title").get(),
                                 getter_Copies(closeButtonTitle));
       bundle->GetStringFromName(NS_LITERAL_STRING("actionButton.label").get(),
                                 getter_Copies(actionButtonTitle));
       bundle->GetStringFromName(NS_LITERAL_STRING("webActions.disable.label").get(),
                                 getter_Copies(disableButtonTitle));
+      bundle->GetStringFromName(NS_LITERAL_STRING("webActions.settings.label").get(),
+                                getter_Copies(settingsButtonTitle));
 
       notification.hasActionButton = YES;
       notification.otherButtonTitle = nsCocoaUtils::ToNSString(closeButtonTitle);
       notification.actionButtonTitle = nsCocoaUtils::ToNSString(actionButtonTitle);
       [(NSObject*)notification setValue:@(YES) forKey:@"_showsButtons"];
       [(NSObject*)notification setValue:@(YES) forKey:@"_alwaysShowAlternateActionMenu"];
       [(NSObject*)notification setValue:@[
-                                          nsCocoaUtils::ToNSString(disableButtonTitle)
+                                          nsCocoaUtils::ToNSString(disableButtonTitle),
+                                          nsCocoaUtils::ToNSString(settingsButtonTitle)
                                           ]
                                forKey:@"_alternateActionButtonTitles"];
     }
   }
   NSString *alertName = [NSString stringWithCharacters:(const unichar *)aAlertName.BeginReading() length:aAlertName.Length()];
   if (!alertName) {
     return NS_ERROR_FAILURE;
   }
@@ -389,16 +393,19 @@ OSXNotificationCenter::OnActivate(NSStri
       if (osxni->mObserver) {
         switch ((int)aActivationType) {
           case NSUserNotificationActivationTypeAdditionalActionClicked:
           case NSUserNotificationActivationTypeActionButtonClicked:
             switch (aAdditionalActionIndex) {
               case OSXNotificationActionDisable:
                 osxni->mObserver->Observe(nullptr, "alertdisablecallback", osxni->mCookie.get());
                 break;
+              case OSXNotificationActionSettings:
+                osxni->mObserver->Observe(nullptr, "alertsettingscallback", osxni->mCookie.get());
+                break;
               default:
                 NS_WARNING("Unknown NSUserNotification additional action clicked");
                 break;
             }
             break;
           default:
             osxni->mObserver->Observe(nullptr, "alertclickcallback", osxni->mCookie.get());
             break;