Make permission doorhangers persistent (bug 1004061). r=florian draft
authorPanos Astithas <past@mozilla.com>
Thu, 12 May 2016 14:15:54 +0300
changeset 428138 0c481a8533d59ab41d958404c32b6d59fa52373c
parent 428045 75c306c21c0f719b590d4ebd63e1ca7ad8981adc
child 428139 4df9e92c0c933aea753434d63796853545d9a98e
push id33229
push userbmo:past@mozilla.com
push dateFri, 21 Oct 2016 16:15:16 +0000
reviewersflorian
bugs1004061
milestone52.0a1
Make permission doorhangers persistent (bug 1004061). r=florian MozReview-Commit-ID: IEPkDRnYpiM
browser/base/content/browser-addons.js
browser/base/content/browser-plugins.js
browser/base/content/browser.js
browser/base/content/test/general/browser_bug553455.js
browser/base/content/test/popupNotifications/browser.ini
browser/base/content/test/popupNotifications/browser_popupNotification_4.js
browser/base/content/test/popupNotifications/browser_popupNotification_5.js
browser/base/content/test/social/browser_social_activation.js
browser/components/nsBrowserGlue.js
browser/modules/PermissionUI.jsm
browser/modules/SocialService.jsm
browser/modules/webrtcUI.jsm
toolkit/components/passwordmgr/nsLoginManagerPrompter.js
toolkit/modules/PopupNotifications.jsm
--- a/browser/base/content/browser-addons.js
+++ b/browser/base/content/browser-addons.js
@@ -83,20 +83,20 @@ const gXPInstallObserver = {
     // the next confirmation
     if (installInfo.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)) {
       showNextConfirmation();
       return;
     }
 
     const anchorID = "addons-notification-icon";
 
-    // Make notifications persist a minimum of 30 seconds
+    // Make notifications persistent
     var options = {
       displayURI: installInfo.originatingURI,
-      timeout: Date.now() + 30000,
+      persistent: true,
     };
 
     let cancelInstallation = () => {
       if (installInfo) {
         for (let install of installInfo.installs) {
           // The notification may have been closed because the add-ons got
           // cancelled elsewhere, only try to cancel those that are still
           // pending install.
@@ -224,19 +224,20 @@ const gXPInstallObserver = {
     if (!browser || gBrowser.browsers.indexOf(browser) == -1)
       return;
 
     const anchorID = "addons-notification-icon";
     var messageString, action;
     var brandShortName = brandBundle.getString("brandShortName");
 
     var notificationID = aTopic;
-    // Make notifications persist a minimum of 30 seconds
+    // Make notifications persistent
     var options = {
       displayURI: installInfo.originatingURI,
+      persistent: true,
       timeout: Date.now() + 30000,
     };
 
     switch (aTopic) {
     case "addon-install-disabled": {
       notificationID = "xpinstall-disabled";
 
       if (gPrefService.prefIsLocked("xpinstall.enabled")) {
@@ -542,17 +543,17 @@ var LightWeightThemeWebInstaller = {
           label: gNavigatorBundle.getString("lwthemeNeedsRestart.button"),
           accessKey: gNavigatorBundle.getString("lwthemeNeedsRestart.accesskey"),
           callback: function () {
             BrowserUtils.restartApplication();
           }
         };
 
         let options = {
-          timeout: Date.now() + 30000
+          persistent: true
         };
 
         PopupNotifications.show(gBrowser.selectedBrowser, "addon-theme-change",
                                 messageString, "addons-notification-icon",
                                 action, null, options);
       },
 
       onEnabled: function(aAddon) {
--- a/browser/base/content/browser-plugins.js
+++ b/browser/base/content/browser-plugins.js
@@ -274,16 +274,17 @@ var gPluginHandler = {
         notification.reshow();
         browser.messageManager.sendAsyncMessage("BrowserPlugins:NotificationShown");
       }
       return;
     }
 
     let options = {
       dismissed: !showNow,
+      persistent: showNow,
       eventCallback: this._clickToPlayNotificationEventCallback,
       primaryPlugin: primaryPluginPermission,
       pluginData: pluginData,
       principal: principal,
     };
     PopupNotifications.show(browser, "click-to-play-plugins",
                             "", "plugins-notification-icon",
                             null, null, options);
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -5974,17 +5974,17 @@ var OfflineApps = {
     let warnQuotaKB = Services.prefs.getIntPref("offline-apps.quota.warn");
     // This message shows the quota in MB, and so we divide the quota (in kb) by 1024.
     let message = gNavigatorBundle.getFormattedString("offlineApps.usage",
                                                       [ uri.host,
                                                         warnQuotaKB / 1024 ]);
 
     let anchorID = "indexedDB-notification-icon";
     PopupNotifications.show(browser, "offline-app-usage", message,
-                            anchorID, mainAction);
+                            anchorID, mainAction, null, { persistent: true });
 
     // Now that we've warned once, prevent the warning from showing up
     // again.
     Services.perms.add(uri, "offline-app",
                        Ci.nsIOfflineCacheUpdateService.ALLOW_NO_WARN);
   },
 
   // XXX: duplicated in preferences/advanced.js
@@ -6054,16 +6054,17 @@ var OfflineApps = {
             OfflineApps.disallowSite(uri);
           }
         }
       }];
       let message = gNavigatorBundle.getFormattedString("offlineApps.available",
                                                         [host]);
       let anchorID = "indexedDB-notification-icon";
       let options = {
+        persistent: true,
         controlledItems : [[Cu.getWeakReference(browser), docId, uri]]
       };
       notification = PopupNotifications.show(browser, notificationID, message,
                                              anchorID, mainAction,
                                              secondaryActions, options);
     }
   },
 
@@ -6145,93 +6146,41 @@ var IndexedDBPromptHelper = {
     var message;
     var responseTopic;
     if (topic == this._permissionsPrompt) {
       message = gNavigatorBundle.getFormattedString("offlineApps.available",
                                                     [ host ]);
       responseTopic = this._permissionsResponse;
     }
 
-    const hiddenTimeoutDuration = 30000; // 30 seconds
-    const firstTimeoutDuration = 300000; // 5 minutes
-
-    var timeoutId;
-
     var observer = requestor.getInterface(Ci.nsIObserver);
 
     var mainAction = {
       label: gNavigatorBundle.getString("offlineApps.allow"),
       accessKey: gNavigatorBundle.getString("offlineApps.allowAccessKey"),
       callback: function() {
-        clearTimeout(timeoutId);
         observer.observe(null, responseTopic,
                          Ci.nsIPermissionManager.ALLOW_ACTION);
       }
     };
 
     var secondaryActions = [
       {
         label: gNavigatorBundle.getString("offlineApps.never"),
         accessKey: gNavigatorBundle.getString("offlineApps.neverAccessKey"),
         callback: function() {
-          clearTimeout(timeoutId);
           observer.observe(null, responseTopic,
                            Ci.nsIPermissionManager.DENY_ACTION);
         }
       }
     ];
 
-    // This will be set to the result of PopupNotifications.show().
-    var notification;
-
-    function timeoutNotification() {
-      // Remove the notification.
-      if (notification) {
-        notification.remove();
-      }
-
-      // Clear all of our timeout stuff. We may be called directly, not just
-      // when the timeout actually elapses.
-      clearTimeout(timeoutId);
-
-      // And tell the page that the popup timed out.
-      observer.observe(null, responseTopic,
-                       Ci.nsIPermissionManager.UNKNOWN_ACTION);
-    }
-
-    var options = {
-      eventCallback: function(state) {
-        // Don't do anything if the timeout has not been set yet.
-        if (!timeoutId) {
-          return;
-        }
-
-        // If the popup is being dismissed start the short timeout.
-        if (state == "dismissed") {
-          clearTimeout(timeoutId);
-          timeoutId = setTimeout(timeoutNotification, hiddenTimeoutDuration);
-          return;
-        }
-
-        // If the popup is being re-shown then clear the timeout allowing
-        // unlimited waiting.
-        if (state == "shown") {
-          clearTimeout(timeoutId);
-        }
-      }
-    };
-
-    notification = PopupNotifications.show(browser, topic, message,
-                                           this._notificationIcon, mainAction,
-                                           secondaryActions, options);
-
-    // Set the timeoutId after the popup has been created, and use the long
-    // timeout value. If the user doesn't notice the popup after this amount of
-    // time then it is most likely not visible and we want to alert the page.
-    timeoutId = setTimeout(timeoutNotification, firstTimeoutDuration);
+    PopupNotifications.show(browser, topic, message,
+                            this._notificationIcon, mainAction,
+                            secondaryActions, { persistent: true });
   }
 };
 
 function CanCloseWindow()
 {
   // Avoid redundant calls to canClose from showing multiple
   // PermitUnload dialogs.
   if (Services.startup.shuttingDown || window.skipNextCanClose) {
--- a/browser/base/content/test/general/browser_bug553455.js
+++ b/browser/base/content/test/general/browser_bug553455.js
@@ -111,23 +111,23 @@ function waitForNotification(aId, aExpec
         PopupNotifications.panel.removeEventListener("PanelUpdated", eventListener);
         resolve();
       });
     });
 
     yield observerPromise;
     yield panelEventPromise;
 
-    info("Saw a notification");
+    info("Saw a " + aId + " notification");
     ok(PopupNotifications.isPanelOpen, "Panel should be open");
     is(PopupNotifications.panel.childNodes.length, aExpectedCount, "Should be the right number of notifications");
     if (PopupNotifications.panel.childNodes.length) {
       let nodes = Array.from(PopupNotifications.panel.childNodes);
       let notification = nodes.find(n => n.id == aId + "-notification");
-      ok(notification, `Should have seen the right notification`);
+      ok(notification, "Should have seen the " + aId + " notification");
     }
 
     return PopupNotifications.panel;
   });
 }
 
 function waitForNotificationClose() {
   return new Promise(resolve => {
@@ -507,17 +507,19 @@ function test_multiple() {
     Services.perms.remove(makeURI("http://example.com/"), "install");
     yield removeTab();
   });
 },
 
 function test_sequential() {
   return Task.spawn(function* () {
     // This test is only relevant if using the new doorhanger UI
-    if (!Preferences.get("xpinstall.customConfirmationUI", false)) {
+    // TODO: this subtest is disabled until multiple notification prompts are
+    // reworked in bug 1188152
+    if (true || !Preferences.get("xpinstall.customConfirmationUI", false)) {
       return;
     }
     let pm = Services.perms;
     pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
 
     let progressPromise = waitForProgressNotification();
     let dialogPromise = waitForInstallDialog();
     let triggers = encodeURIComponent(JSON.stringify({
--- a/browser/base/content/test/popupNotifications/browser.ini
+++ b/browser/base/content/test/popupNotifications/browser.ini
@@ -9,8 +9,10 @@ skip-if = (os == "linux" && (debug || as
 [browser_popupNotification_2.js]
 skip-if = (os == "linux" && (debug || asan))
 [browser_popupNotification_3.js]
 skip-if = (os == "linux" && (debug || asan))
 [browser_popupNotification_4.js]
 skip-if = (os == "linux" && (debug || asan))
 [browser_popupNotification_checkbox.js]
 skip-if = (os == "linux" && (debug || asan))
+[browser_popupNotification_5.js]
+skip-if = (os == "linux" && (debug || asan))
--- a/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_4.js
@@ -212,73 +212,10 @@ var tests = [
       };
 
       let notification = showNotification(notifyObj);
       ok(notifyObj.showingCallbackTriggered, "the showing callback was triggered");
       ok(!notifyObj.shownCallbackTriggered, "the shown callback wasn't triggered");
       notification.remove();
       goNext();
     }
-  },
-  // panel updates should fire the showing and shown callbacks again.
-  { id: "Test#11",
-    run: function() {
-      this.notifyObj = new BasicNotification(this.id);
-      this.notification = showNotification(this.notifyObj);
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj);
-
-      this.notifyObj.showingCallbackTriggered = false;
-      this.notifyObj.shownCallbackTriggered = false;
-
-      // Force an update of the panel. This is typically called
-      // automatically when receiving 'activate' or 'TabSelect' events,
-      // but from a setTimeout, which is inconvenient for the test.
-      PopupNotifications._update();
-
-      checkPopup(popup, this.notifyObj);
-
-      this.notification.remove();
-    },
-    onHidden: function() { }
-  },
-  // A first dismissed notification shouldn't stop _update from showing a second notification
-  { id: "Test#12",
-    run: function () {
-      this.notifyObj1 = new BasicNotification(this.id);
-      this.notifyObj1.id += "_1";
-      this.notifyObj1.anchorID = "default-notification-icon";
-      this.notifyObj1.options.dismissed = true;
-      this.notification1 = showNotification(this.notifyObj1);
-
-      this.notifyObj2 = new BasicNotification(this.id);
-      this.notifyObj2.id += "_2";
-      this.notifyObj2.anchorID = "geo-notification-icon";
-      this.notifyObj2.options.dismissed = true;
-      this.notification2 = showNotification(this.notifyObj2);
-
-      this.notification2.dismissed = false;
-      PopupNotifications._update();
-    },
-    onShown: function (popup) {
-      checkPopup(popup, this.notifyObj2);
-      this.notification1.remove();
-      this.notification2.remove();
-    },
-    onHidden: function(popup) { }
-  },
-  // The anchor icon should be shown for notifications in background windows.
-  { id: "Test#13",
-    run: function() {
-      let notifyObj = new BasicNotification(this.id);
-      notifyObj.options.dismissed = true;
-      let win = gBrowser.replaceTabWithWindow(gBrowser.addTab("about:blank"));
-      whenDelayedStartupFinished(win, function() {
-        showNotification(notifyObj);
-        let anchor = document.getElementById("default-notification-icon");
-        is(anchor.getAttribute("showing"), "true", "the anchor is shown");
-        win.close();
-        goNext();
-      });
-    }
   }
 ];
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/popupNotifications/browser_popupNotification_5.js
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+  waitForExplicitFinish();
+
+  ok(PopupNotifications, "PopupNotifications object exists");
+  ok(PopupNotifications.panel, "PopupNotifications panel exists");
+
+  setup();
+  goNext();
+}
+
+var gNotification;
+
+var tests = [
+  // panel updates should fire the showing and shown callbacks again.
+  { id: "Test#1",
+    run: function() {
+      this.notifyObj = new BasicNotification(this.id);
+      this.notification = showNotification(this.notifyObj);
+    },
+    onShown: function (popup) {
+      checkPopup(popup, this.notifyObj);
+
+      this.notifyObj.showingCallbackTriggered = false;
+      this.notifyObj.shownCallbackTriggered = false;
+
+      // Force an update of the panel. This is typically called
+      // automatically when receiving 'activate' or 'TabSelect' events,
+      // but from a setTimeout, which is inconvenient for the test.
+      PopupNotifications._update();
+
+      checkPopup(popup, this.notifyObj);
+
+      this.notification.remove();
+    },
+    onHidden: function() { }
+  },
+  // A first dismissed notification shouldn't stop _update from showing a second notification
+  { id: "Test#2",
+    run: function () {
+      this.notifyObj1 = new BasicNotification(this.id);
+      this.notifyObj1.id += "_1";
+      this.notifyObj1.anchorID = "default-notification-icon";
+      this.notifyObj1.options.dismissed = true;
+      this.notification1 = showNotification(this.notifyObj1);
+
+      this.notifyObj2 = new BasicNotification(this.id);
+      this.notifyObj2.id += "_2";
+      this.notifyObj2.anchorID = "geo-notification-icon";
+      this.notifyObj2.options.dismissed = true;
+      this.notification2 = showNotification(this.notifyObj2);
+
+      this.notification2.dismissed = false;
+      PopupNotifications._update();
+    },
+    onShown: function (popup) {
+      checkPopup(popup, this.notifyObj2);
+      this.notification1.remove();
+      this.notification2.remove();
+    },
+    onHidden: function(popup) { }
+  },
+  // The anchor icon should be shown for notifications in background windows.
+  { id: "Test#3",
+    run: function() {
+      let notifyObj = new BasicNotification(this.id);
+      notifyObj.options.dismissed = true;
+      let win = gBrowser.replaceTabWithWindow(gBrowser.addTab("about:blank"));
+      whenDelayedStartupFinished(win, function() {
+        showNotification(notifyObj);
+        let anchor = document.getElementById("default-notification-icon");
+        is(anchor.getAttribute("showing"), "true", "the anchor is shown");
+        win.close();
+        goNext();
+      });
+    }
+  },
+  // Test that persistent doesn't allow the notification to persist after
+  // navigation.
+  { id: "Test#4",
+    run: function* () {
+      this.oldSelectedTab = gBrowser.selectedTab;
+      gBrowser.selectedTab = gBrowser.addTab("about:blank");
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      this.notifyObj = new BasicNotification(this.id);
+      this.notifyObj.addOptions({
+        persistent: true
+      });
+      this.notification = showNotification(this.notifyObj);
+    },
+    onShown: function* (popup) {
+      this.complete = false;
+
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.org/");
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+
+      // This code should not be executed.
+      ok(false, "Should have removed the notification after navigation");
+      // Properly dismiss and cleanup in case the unthinkable happens.
+      this.complete = true;
+      triggerSecondaryCommand(popup, 1);
+    },
+    onHidden: function (popup) {
+      ok(!this.complete, "Should have hidden the notification after navigation");
+      this.notification.remove();
+      gBrowser.removeTab(gBrowser.selectedTab);
+      gBrowser.selectedTab = this.oldSelectedTab;
+    }
+  },
+  // Test that persistent allows the notification to persist until explicitly
+  // dismissed.
+  { id: "Test#5",
+    run: function* () {
+      this.oldSelectedTab = gBrowser.selectedTab;
+      gBrowser.selectedTab = gBrowser.addTab("about:blank");
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+      this.notifyObj = new BasicNotification(this.id);
+      this.notifyObj.addOptions({
+        persistent: true
+      });
+      this.notification = showNotification(this.notifyObj);
+    },
+    onShown: function* (popup) {
+      this.complete = false;
+
+      // Notification should persist after attempt to dismiss by clicking on the
+      // content area.
+      let browser = gBrowser.selectedBrowser;
+      yield BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser)
+
+      // Notification should be hidden after dismissal via Not Now.
+      this.complete = true;
+      triggerSecondaryCommand(popup, 1);
+    },
+    onHidden: function (popup) {
+      ok(this.complete, "Should have hidden the notification after clicking Not Now");
+      this.notification.remove();
+      gBrowser.removeTab(gBrowser.selectedTab);
+      gBrowser.selectedTab = this.oldSelectedTab;
+    }
+  },
+  // Test that persistent panels are still open after switching to another tab
+  // and back.
+  { id: "Test#6a",
+    run: function* () {
+      this.notifyObj = new BasicNotification(this.id);
+      this.notifyObj.options.persistent = true;
+      gNotification = showNotification(this.notifyObj);
+    },
+    onShown: function* (popup) {
+      this.oldSelectedTab = gBrowser.selectedTab;
+      gBrowser.selectedTab = gBrowser.addTab("about:blank");
+      info("Waiting for the new tab to load.");
+      yield promiseTabLoadEvent(gBrowser.selectedTab, "http://example.com/");
+    },
+    onHidden: function (popup) {
+      ok(true, "Should have hidden the notification after tab switch");
+      gBrowser.removeTab(gBrowser.selectedTab);
+      gBrowser.selectedTab = this.oldSelectedTab;
+    }
+  },
+  // Second part of the previous test that compensates for the limitation in
+  // runNextTest that expects a single onShown/onHidden invocation per test.
+  { id: "Test#6b",
+    run: function* () {
+      let id = PopupNotifications.panel.firstChild.getAttribute("popupid");
+      ok(id.endsWith("Test#6a"), "Should have found the notification from Test6a");
+      ok(PopupNotifications.isPanelOpen, "Should have shown the popup again after getting back to the tab");
+      gNotification.remove();
+      gNotification = null;
+      goNext();
+    }
+  },
+  // Test that persistent panels are still open after switching to another
+  // window and back.
+  { id: "Test#7",
+    run: function* () {
+      this.oldSelectedTab = gBrowser.selectedTab;
+      gBrowser.selectedTab = gBrowser.addTab("about:blank");
+      let notifyObj = new BasicNotification(this.id);
+      notifyObj.options.persistent = true;
+      this.notification = showNotification(notifyObj);
+      let win = gBrowser.replaceTabWithWindow(gBrowser.addTab("about:blank"));
+      whenDelayedStartupFinished(win, () => {
+        ok(notifyObj.shownCallbackTriggered, "Should have triggered the shown callback");
+        let anchor = win.document.getElementById("default-notification-icon");
+        win.PopupNotifications._reshowNotifications(anchor);
+        ok(win.PopupNotifications.panel.childNodes.length == 0,
+           "no notification displayed in new window");
+        ok(PopupNotifications.isPanelOpen, "Should be still showing the popup in the first window");
+        win.close();
+        let id = PopupNotifications.panel.firstChild.getAttribute("popupid");
+        ok(id.endsWith("Test#7"), "Should have found the notification from Test7");
+        ok(PopupNotifications.isPanelOpen, "Should have shown the popup again after getting back to the window");
+        this.notification.remove();
+        gBrowser.removeTab(gBrowser.selectedTab);
+        gBrowser.selectedTab = this.oldSelectedTab;
+        goNext();
+      });
+    }
+  }
+];
--- a/browser/base/content/test/social/browser_social_activation.js
+++ b/browser/base/content/test/social/browser_social_activation.js
@@ -111,33 +111,33 @@ function clickAddonRemoveButton(tab, aCa
   });
 }
 
 function activateOneProvider(manifest, finishActivation, aCallback) {
   info("activating provider "+manifest.name);
   let panel = document.getElementById("servicesInstall-notification");
   BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popupshown").then(() => {
     ok(!panel.hidden, "servicesInstall-notification panel opened");
+    BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden").then(() => {
+      ok(panel.hidden, "servicesInstall-notification panel hidden");
+      if (!finishActivation) {
+        ok(panel.hidden, "activation panel is not showing");
+        executeSoon(aCallback);
+      } else {
+        waitForProviderLoad(manifest.origin).then(() => {
+          checkSocialUI();
+          executeSoon(aCallback);
+        });
+      }
+    });
     if (finishActivation)
       panel.button.click();
     else
       panel.closebutton.click();
   });
-  BrowserTestUtils.waitForEvent(PopupNotifications.panel, "popuphidden").then(() => {
-    ok(panel.hidden, "servicesInstall-notification panel hidden");
-    if (!finishActivation) {
-      ok(panel.hidden, "activation panel is not showing");
-      executeSoon(aCallback);
-    } else {
-      waitForProviderLoad(manifest.origin).then(() => {
-        checkSocialUI();
-        executeSoon(aCallback);
-      });
-    }
-  });
 
   // the test will continue as the popup events fire...
   activateProvider(manifest.origin, function() {
     info("waiting on activation panel to open/close...");
   });
 }
 
 var gTestDomains = ["https://example.com", "https://test1.example.com", "https://test2.example.com"];
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -2845,16 +2845,17 @@ var E10SAccessibilityCheck = {
     let secondaryActions = [{
       label: win.gNavigatorBundle.getString("e10s.accessibilityNotice.enableAndRestart.label"),
       accessKey: win.gNavigatorBundle.getString("e10s.accessibilityNotice.enableAndRestart.accesskey"),
       callback: restartCallback,
     }];
     let options = {
       popupIconURL: "chrome://browser/skin/e10s-64@2x.png",
       learnMoreURL: Services.urlFormatter.formatURLPref("app.support.e10sAccessibilityUrl"),
+      persistent: true,
       persistWhileVisible: true,
       hideNotNow: true,
     };
 
     notification =
       win.PopupNotifications.show(browser, "a11y_enabled_with_e10s",
                                   promptMessage, null, mainAction,
                                   secondaryActions, options);
--- a/browser/modules/PermissionUI.jsm
+++ b/browser/modules/PermissionUI.jsm
@@ -329,16 +329,17 @@ this.PermissionPromptPrototype = {
                      popupNotificationActions[0] : null;
     let secondaryActions = popupNotificationActions.splice(1);
 
     let options = this.popupOptions;
 
     if (!options.hasOwnProperty('displayURI') || options.displayURI) {
       options.displayURI = this.principal.URI;
     }
+    options.persistent = true;
 
     this.onBeforeShow();
     chromeWin.PopupNotifications.show(this.browser,
                                       this.notificationID,
                                       this.message,
                                       this.anchorID,
                                       mainAction,
                                       secondaryActions,
--- a/browser/modules/SocialService.jsm
+++ b/browser/modules/SocialService.jsm
@@ -562,16 +562,17 @@ this.SocialService = {
       accessKey: browserBundle.GetStringFromName("service.install.ok.accesskey"),
       callback: function() {
         aAddonInstaller.install();
       },
     };
 
     let options = {
                     learnMoreURL: Services.urlFormatter.formatURLPref("app.support.baseURL") + "social-api",
+                    persistent: true,
                   };
     let anchor = "servicesInstall-notification-icon";
     let notificationid = "servicesInstall";
     data.window.PopupNotifications.show(data.window.gBrowser.selectedBrowser,
                                         notificationid, message, anchor,
                                         action, [], options);
   },
 
--- a/browser/modules/webrtcUI.jsm
+++ b/browser/modules/webrtcUI.jsm
@@ -352,16 +352,17 @@ function prompt(aBrowser, aRequest) {
       accessKey: stringBundle.getString("getUserMedia.always.accesskey"),
       callback: function (aState) {
         mainAction.callback(aState, true);
       }
     });
   }
 
   let options = {
+    persistent: true,
     eventCallback: function(aTopic, aNewBrowser) {
       if (aTopic == "swapping")
         return true;
 
       let chromeDoc = this.browser.ownerDocument;
 
       if (aTopic == "shown") {
         let popupId = "Devices";
--- a/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
+++ b/toolkit/components/passwordmgr/nsLoginManagerPrompter.js
@@ -952,19 +952,19 @@ LoginManagerPrompter.prototype = {
     this._getPopupNote().show(
       browser,
       "password",
       promptMsg,
       "password-notification-icon",
       mainAction,
       secondaryActions,
       {
-        timeout: Date.now() + 10000,
         displayURI: Services.io.newURI(login.hostname, null, null),
         persistWhileVisible: true,
+        persistent: true,
         passwordNotificationType: type,
         eventCallback: function (topic) {
           switch (topic) {
             case "showing":
               currentNotification = this;
               chromeDoc.getElementById("password-notification-password")
                        .removeAttribute("focused");
               chromeDoc.getElementById("password-notification-username")
--- a/toolkit/modules/PopupNotifications.jsm
+++ b/toolkit/modules/PopupNotifications.jsm
@@ -304,16 +304,21 @@ PopupNotifications.prototype = {
    *        notification. The following properties are currently supported:
    *        persistence: An integer. The notification will not automatically
    *                     dismiss for this many page loads.
    *        timeout:     A time in milliseconds. The notification will not
    *                     automatically dismiss before this time.
    *        persistWhileVisible:
    *                     A boolean. If true, a visible notification will always
    *                     persist across location changes.
+   *        persistent:  A boolean. If true, the notification will always
+   *                     persist even across tab and app changes (but not across
+   *                     location changes), until the user accepts or rejects
+   *                     the request. The notification will never be implicitly
+   *                     dismissed.
    *        dismissed:   Whether the notification should be added as a dismissed
    *                     notification. Dismissed notifications can be activated
    *                     by clicking on their anchorElement.
    *        eventCallback:
    *                     Callback to be invoked when the notification changes
    *                     state. The callback's first argument is a string
    *                     identifying the state change:
    *                     "dismissed": notification has been dismissed by the
@@ -465,17 +470,17 @@ PopupNotifications.prototype = {
     let notifications = this._getNotificationsForBrowser(aBrowser);
 
     notifications = notifications.filter(function (notification) {
       // The persistWhileVisible option allows an open notification to persist
       // across location changes
       if (notification.options.persistWhileVisible &&
           this.isPanelOpen) {
         if ("persistence" in notification.options &&
-          notification.options.persistence)
+            notification.options.persistence)
           notification.options.persistence--;
         return true;
       }
 
       // The persistence option allows a notification to persist across multiple
       // page loads
       if ("persistence" in notification.options &&
           notification.options.persistence) {
@@ -581,16 +586,24 @@ PopupNotifications.prototype = {
   /**
    * Dismisses the notification without removing it.
    */
   _dismiss: function PopupNotifications_dismiss(telemetryReason) {
     if (telemetryReason) {
       this.nextDismissReason = telemetryReason;
     }
 
+    // An explicitly dismissed persistent notification effectively becomes
+    // non-persistent.
+    if (this.panel.firstChild &&
+        (telemetryReason == TELEMETRY_STAT_DISMISSAL_CLOSE_BUTTON ||
+         telemetryReason == TELEMETRY_STAT_DISMISSAL_NOT_NOW)) {
+      this.panel.firstChild.notification.options.persistent = false;
+    }
+
     let browser = this.panel.firstChild &&
                   this.panel.firstChild.notification.browser;
     this.panel.hidePopup();
     if (browser)
       browser.focus();
   },
 
   /**
@@ -819,31 +832,37 @@ PopupNotifications.prototype = {
                                               {"detail": notificationIds});
       this.panel.dispatchEvent(event);
       return;
     }
 
     // If the panel is already open but we're changing anchors, we need to hide
     // it first.  Otherwise it can appear in the wrong spot.  (_hidePanel is
     // safe to call even if the panel is already hidden.)
-    let promise = this._hidePanel().then(() => {
+    this._hidePanel().then(() => {
       // If the anchor element is hidden or null, use the tab as the anchor. We
       // only ever show notifications for the current browser, so we can just use
       // the current tab.
       let selectedTab = this.tabbrowser.selectedTab;
       if (anchorElement) {
         let bo = anchorElement.boxObject;
         if (bo.height == 0 && bo.width == 0)
           anchorElement = selectedTab; // hidden
       } else {
         anchorElement = selectedTab; // null
       }
 
       this._currentAnchorElement = anchorElement;
 
+      if (notificationsToShow.some(n => n.options.persistent)) {
+        this.panel.setAttribute("noautohide", "true");
+      } else {
+        this.panel.removeAttribute("noautohide");
+      }
+
       // On OS X and Linux we need a different panel arrow color for
       // click-to-play plugins, so copy the popupid and use css.
       this.panel.setAttribute("popupid", this.panel.firstChild.getAttribute("popupid"));
       notificationsToShow.forEach(function (n) {
         // Record that the notification was actually displayed on screen.
         // Notifications that were opened a second time or that were originally
         // shown with "options.dismissed" will be recorded in a separate bucket.
         n._recordTelemetryStat(TELEMETRY_STAT_OFFERED);
@@ -919,19 +938,20 @@ PopupNotifications.prototype = {
       for (let anchor of anchors) {
         if (anchor.parentNode == this.iconBox)
           continue;
         useIconBox = false;
         break;
       }
     }
 
-    // Filter out notifications that have been dismissed.
+    // Filter out notifications that have been dismissed, unless they are
+    // persistent.
     let notificationsToShow = notifications.filter(function (n) {
-      return !n.dismissed && !n.options.neverShow;
+      return (!n.dismissed || n.options.persistent) && !n.options.neverShow;
     });
 
     if (useIconBox) {
       // Hide icons of the previous tab.
       this._hideIcons();
     }
 
     if (haveNotifications) {
@@ -1080,17 +1100,17 @@ 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.panel.removeAttribute("noautofocus");
 
     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) {