Merge fx-team to central, a=merge CLOSED TREE
authorWes Kocher <wkocher@mozilla.com>
Tue, 21 Jun 2016 10:10:21 -0700
changeset 302209 51377a64158941f89ed73f388ae437cfa494c030
parent 302199 20167cee53dcbb90a2af3142306c64331ebda025 (current diff)
parent 302208 eb945fc547afc9a4254ef339335fe831c0f68db2 (diff)
child 302210 5ac2f675c083d59ef6329d164cf92733ab344557
child 302314 448ad5bf7e92885c8c73acb0aee5d3e217712f95
push id78647
push userkwierso@gmail.com
push dateTue, 21 Jun 2016 17:12:12 +0000
treeherdermozilla-inbound@5ac2f675c083 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone50.0a1
first release with
nightly linux32
51377a641589 / 50.0a1 / 20160622030210 / files
nightly linux64
51377a641589 / 50.0a1 / 20160622030210 / files
nightly mac
51377a641589 / 50.0a1 / 20160622030210 / files
nightly win32
51377a641589 / 50.0a1 / 20160622030210 / files
nightly win64
51377a641589 / 50.0a1 / 20160622030210 / 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 central, a=merge CLOSED TREE
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -403,16 +403,24 @@ const gSessionHistoryObserver = {
     // Hide session restore button on about:home
     window.messageManager.broadcastAsyncMessage("Browser:HideSessionRestoreButton");
 
     // Clear undo history of the URL bar
     gURLBar.editor.transactionManager.clear()
   }
 };
 
+const gPermissionObserver = {
+  observe: function(subject, topic, data) {
+    if (topic === "perm-changed") {
+      gIdentityHandler.refreshIdentityBlock();
+    }
+  }
+};
+
 /**
  * Given a starting docshell and a URI to look up, find the docshell the URI
  * is loaded in.
  * @param   aDocument
  *          A document to find instead of using just a URI - this is more specific.
  * @param   aDocShell
  *          The doc shell to start at
  * @param   aSoughtURI
@@ -1166,16 +1174,17 @@ var gBrowserInit = {
       }
     }
 
     if (AppConstants.MOZ_SAFE_BROWSING) {
       // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008.
       setTimeout(function() { SafeBrowsing.init(); }, 2000);
     }
 
+    Services.obs.addObserver(gPermissionObserver, "perm-changed", false);
     Services.obs.addObserver(gSessionHistoryObserver, "browser:purge-session-history", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-started", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-origin-blocked", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-failed", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation", false);
     Services.obs.addObserver(gXPInstallObserver, "addon-install-complete", false);
@@ -1489,16 +1498,17 @@ var gBrowserInit = {
         Win7Features.onCloseWindow();
 
       gPrefService.removeObserver(ctrlTab.prefName, ctrlTab);
       ctrlTab.uninit();
       SocialUI.uninit();
       gBrowserThumbnails.uninit();
       FullZoom.destroy();
 
+      Services.obs.removeObserver(gPermissionObserver, "perm-changed");
       Services.obs.removeObserver(gSessionHistoryObserver, "browser:purge-session-history");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-origin-blocked");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-confirmation");
       Services.obs.removeObserver(gXPInstallObserver, "addon-install-complete");
--- a/browser/base/content/test/general/browser_permissions.js
+++ b/browser/base/content/test/general/browser_permissions.js
@@ -53,31 +53,27 @@ add_task(function* testMainViewVisible()
 });
 
 add_task(function* testIdentityIcon() {
   let {gIdentityHandler} = gBrowser.ownerGlobal;
   let tab = gBrowser.selectedTab = gBrowser.addTab();
   yield promiseTabLoadEvent(tab, PERMISSIONS_PAGE);
 
   gIdentityHandler.setPermission("geo", SitePermissions.ALLOW);
-  gIdentityHandler.refreshIdentityBlock();
 
   ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box signals granted permssions");
 
   gIdentityHandler.setPermission("geo", SitePermissions.getDefault("geo"));
-  gIdentityHandler.refreshIdentityBlock();
 
   ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box doesn't signal granted permssions");
 
   gIdentityHandler.setPermission("camera", SitePermissions.BLOCK);
-  gIdentityHandler.refreshIdentityBlock();
 
   ok(!gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box doesn't signal granted permssions");
 
   gIdentityHandler.setPermission("cookie", SitePermissions.SESSION);
-  gIdentityHandler.refreshIdentityBlock();
 
   ok(gIdentityHandler._identityBox.classList.contains("grantedPermissions"),
     "identity-box signals granted permssions");
 });
--- a/devtools/client/inspector/computed/computed.js
+++ b/devtools/client/inspector/computed/computed.js
@@ -200,17 +200,17 @@ function CssComputedView(inspector, docu
   gDevTools.on("pref-changed", this._handlePrefChange);
 
   // Refresh panel when pref for showing original sources changes
   this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
   this._prefObserver = new PrefObserver("devtools.");
   this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
 
   // The element that we're inspecting, and the document that it comes from.
-  this.viewedElement = null;
+  this._viewedElement = null;
 
   this.createStyleViews();
 
   this._contextmenu = new StyleInspectorMenu(this, { isRuleView: false });
 
   // Add the tooltips and highlightersoverlay
   this.tooltips = new overlays.TooltipsOverlay(this);
   this.tooltips.addToView();
@@ -271,34 +271,34 @@ CssComputedView.prototype = {
    * will show the style information for the given element.
    *
    * @param {NodeFront} element
    *        The highlighted node to get styles for.
    * @returns a promise that will be resolved when highlighting is complete.
    */
   selectElement: function (element) {
     if (!element) {
-      this.viewedElement = null;
+      this._viewedElement = null;
       this.noResults.hidden = false;
 
       if (this._refreshProcess) {
         this._refreshProcess.cancel();
       }
       // Hiding all properties
       for (let propView of this.propertyViews) {
         propView.refresh();
       }
       return promise.resolve(undefined);
     }
 
-    if (element === this.viewedElement) {
+    if (element === this._viewedElement) {
       return promise.resolve(undefined);
     }
 
-    this.viewedElement = element;
+    this._viewedElement = element;
     this.refreshSourceFilter();
 
     return this.refreshPanel();
   },
 
   /**
    * Get the type of a given node in the computed-view
    *
@@ -430,33 +430,33 @@ CssComputedView.prototype = {
     this._createViewsProcess.schedule();
     return deferred.promise;
   },
 
   /**
    * Refresh the panel content.
    */
   refreshPanel: function () {
-    if (!this.viewedElement) {
+    if (!this._viewedElement) {
       return promise.resolve();
     }
 
     // Capture the current viewed element to return from the promise handler
     // early if it changed
-    let viewedElement = this.viewedElement;
+    let viewedElement = this._viewedElement;
 
     return promise.all([
       this._createPropertyViews(),
-      this.pageStyle.getComputed(this.viewedElement, {
+      this.pageStyle.getComputed(this._viewedElement, {
         filter: this._sourceFilter,
         onlyMatched: !this.includeBrowserStyles,
         markMatched: true
       })
     ]).then(([, computed]) => {
-      if (viewedElement !== this.viewedElement) {
+      if (viewedElement !== this._viewedElement) {
         return promise.resolve();
       }
 
       this._matchedProperties = new Set();
       for (let name in computed) {
         if (computed[name].matched) {
           this._matchedProperties.add(name);
         }
@@ -733,17 +733,17 @@ CssComputedView.prototype = {
       console.error(e);
     }
   },
 
   /**
    * Destructor for CssComputedView.
    */
   destroy: function () {
-    this.viewedElement = null;
+    this._viewedElement = null;
     this._outputParser = null;
 
     gDevTools.off("pref-changed", this._handlePrefChange);
 
     this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
     this._prefObserver.destroy();
 
     // Cancel tree construction
@@ -852,17 +852,17 @@ PropertyView.prototype = {
 
   // Matched selector expando
   matchedExpander: null,
 
   // Cache for matched selector views
   _matchedSelectorViews: null,
 
   // The previously selected element used for the selector view caches
-  prevViewedElement: null,
+  _prevViewedElement: null,
 
   /**
    * Get the computed style for the current property.
    *
    * @return {String} the computed style for the current property of the
    * currently highlighted element.
    */
   get value() {
@@ -882,17 +882,17 @@ PropertyView.prototype = {
   get hasMatchedSelectors() {
     return this.tree.matchedProperties.has(this.name);
   },
 
   /**
    * Should this property be visible?
    */
   get visible() {
-    if (!this.tree.viewedElement) {
+    if (!this.tree._viewedElement) {
       return false;
     }
 
     if (!this.tree.includeBrowserStyles && !this.hasMatchedSelectors) {
       return false;
     }
 
     let searchTerm = this.tree.searchField.value.toLowerCase();
@@ -1012,22 +1012,22 @@ PropertyView.prototype = {
 
   /**
    * Refresh the panel's CSS property value.
    */
   refresh: function () {
     this.element.className = this.propertyHeaderClassName;
     this.element.nextElementSibling.className = this.propertyContentClassName;
 
-    if (this.prevViewedElement !== this.tree.viewedElement) {
+    if (this._prevViewedElement !== this.tree._viewedElement) {
       this._matchedSelectorViews = null;
-      this.prevViewedElement = this.tree.viewedElement;
+      this._prevViewedElement = this.tree._viewedElement;
     }
 
-    if (!this.tree.viewedElement || !this.visible) {
+    if (!this.tree._viewedElement || !this.visible) {
       this.valueNode.textContent = this.valueNode.title = "";
       this.matchedSelectorsContainer.parentNode.hidden = true;
       this.matchedSelectorsContainer.textContent = "";
       this.matchedExpander.removeAttribute("open");
       return;
     }
 
     this.tree.numVisibleProperties++;
@@ -1057,17 +1057,17 @@ PropertyView.prototype = {
     if (hasMatchedSelectors) {
       this.matchedExpander.classList.add("expandable");
     } else {
       this.matchedExpander.classList.remove("expandable");
     }
 
     if (this.matchedExpanded && hasMatchedSelectors) {
       return this.tree.pageStyle
-        .getMatchedSelectors(this.tree.viewedElement, this.name)
+        .getMatchedSelectors(this.tree._viewedElement, this.name)
         .then(matched => {
           if (!this.matchedExpanded) {
             return promise.resolve(undefined);
           }
 
           this._matchedSelectorResponse = matched;
 
           return this._buildMatchedSelectors().then(() => {
@@ -1469,17 +1469,17 @@ ComputedViewTool.prototype = {
 
   refresh: function () {
     if (this.isSidebarActive()) {
       this.view.refreshPanel();
     }
   },
 
   onPanelSelected: function () {
-    if (this.inspector.selection.nodeFront === this.view.viewedElement) {
+    if (this.inspector.selection.nodeFront === this.view._viewedElement) {
       this.refresh();
     } else {
       this.onSelected();
     }
   },
 
   /**
    * When markup mutations occur, if an attribute of the selected node changes,
--- a/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js
+++ b/devtools/client/inspector/computed/test/browser_computed_matched-selectors_01.js
@@ -17,17 +17,17 @@ add_task(function* () {
   yield selectNode("#test", inspector);
   yield testMatchedSelectors(view, inspector);
 });
 
 function* testMatchedSelectors(view, inspector) {
   info("checking selector counts, matched rules and titles");
 
   let nodeFront = yield getNodeFront("#test", inspector);
-  is(nodeFront, view.viewedElement,
+  is(nodeFront, view._viewedElement,
     "style inspector node matches the selected node");
 
   let propertyView = new PropertyView(view, "color");
   propertyView.buildMain();
   propertyView.buildSelectorContainer();
   propertyView.matchedExpanded = true;
 
   yield propertyView.refreshMatchedSelectors();
--- a/devtools/client/inspector/layout/test/browser.ini
+++ b/devtools/client/inspector/layout/test/browser.ini
@@ -15,13 +15,14 @@ support-files =
 [browser_layout_editablemodel.js]
 # [browser_layout_editablemodel_allproperties.js]
 # Disabled for too many intermittent failures (bug 1009322)
 [browser_layout_editablemodel_bluronclick.js]
 [browser_layout_editablemodel_border.js]
 [browser_layout_editablemodel_stylerules.js]
 [browser_layout_guides.js]
 [browser_layout_rotate-labels-on-sides.js]
+[browser_layout_sync.js]
 [browser_layout_tooltips.js]
 [browser_layout_update-after-navigation.js]
 [browser_layout_update-after-reload.js]
 # [browser_layout_update-in-iframes.js]
 # Bug 1020038 layout-view updates for iframe elements changes
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/layout/test/browser_layout_sync.js
@@ -0,0 +1,44 @@
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test editing box model syncs with the rule view.
+
+const TEST_URI = "<p>hello</p>";
+
+add_task(function* () {
+  yield addTab("data:text/html," + encodeURIComponent(TEST_URI));
+  let {inspector, view} = yield openLayoutView();
+
+  info("When a property is edited, it should sync in the rule view");
+
+  yield selectNode("p", inspector);
+
+  info("Modify padding-bottom in layout view");
+  let span = view.doc.querySelector(".layout-padding.layout-bottom > span");
+  EventUtils.synthesizeMouseAtCenter(span, {}, view.doc.defaultView);
+  let editor = view.doc.querySelector(".styleinspector-propertyeditor");
+
+  EventUtils.synthesizeKey("7", {}, view.doc.defaultView);
+  yield waitForUpdate(inspector);
+  is(editor.value, "7", "Should have the right value in the editor.");
+  EventUtils.synthesizeKey("VK_RETURN", {}, view.doc.defaultView);
+
+  let onRuleViewRefreshed = once(inspector, "rule-view-refreshed");
+  let onRuleViewSelected = once(inspector.sidebar, "ruleview-selected");
+  info("Select the rule view and check that the property was synced there");
+  let ruleView = selectRuleView(inspector);
+
+  info("Wait for the rule view to be selected");
+  yield onRuleViewSelected;
+
+  info("Wait for the rule view to be refreshed");
+  yield onRuleViewRefreshed;
+  ok(true, "The rule view was refreshed");
+
+  let ruleEditor = getRuleViewRuleEditor(ruleView, 0);
+  let textProp = ruleEditor.rule.textProps[0];
+  is(textProp.value, "7px", "The property has the right value");
+});
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -1647,17 +1647,17 @@ RuleViewTool.prototype = {
 
   clearUserProperties: function () {
     if (this.view && this.view.store && this.view.store.userProperties) {
       this.view.store.userProperties.clear();
     }
   },
 
   onPanelSelected: function () {
-    if (this.inspector.selection.nodeFront === this.view.viewedElement) {
+    if (this.inspector.selection.nodeFront === this.view._viewedElement) {
       this.refresh();
     } else {
       this.onSelected();
     }
   },
 
   onLinkClicked: function (e, rule) {
     let sheet = rule.parentStyleSheet;
--- a/devtools/client/inspector/rules/test/head.js
+++ b/devtools/client/inspector/rules/test/head.js
@@ -485,33 +485,16 @@ function getRuleViewLinkByIndex(view, in
  * @return {String} The string at this index
  */
 function getRuleViewLinkTextByIndex(view, index) {
   let link = getRuleViewLinkByIndex(view, index);
   return link.querySelector(".ruleview-rule-source-label").value;
 }
 
 /**
- * Get the rule editor from the rule-view given its index
- *
- * @param {CssRuleView} view
- *        The instance of the rule-view panel
- * @param {Number} childrenIndex
- *        The children index of the element to get
- * @param {Number} nodeIndex
- *        The child node index of the element to get
- * @return {DOMNode} The rule editor if any at this index
- */
-function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) {
-  return nodeIndex !== undefined ?
-    view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor :
-    view.element.children[childrenIndex]._ruleEditor;
-}
-
-/**
  * Simulate adding a new property in an existing rule in the rule-view.
  *
  * @param {CssRuleView} view
  *        The instance of the rule-view panel
  * @param {Number} ruleIndex
  *        The index of the rule to use. Note that if ruleIndex is 0, you might
  *        want to also listen to markupmutation events in your test since
  *        that's going to change the style attribute of the selected node.
--- a/devtools/client/inspector/shared/test/head.js
+++ b/devtools/client/inspector/shared/test/head.js
@@ -420,33 +420,16 @@ function getRuleViewLinkByIndex(view, in
  * @return {String} The string at this index
  */
 function getRuleViewLinkTextByIndex(view, index) {
   let link = getRuleViewLinkByIndex(view, index);
   return link.querySelector(".ruleview-rule-source-label").value;
 }
 
 /**
- * Get the rule editor from the rule-view given its index
- *
- * @param {CssRuleView} view
- *        The instance of the rule-view panel
- * @param {Number} childrenIndex
- *        The children index of the element to get
- * @param {Number} nodeIndex
- *        The child node index of the element to get
- * @return {DOMNode} The rule editor if any at this index
- */
-function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) {
-  return nodeIndex !== undefined ?
-    view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor :
-    view.element.children[childrenIndex]._ruleEditor;
-}
-
-/**
  * Click on a rule-view's close brace to focus a new property name editor
  *
  * @param {RuleEditor} ruleEditor
  *        An instance of RuleEditor that will receive the new property
  * @return a promise that resolves to the newly created editor when ready and
  * focused
  */
 var focusNewRuleViewProperty = Task.async(function* (ruleEditor) {
--- a/devtools/client/inspector/test/head.js
+++ b/devtools/client/inspector/test/head.js
@@ -821,8 +821,25 @@ function openContextMenuAndGetAllItems(i
     if (item.submenu) {
       return addItem(item.submenu.items);
     }
     return item;
   }));
 
   return allItems;
 }
+
+/**
+ * Get the rule editor from the rule-view given its index
+ *
+ * @param {CssRuleView} view
+ *        The instance of the rule-view panel
+ * @param {Number} childrenIndex
+ *        The children index of the element to get
+ * @param {Number} nodeIndex
+ *        The child node index of the element to get
+ * @return {DOMNode} The rule editor if any at this index
+ */
+function getRuleViewRuleEditor(view, childrenIndex, nodeIndex) {
+  return nodeIndex !== undefined ?
+    view.element.children[childrenIndex].childNodes[nodeIndex]._ruleEditor :
+    view.element.children[childrenIndex]._ruleEditor;
+}
--- a/dom/interfaces/notification/nsINotificationStorage.idl
+++ b/dom/interfaces/notification/nsINotificationStorage.idl
@@ -23,17 +23,17 @@ interface nsINotificationStorageCallback
               in DOMString title,
               in DOMString dir,
               in DOMString lang,
               in DOMString body,
               in DOMString tag,
               in DOMString icon,
               in DOMString data,
               in DOMString behavior,
-              in DOMString serviceWorkerRegistrationID);
+              in DOMString serviceWorkerRegistrationScope);
 
   /**
    * Callback function used to notify C++ the we have returned
    * all notification objects for this Notification.get call.
    */
   void done();
 };
 
@@ -71,17 +71,17 @@ interface nsINotificationStorage : nsISu
            in DOMString dir,
            in DOMString lang,
            in DOMString body,
            in DOMString tag,
            in DOMString icon,
            in DOMString alertName,
            in DOMString data,
            in DOMString behavior,
-           in DOMString serviceWorkerRegistrationID);
+           in DOMString serviceWorkerRegistrationScope);
 
   /**
    * Retrieve a list of notifications.
    *
    * @param origin: the origin/app for which to fetch notifications from
    * @param tag: used to fetch only a specific tag
    * @param callback: nsINotificationStorageCallback, used for
    *                  returning notifications objects
--- a/dom/notification/Notification.cpp
+++ b/dom/notification/Notification.cpp
@@ -1,16 +1,17 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "mozilla/dom/Notification.h"
 
+#include "mozilla/JSONWriter.h"
 #include "mozilla/Move.h"
 #include "mozilla/OwningNonNull.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/Services.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/unused.h"
 
 #include "mozilla/dom/AppNotificationServiceOptionsBinding.h"
@@ -73,17 +74,17 @@ struct NotificationStrings
   const nsString mTitle;
   const nsString mDir;
   const nsString mLang;
   const nsString mBody;
   const nsString mTag;
   const nsString mIcon;
   const nsString mData;
   const nsString mBehavior;
-  const nsString mServiceWorkerRegistrationID;
+  const nsString mServiceWorkerRegistrationScope;
 };
 
 class ScopeCheckingGetCallback : public nsINotificationStorageCallback
 {
   const nsString mScope;
 public:
   explicit ScopeCheckingGetCallback(const nsAString& aScope)
     : mScope(aScope)
@@ -93,37 +94,37 @@ public:
                     const nsAString& aTitle,
                     const nsAString& aDir,
                     const nsAString& aLang,
                     const nsAString& aBody,
                     const nsAString& aTag,
                     const nsAString& aIcon,
                     const nsAString& aData,
                     const nsAString& aBehavior,
-                    const nsAString& aServiceWorkerRegistrationID) final
+                    const nsAString& aServiceWorkerRegistrationScope) final
   {
     AssertIsOnMainThread();
     MOZ_ASSERT(!aID.IsEmpty());
 
     // Skip scopes that don't match when called from getNotifications().
-    if (!mScope.IsEmpty() && !mScope.Equals(aServiceWorkerRegistrationID)) {
+    if (!mScope.IsEmpty() && !mScope.Equals(aServiceWorkerRegistrationScope)) {
       return NS_OK;
     }
 
     NotificationStrings strings = {
       nsString(aID),
       nsString(aTitle),
       nsString(aDir),
       nsString(aLang),
       nsString(aBody),
       nsString(aTag),
       nsString(aIcon),
       nsString(aData),
       nsString(aBehavior),
-      nsString(aServiceWorkerRegistrationID),
+      nsString(aServiceWorkerRegistrationScope),
     };
 
     mStrings.AppendElement(Move(strings));
     return NS_OK;
   }
 
   NS_IMETHOD Done() override = 0;
 
@@ -164,17 +165,17 @@ public:
                                           mStrings[i].mDir,
                                           mStrings[i].mLang,
                                           mStrings[i].mBody,
                                           mStrings[i].mTag,
                                           mStrings[i].mIcon,
                                           mStrings[i].mData,
                                           /* mStrings[i].mBehavior, not
                                            * supported */
-                                          mStrings[i].mServiceWorkerRegistrationID,
+                                          mStrings[i].mServiceWorkerRegistrationScope,
                                           result);
 
       n->SetStoredState(true);
       Unused << NS_WARN_IF(result.Failed());
       if (!result.Failed()) {
         notifications.AppendElement(n.forget());
       }
     }
@@ -1061,17 +1062,17 @@ Notification::ConstructFromFields(
     const nsAString& aID,
     const nsAString& aTitle,
     const nsAString& aDir,
     const nsAString& aLang,
     const nsAString& aBody,
     const nsAString& aTag,
     const nsAString& aIcon,
     const nsAString& aData,
-    const nsAString& aServiceWorkerRegistrationID,
+    const nsAString& aServiceWorkerRegistrationScope,
     ErrorResult& aRv)
 {
   MOZ_ASSERT(aGlobal);
 
   RootedDictionary<NotificationOptions> options(nsContentUtils::RootingCxForThread());
   options.mDir = Notification::StringToDirection(nsString(aDir));
   options.mLang = aLang;
   options.mBody = aBody;
@@ -1080,17 +1081,17 @@ Notification::ConstructFromFields(
   RefPtr<Notification> notification = CreateInternal(aGlobal, aID, aTitle,
                                                      options);
 
   notification->InitFromBase64(aData, aRv);
   if (NS_WARN_IF(aRv.Failed())) {
     return nullptr;
   }
 
-  notification->SetScope(aServiceWorkerRegistrationID);
+  notification->SetScope(aServiceWorkerRegistrationScope);
 
   return notification.forget();
 }
 
 nsresult
 Notification::PersistNotification()
 {
   AssertIsOnMainThread();
@@ -1660,16 +1661,29 @@ Notification::IsInPrivateBrowsing()
                                   getter_AddRefs(loadContext));
     return loadContext && loadContext->UsePrivateBrowsing();
   }
 
   //XXXnsm Should this default to true?
   return false;
 }
 
+namespace {
+  struct StringWriteFunc : public JSONWriteFunc
+  {
+    nsAString& mBuffer; // This struct must not outlive this buffer
+    explicit StringWriteFunc(nsAString& buffer) : mBuffer(buffer) {}
+
+    void Write(const char* aStr)
+    {
+      mBuffer.Append(NS_ConvertUTF8toUTF16(aStr));
+    }
+  };
+}
+
 void
 Notification::ShowInternal()
 {
   AssertIsOnMainThread();
   MOZ_ASSERT(mTempRef, "Notification should take ownership of itself before"
                        "calling ShowInternal!");
   // A notification can only have one observer and one call to ShowInternal.
   MOZ_ASSERT(!mObserver);
@@ -1711,30 +1725,32 @@ Notification::ShowInternal()
     }
     return;
   }
 
   nsAutoString iconUrl;
   nsAutoString soundUrl;
   ResolveIconAndSoundURL(iconUrl, soundUrl);
 
+  bool isPersistent = false;
   nsCOMPtr<nsIObserver> observer;
   if (mScope.IsEmpty()) {
     // Ownership passed to observer.
     if (mWorkerPrivate) {
       // Scope better be set on ServiceWorker initiated requests.
       MOZ_ASSERT(!mWorkerPrivate->IsServiceWorker());
       // Keep a pointer so that the feature can tell the observer not to release
       // the notification.
       mObserver = new WorkerNotificationObserver(Move(ownership));
       observer = mObserver;
     } else {
       observer = new MainThreadNotificationObserver(Move(ownership));
     }
   } else {
+    isPersistent = true;
     // This observer does not care about the Notification. It will be released
     // at the end of this function.
     //
     // The observer is wholly owned by the NotificationObserver passed to the alert service.
     nsAutoString behavior;
     if (NS_WARN_IF(!mBehavior.ToJSON(behavior))) {
       behavior.Truncate();
     }
@@ -1808,26 +1824,49 @@ Notification::ShowInternal()
   uniqueCookie.AppendInt(sCount++);
   bool inPrivateBrowsing = IsInPrivateBrowsing();
 
   nsAutoString alertName;
   GetAlertName(alertName);
   nsCOMPtr<nsIAlertNotification> alert =
     do_CreateInstance(ALERT_NOTIFICATION_CONTRACTID);
   NS_ENSURE_TRUE_VOID(alert);
+  nsIPrincipal* principal = GetPrincipal();
   rv = alert->Init(alertName, iconUrl, mTitle, mBody,
                    true,
                    uniqueCookie,
                    DirectionToString(mDir),
                    mLang,
                    mDataAsBase64,
                    GetPrincipal(),
                    inPrivateBrowsing);
   NS_ENSURE_SUCCESS_VOID(rv);
-  alertService->ShowAlert(alert, alertObserver);
+
+  if (isPersistent) {
+    nsAutoString persistentData;
+
+    JSONWriter w(MakeUnique<StringWriteFunc>(persistentData));
+    w.Start();
+
+    nsAutoString origin;
+    Notification::GetOrigin(principal, origin);
+    w.StringProperty("origin", NS_ConvertUTF16toUTF8(origin).get());
+
+    w.StringProperty("id", NS_ConvertUTF16toUTF8(mID).get());
+
+    nsAutoCString originSuffix;
+    principal->GetOriginSuffix(originSuffix);
+    w.StringProperty("originSuffix", originSuffix.get());
+
+    w.End();
+
+    alertService->ShowPersistentNotification(persistentData, alert, alertObserver);
+  } else {
+    alertService->ShowAlert(alert, alertObserver);
+  }
 }
 
 /* static */ bool
 Notification::RequestPermissionEnabledForScope(JSContext* aCx, JSObject* /* unused */)
 {
   // requestPermission() is not allowed on workers. The calling page should ask
   // for permission on the worker's behalf. This is to prevent 'which window
   // should show the browser pop-up'. See discussion:
@@ -2107,17 +2146,17 @@ public:
                                           mStrings[i].mDir,
                                           mStrings[i].mLang,
                                           mStrings[i].mBody,
                                           mStrings[i].mTag,
                                           mStrings[i].mIcon,
                                           mStrings[i].mData,
                                           /* mStrings[i].mBehavior, not
                                            * supported */
-                                          mStrings[i].mServiceWorkerRegistrationID,
+                                          mStrings[i].mServiceWorkerRegistrationScope,
                                           result);
 
       n->SetStoredState(true);
       Unused << NS_WARN_IF(result.Failed());
       if (!result.Failed()) {
         notifications.AppendElement(n.forget());
       }
     }
--- a/dom/notification/Notification.h
+++ b/dom/notification/Notification.h
@@ -180,17 +180,17 @@ public:
     const nsAString& aID,
     const nsAString& aTitle,
     const nsAString& aDir,
     const nsAString& aLang,
     const nsAString& aBody,
     const nsAString& aTag,
     const nsAString& aIcon,
     const nsAString& aData,
-    const nsAString& aServiceWorkerRegistrationID,
+    const nsAString& aServiceWorkerRegistrationScope,
     ErrorResult& aRv);
 
   void GetID(nsAString& aRetval) {
     aRetval = mID;
   }
 
   void GetTitle(nsAString& aRetval)
   {
--- a/dom/notification/NotificationDB.jsm
+++ b/dom/notification/NotificationDB.jsm
@@ -77,36 +77,45 @@ var NotificationDB = {
     if (aTopic == "xpcom-shutdown") {
       this._shutdownInProgress = true;
       Services.obs.removeObserver(this, "xpcom-shutdown");
       this.unregisterListeners();
     }
   },
 
   filterNonAppNotifications: function(notifications) {
-    let origins = Object.keys(notifications);
-    for (let origin of origins) {
+    for (let origin in notifications) {
       let canPut = notificationStorage.canPut(origin);
       if (!canPut) {
-        if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting.");
-	delete notifications[origin];
+        let persistentNotificationCount = 0;
+        for (let id in notifications[origin]) {
+          if (notifications[origin][id].serviceWorkerRegistrationScope) {
+            persistentNotificationCount++;
+          } else {
+            delete notifications[origin][id];
+          }
+        }
+        if (persistentNotificationCount == 0) {
+          if (DEBUG) debug("Origin " + origin + " is not linked to an app manifest, deleting.");
+          delete notifications[origin];
+        }
       }
     }
     return notifications;
   },
 
   // Attempt to read notification file, if it's not there we will create it.
   load: function() {
     var promise = OS.File.read(NOTIFICATION_STORE_PATH, { encoding: "utf-8"});
     return promise.then(
       function onSuccess(data) {
         if (data.length > 0) {
-	  // Preprocessing phase intends to cleanly separate any migration-related
+          // Preprocessing phase intends to cleanly separate any migration-related
           // tasks.
-	  this.notifications = this.filterNonAppNotifications(JSON.parse(data));
+          this.notifications = this.filterNonAppNotifications(JSON.parse(data));
         }
 
         // populate the list of notifications by tag
         if (this.notifications) {
           for (var origin in this.notifications) {
             this.byTag[origin] = {};
             for (var id in this.notifications[origin]) {
               var curNotification = this.notifications[origin][id];
--- a/dom/notification/NotificationStorage.js
+++ b/dom/notification/NotificationStorage.js
@@ -79,32 +79,32 @@ NotificationStorage.prototype = {
   canPut: function(aOrigin) {
     if (DEBUG) debug("Querying appService for: " + aOrigin);
     let rv = !!appsService.getAppByManifestURL(aOrigin);
     if (DEBUG) debug("appService returned: " + rv);
     return rv;
   },
 
   put: function(origin, id, title, dir, lang, body, tag, icon, alertName,
-                data, behavior, serviceWorkerRegistrationID) {
+                data, behavior, serviceWorkerRegistrationScope) {
     if (DEBUG) { debug("PUT: " + origin + " " + id + ": " + title); }
     var notification = {
       id: id,
       title: title,
       dir: dir,
       lang: lang,
       body: body,
       tag: tag,
       icon: icon,
       alertName: alertName,
       timestamp: new Date().getTime(),
       origin: origin,
       data: data,
       mozbehavior: behavior,
-      serviceWorkerRegistrationID: serviceWorkerRegistrationID,
+      serviceWorkerRegistrationScope: serviceWorkerRegistrationScope,
     };
 
     this._notifications[id] = notification;
     if (tag) {
       if (!this._byTag[origin]) {
         this._byTag[origin] = {};
       }
 
@@ -113,17 +113,17 @@ NotificationStorage.prototype = {
       if (this._byTag[origin][tag]) {
         var oldNotification = this._byTag[origin][tag];
         delete this._notifications[oldNotification.id];
       }
 
       this._byTag[origin][tag] = notification;
     };
 
-    if (this.canPut(origin)) {
+    if (serviceWorkerRegistrationScope || this.canPut(origin)) {
       cpmm.sendAsyncMessage("Notification:Save", {
         origin: origin,
         notification: notification
       });
     }
   },
 
   get: function(origin, tag, callback) {
@@ -136,19 +136,19 @@ NotificationStorage.prototype = {
   },
 
   getByID: function(origin, id, callback) {
     if (DEBUG) { debug("GETBYID: " + origin + " " + id); }
     var GetByIDProxyCallback = function(id, originalCallback) {
       this.searchID = id;
       this.originalCallback = originalCallback;
       var self = this;
-      this.handle = function(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationID) {
+      this.handle = function(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationScope) {
         if (id == this.searchID) {
-          self.originalCallback.handle(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationID);
+          self.originalCallback.handle(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationScope);
         }
       };
       this.done = function() {
         self.originalCallback.done();
       };
     };
 
     return this.get(origin, "", new GetByIDProxyCallback(id, callback));
@@ -241,17 +241,17 @@ NotificationStorage.prototype = {
                                notification.title,
                                notification.dir,
                                notification.lang,
                                notification.body,
                                notification.tag,
                                notification.icon,
                                notification.data,
                                notification.mozbehavior,
-                               notification.serviceWorkerRegistrationID),
+                               notification.serviceWorkerRegistrationScope),
           Ci.nsIThread.DISPATCH_NORMAL);
       } catch (e) {
         if (DEBUG) { debug("Error calling callback handle: " + e); }
       }
     });
     try {
       Services.tm.currentThread.dispatch(callback.done,
                                          Ci.nsIThread.DISPATCH_NORMAL);
--- a/dom/tests/mochitest/notification/MockServices.js
+++ b/dom/tests/mochitest/notification/MockServices.js
@@ -24,16 +24,20 @@ var MockServices = (function () {
         delete activeAlertNotifications[alertName];
         delete activeAppNotifications[alertName];
         return;
       }
     }
   });
 
   var mockAlertsService = {
+    showPersistentNotification: function(persistentData, alert, alertListener) {
+      this.showAlert(alert, alertListener);
+    },
+
     showAlert: function(alert, alertListener) {
       var listener = SpecialPowers.wrap(alertListener);
       activeAlertNotifications[alert.name] = {
         listener: listener,
         cookie: alert.cookie,
         title: alert.title
       };
 
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoAppShell.java
@@ -891,16 +891,29 @@ public class GeckoAppShell
     public static void setNotificationClient(NotificationClient client) {
         if (notificationClient == null) {
             notificationClient = client;
         } else {
             Log.d(LOGTAG, "Notification client already set");
         }
     }
 
+    @WrapForJNI(stubName = "ShowPersistentAlertNotificationWrapper")
+    public static void showPersistentAlertNotification(
+          String aPersistentData,
+          String aImageUrl, String aAlertTitle, String aAlertText,
+          String aAlertCookie, String aAlertName, String aHost) {
+        Intent notificationIntent = GeckoService.getIntentToCreateServices(
+                getApplicationContext(), "persistent-notification-click", aPersistentData);
+        int notificationID = aAlertName.hashCode();
+        PendingIntent contentIntent = PendingIntent.getService(
+                getApplicationContext(), 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+        notificationClient.add(notificationID, aImageUrl, aHost, aAlertTitle, aAlertText, contentIntent);
+    }
+
     @WrapForJNI(stubName = "ShowAlertNotificationWrapper")
     public static void showAlertNotification(String aImageUrl, String aAlertTitle, String aAlertText, String aAlertCookie, String aAlertName, String aHost) {
         // The intent to launch when the user clicks the expanded notification
         Intent notificationIntent = new Intent(GeckoApp.ACTION_ALERT_CALLBACK);
         notificationIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.MOZ_ANDROID_BROWSER_INTENT_CLASS);
         notificationIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 
         int notificationID = aAlertName.hashCode();
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoService.java
@@ -124,19 +124,17 @@ public class GeckoService extends Servic
     public static Intent getIntentToCreateServices(final Context context, final String category, final String data) {
         final Intent intent = getIntentForAction(context, INTENT_ACTION_CREATE_SERVICES);
         intent.putExtra(INTENT_SERVICE_CATEGORY, category);
         intent.putExtra(INTENT_SERVICE_DATA, data);
         return intent;
     }
 
     public static Intent getIntentToCreateServices(final Context context, final String category) {
-        final Intent intent = getIntentForAction(context, INTENT_ACTION_CREATE_SERVICES);
-        intent.putExtra(INTENT_SERVICE_CATEGORY, category);
-        return intent;
+        return getIntentToCreateServices(context, category, /* data */ null);
     }
 
     public static void setIntentProfile(final Intent intent, final String profileName,
                                         final String profileDir) {
         intent.putExtra(INTENT_PROFILE_NAME, profileName);
         intent.putExtra(INTENT_PROFILE_DIR, profileDir);
     }
 
--- a/mobile/android/components/MobileComponents.manifest
+++ b/mobile/android/components/MobileComponents.manifest
@@ -111,8 +111,13 @@ category update-timer Snippets @mozilla.
 
 # ColorPicker.js
 component {430b987f-bb9f-46a3-99a5-241749220b29} ColorPicker.js
 contract @mozilla.org/colorpicker;1 {430b987f-bb9f-46a3-99a5-241749220b29}
 
 # AndroidActivitiesGlue.js
 component {e4deb5f6-d5e3-4fce-bc53-901dd9951c48} AndroidActivitiesGlue.js
 contract @mozilla.org/dom/activities/ui-glue;1 {e4deb5f6-d5e3-4fce-bc53-901dd9951c48}
+
+# PersistentNotificationHandler.js
+component {75390fe7-f8a3-423a-b3b1-258d7eabed40} PersistentNotificationHandler.js
+contract @mozilla.org/persistent-notification-handler;1 {75390fe7-f8a3-423a-b3b1-258d7eabed40}
+category persistent-notification-click PersistentNotificationHandler @mozilla.org/persistent-notification-handler;1
new file mode 100644
--- /dev/null
+++ b/mobile/android/components/PersistentNotificationHandler.js
@@ -0,0 +1,57 @@
+/* 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/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Messaging.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, 'Services', // jshint ignore:line
+                                  'resource://gre/modules/Services.jsm');
+XPCOMUtils.defineLazyServiceGetter(this, "notificationStorage",
+                                   "@mozilla.org/notificationStorage;1",
+                                   "nsINotificationStorage");
+XPCOMUtils.defineLazyServiceGetter(this, "serviceWorkerManager",
+                                   "@mozilla.org/serviceworkers/manager;1",
+                                   "nsIServiceWorkerManager");
+
+function PersistentNotificationHandler() {
+}
+
+PersistentNotificationHandler.prototype = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+  classID: Components.ID("{75390fe7-f8a3-423a-b3b1-258d7eabed40}"),
+
+  observe(subject, topic, data) {
+    if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
+      Cu.import("resource://gre/modules/NotificationDB.jsm");
+    }
+    const persistentInfo = JSON.parse(data);
+
+    notificationStorage.getByID(persistentInfo.origin, persistentInfo.id, {
+      handle(id, title, dir, lang, body, tag, icon, data, behavior, serviceWorkerRegistrationScope) {
+        serviceWorkerManager.sendNotificationClickEvent(
+          persistentInfo.originSuffix,
+          serviceWorkerRegistrationScope,
+          id,
+          title,
+          dir,
+          lang,
+          body,
+          tag,
+          icon,
+          data,
+          behavior
+        );
+        notificationStorage.delete(persistentInfo.origin, persistentInfo.id);
+      }
+    });
+  }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
+  PersistentNotificationHandler
+]);
--- a/mobile/android/components/moz.build
+++ b/mobile/android/components/moz.build
@@ -20,16 +20,17 @@ EXTRA_COMPONENTS += [
     'ContentDispatchChooser.js',
     'ContentPermissionPrompt.js',
     'DirectoryProvider.js',
     'FilePicker.js',
     'HelperAppDialog.js',
     'ImageBlockingPolicy.js',
     'LoginManagerPrompter.js',
     'NSSDialogService.js',
+    'PersistentNotificationHandler.js',
     'PresentationDevicePrompt.js',
     'PromptService.js',
     'SessionStore.js',
     'SiteSpecificUserAgent.js',
     'Snippets.js',
     'TabSource.js',
     'XPIDialogService.js',
 ]
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -536,16 +536,17 @@
 @BINPATH@/components/ImageBlockingPolicy.js
 @BINPATH@/components/DirectoryProvider.js
 @BINPATH@/components/FilePicker.js
 @BINPATH@/components/HelperAppDialog.js
 @BINPATH@/components/LoginManagerPrompter.js
 @BINPATH@/components/MobileComponents.manifest
 @BINPATH@/components/MobileComponents.xpt
 @BINPATH@/components/NSSDialogService.js
+@BINPATH@/components/PersistentNotificationHandler.js
 @BINPATH@/components/PresentationDevicePrompt.js
 @BINPATH@/components/PromptService.js
 @BINPATH@/components/SessionStore.js
 @BINPATH@/components/SiteSpecificUserAgent.js
 @BINPATH@/components/Snippets.js
 
 @BINPATH@/components/XPIDialogService.js
 
--- a/toolkit/components/alerts/nsAlertsService.cpp
+++ b/toolkit/components/alerts/nsAlertsService.cpp
@@ -203,16 +203,23 @@ NS_IMETHODIMP nsAlertsService::ShowAlert
   NS_ENSURE_SUCCESS(rv, rv);
   return ShowAlert(alert, aAlertListener);
 }
 
 
 NS_IMETHODIMP nsAlertsService::ShowAlert(nsIAlertNotification * aAlert,
                                          nsIObserver * aAlertListener)
 {
+  return ShowPersistentNotification(EmptyString(), aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP nsAlertsService::ShowPersistentNotification(const nsAString & aPersistentData,
+                                                          nsIAlertNotification * aAlert,
+                                                          nsIObserver * aAlertListener)
+{
   NS_ENSURE_ARG(aAlert);
 
   nsAutoString cookie;
   nsresult rv = aAlert->GetCookie(cookie);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (XRE_IsContentProcess()) {
     ContentChild* cpc = ContentChild::GetSingleton();
@@ -240,18 +247,24 @@ NS_IMETHODIMP nsAlertsService::ShowAlert
   nsAutoString name;
   rv = aAlert->GetName(name);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsCOMPtr<nsIPrincipal> principal;
   rv = aAlert->GetPrincipal(getter_AddRefs(principal));
   NS_ENSURE_SUCCESS(rv, rv);
 
-  mozilla::AndroidBridge::Bridge()->ShowAlertNotification(imageUrl, title, text, cookie,
-                                                          aAlertListener, name, principal);
+  if (!aPersistentData.IsEmpty()) {
+    mozilla::AndroidBridge::Bridge()->ShowPersistentAlertNotification
+        (aPersistentData, imageUrl, title, text, cookie, name, principal);
+  } else {
+    mozilla::AndroidBridge::Bridge()->ShowAlertNotification
+        (imageUrl, title, text, cookie, aAlertListener, name, principal);
+  }
+
   return NS_OK;
 #else
   // Check if there is an optional service that handles system-level notifications
   if (mBackend) {
     rv = ShowWithBackend(mBackend, aAlert, aAlertListener);
     if (NS_SUCCEEDED(rv)) {
       return rv;
     }
--- a/toolkit/components/alerts/nsIAlertsService.idl
+++ b/toolkit/components/alerts/nsIAlertsService.idl
@@ -110,16 +110,20 @@ interface nsIAlertNotification : nsISupp
    * is not actionable.
    */
   readonly attribute AString source;
 };
 
 [scriptable, uuid(f7a36392-d98b-4141-a7d7-4e46642684e3)]
 interface nsIAlertsService : nsISupports
 {
+  void showPersistentNotification(in AString persistentData,
+                                  in nsIAlertNotification alert,
+                                  [optional] in nsIObserver alertListener);
+
   void showAlert(in nsIAlertNotification alert,
                  [optional] in nsIObserver alertListener);
   /**
    * Initializes and shows an |nsIAlertNotification| with the given parameters.
    *
    * @param alertListener  Used for callbacks. May be null if the caller
    *                       doesn't care about callbacks.
    * @see nsIAlertNotification for descriptions of all other parameters.
--- a/toolkit/components/alerts/nsXULAlerts.cpp
+++ b/toolkit/components/alerts/nsXULAlerts.cpp
@@ -86,16 +86,24 @@ nsXULAlerts::ShowAlertNotification(const
                             aAlertText, aAlertTextClickable,
                             aAlertCookie, aBidi, aLang, aData,
                             aPrincipal, aInPrivateBrowsing);
   NS_ENSURE_SUCCESS(rv, rv);
   return ShowAlert(alert, aAlertListener);
 }
 
 NS_IMETHODIMP
+nsXULAlerts::ShowPersistentNotification(const nsAString& aPersistentData,
+                                        nsIAlertNotification* aAlert,
+                                        nsIObserver* aAlertListener)
+{
+  return ShowAlert(aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP
 nsXULAlerts::ShowAlert(nsIAlertNotification* aAlert,
                        nsIObserver* aAlertListener)
 {
   return ShowAlertWithIconURI(aAlert, aAlertListener, nullptr);
 }
 
 NS_IMETHODIMP
 nsXULAlerts::ShowAlertWithIconURI(nsIAlertNotification* aAlert,
--- a/toolkit/system/gnome/nsSystemAlertsService.cpp
+++ b/toolkit/system/gnome/nsSystemAlertsService.cpp
@@ -47,16 +47,23 @@ NS_IMETHODIMP nsSystemAlertsService::Sho
   nsresult rv = alert->Init(aAlertName, aImageUrl, aAlertTitle,
                             aAlertText, aAlertTextClickable,
                             aAlertCookie, aBidi, aLang, aData,
                             aPrincipal, aInPrivateBrowsing);
   NS_ENSURE_SUCCESS(rv, rv);
   return ShowAlert(alert, aAlertListener);
 }
 
+NS_IMETHODIMP nsSystemAlertsService::ShowPersistentNotification(const nsAString& aPersistentData,
+                                                                nsIAlertNotification* aAlert,
+                                                                nsIObserver* aAlertListener)
+{
+  return ShowAlert(aAlert, aAlertListener);
+}
+
 NS_IMETHODIMP nsSystemAlertsService::ShowAlert(nsIAlertNotification* aAlert,
                                                nsIObserver* aAlertListener)
 {
   NS_ENSURE_ARG(aAlert);
 
   nsAutoString alertName;
   nsresult rv = aAlert->GetName(alertName);
   NS_ENSURE_SUCCESS(rv, rv);
--- a/widget/android/AndroidBridge.cpp
+++ b/widget/android/AndroidBridge.cpp
@@ -503,16 +503,32 @@ AndroidBridge::GetClipboardText(nsAStrin
 
     if (text) {
         aText = text->ToString();
     }
     return !!text;
 }
 
 void
+AndroidBridge::ShowPersistentAlertNotification(const nsAString& aPersistentData,
+                                               const nsAString& aImageUrl,
+                                               const nsAString& aAlertTitle,
+                                               const nsAString& aAlertText,
+                                               const nsAString& aAlertCookie,
+                                               const nsAString& aAlertName,
+                                               nsIPrincipal* aPrincipal)
+{
+    nsAutoString host;
+    nsAlertsUtils::GetSourceHostPort(aPrincipal, host);
+
+    GeckoAppShell::ShowPersistentAlertNotificationWrapper
+        (aPersistentData, aImageUrl, aAlertTitle, aAlertText, aAlertCookie, aAlertName, host);
+}
+
+void
 AndroidBridge::ShowAlertNotification(const nsAString& aImageUrl,
                                      const nsAString& aAlertTitle,
                                      const nsAString& aAlertText,
                                      const nsAString& aAlertCookie,
                                      nsIObserver *aAlertListener,
                                      const nsAString& aAlertName,
                                      nsIPrincipal* aPrincipal)
 {
--- a/widget/android/AndroidBridge.h
+++ b/widget/android/AndroidBridge.h
@@ -172,20 +172,28 @@ public:
     bool GetHWEncoderCapability();
     bool GetHWDecoderCapability();
 
     void GetMimeTypeFromExtensions(const nsACString& aFileExt, nsCString& aMimeType);
     void GetExtensionFromMimeType(const nsACString& aMimeType, nsACString& aFileExt);
 
     bool GetClipboardText(nsAString& aText);
 
+    void ShowPersistentAlertNotification(const nsAString& aPersistentData,
+                                         const nsAString& aImageUrl,
+                                         const nsAString& aAlertTitle,
+                                         const nsAString& aAlertText,
+                                         const nsAString& aAlertCookie,
+                                         const nsAString& aAlertName,
+                                         nsIPrincipal* aPrincipal);
+
     void ShowAlertNotification(const nsAString& aImageUrl,
                                const nsAString& aAlertTitle,
                                const nsAString& aAlertText,
-                               const nsAString& aAlertData,
+                               const nsAString& aAlertCookie,
                                nsIObserver *aAlertListener,
                                const nsAString& aAlertName,
                                nsIPrincipal* aPrincipal);
 
     int GetDPI();
     int GetScreenDepth();
 
     void Vibrate(const nsTArray<uint32_t>& aPattern);
--- a/widget/android/GeneratedJNIWrappers.cpp
+++ b/widget/android/GeneratedJNIWrappers.cpp
@@ -680,16 +680,24 @@ auto GeckoAppShell::SetURITitle(mozilla:
 constexpr char GeckoAppShell::ShowAlertNotificationWrapper_t::name[];
 constexpr char GeckoAppShell::ShowAlertNotificationWrapper_t::signature[];
 
 auto GeckoAppShell::ShowAlertNotificationWrapper(mozilla::jni::String::Param a0, mozilla::jni::String::Param a1, mozilla::jni::String::Param a2, mozilla::jni::String::Param a3, mozilla::jni::String::Param a4, mozilla::jni::String::Param a5) -> void
 {
     return mozilla::jni::Method<ShowAlertNotificationWrapper_t>::Call(GeckoAppShell::Context(), nullptr, a0, a1, a2, a3, a4, a5);
 }
 
+constexpr char GeckoAppShell::ShowPersistentAlertNotificationWrapper_t::name[];
+constexpr char GeckoAppShell::ShowPersistentAlertNotificationWrapper_t::signature[];
+
+auto GeckoAppShell::ShowPersistentAlertNotificationWrapper(mozilla::jni::String::Param a0, mozilla::jni::String::Param a1, mozilla::jni::String::Param a2, mozilla::jni::String::Param a3, mozilla::jni::String::Param a4, mozilla::jni::String::Param a5, mozilla::jni::String::Param a6) -> void
+{
+    return mozilla::jni::Method<ShowPersistentAlertNotificationWrapper_t>::Call(GeckoAppShell::Context(), nullptr, a0, a1, a2, a3, a4, a5, a6);
+}
+
 constexpr char GeckoAppShell::StartMonitoringGamepad_t::name[];
 constexpr char GeckoAppShell::StartMonitoringGamepad_t::signature[];
 
 auto GeckoAppShell::StartMonitoringGamepad() -> void
 {
     return mozilla::jni::Method<StartMonitoringGamepad_t>::Call(GeckoAppShell::Context(), nullptr);
 }
 
--- a/widget/android/GeneratedJNIWrappers.h
+++ b/widget/android/GeneratedJNIWrappers.h
@@ -1425,16 +1425,38 @@ public:
                 "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V";
         static const bool isStatic = true;
         static const mozilla::jni::ExceptionMode exceptionMode =
                 mozilla::jni::ExceptionMode::ABORT;
     };
 
     static auto ShowAlertNotificationWrapper(mozilla::jni::String::Param, mozilla::jni::String::Param, mozilla::jni::String::Param, mozilla::jni::String::Param, mozilla::jni::String::Param, mozilla::jni::String::Param) -> void;
 
+    struct ShowPersistentAlertNotificationWrapper_t {
+        typedef GeckoAppShell Owner;
+        typedef void ReturnType;
+        typedef void SetterType;
+        typedef mozilla::jni::Args<
+                mozilla::jni::String::Param,
+                mozilla::jni::String::Param,
+                mozilla::jni::String::Param,
+                mozilla::jni::String::Param,
+                mozilla::jni::String::Param,
+                mozilla::jni::String::Param,
+                mozilla::jni::String::Param> Args;
+        static constexpr char name[] = "showPersistentAlertNotification";
+        static constexpr char signature[] =
+                "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V";
+        static const bool isStatic = true;
+        static const mozilla::jni::ExceptionMode exceptionMode =
+                mozilla::jni::ExceptionMode::ABORT;
+    };
+
+    static auto ShowPersistentAlertNotificationWrapper(mozilla::jni::String::Param, mozilla::jni::String::Param, mozilla::jni::String::Param, mozilla::jni::String::Param, mozilla::jni::String::Param, mozilla::jni::String::Param, mozilla::jni::String::Param) -> void;
+
     struct StartMonitoringGamepad_t {
         typedef GeckoAppShell Owner;
         typedef void ReturnType;
         typedef void SetterType;
         typedef mozilla::jni::Args<> Args;
         static constexpr char name[] = "startMonitoringGamepad";
         static constexpr char signature[] =
                 "()V";
--- a/widget/cocoa/OSXNotificationCenter.mm
+++ b/widget/cocoa/OSXNotificationCenter.mm
@@ -249,16 +249,24 @@ OSXNotificationCenter::ShowAlertNotifica
                             aAlertText, aAlertTextClickable,
                             aAlertCookie, aBidi, aLang, aData,
                             aPrincipal, aInPrivateBrowsing);
   NS_ENSURE_SUCCESS(rv, rv);
   return ShowAlert(alert, aAlertListener);
 }
 
 NS_IMETHODIMP
+OSXNotificationCenter::ShowPersistentNotification(const nsAString& aPersistentData,
+                                                  nsIAlertNotification* aAlert,
+                                                  nsIObserver* aAlertListener)
+{
+  return ShowAlert(aAlert, aAlertListener);
+}
+
+NS_IMETHODIMP
 OSXNotificationCenter::ShowAlert(nsIAlertNotification* aAlert,
                                  nsIObserver* aAlertListener)
 {
   return ShowAlertWithIconData(aAlert, aAlertListener, 0, nullptr);
 }
 
 NS_IMETHODIMP
 OSXNotificationCenter::ShowAlertWithIconData(nsIAlertNotification* aAlert,