<?xml version="1.0"?>

<!DOCTYPE bindings [
<!ENTITY % notificationDTD SYSTEM "chrome://global/locale/notification.dtd">
%notificationDTD;
]>

<bindings id="browserNotificationBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xbl="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

  <binding id="browser-notificationbox"
           extends="chrome://global/content/bindings/notification.xml#notificationbox">
    <implementation implements="nsIObserver, nsIWebProgressListener, nsIWebProgressListener2, nsIDOMEventListener">
      <field name="_stringBundle" readonly="true">
        <![CDATA[
          Components.classes["@mozilla.org/intl/stringbundle;1"]
                    .getService(Components.interfaces.nsIStringBundleService)
                    .createBundle("chrome://communicator/locale/notification.properties");
        ]]>
      </field>

      <field name="_brandStringBundle" readonly="true">
        <![CDATA[
          Components.classes["@mozilla.org/intl/stringbundle;1"]
                    .getService(Components.interfaces.nsIStringBundleService)
                    .createBundle("chrome://branding/locale/brand.properties");
        ]]>
      </field>

      <field name="_prefs" readonly="true">
        <![CDATA[
          Components.classes["@mozilla.org/preferences-service;1"]
                    .getService(Components.interfaces.nsIPrefBranch2);
        ]]>
      </field>

      <field name="_urlFormatter" readonly="true">
        <![CDATA[
          Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
                    .getService(Components.interfaces.nsIURLFormatter);
        ]]>
      </field>

      <field name="wrappedJSObject">this</field>

      <field name="_activeBrowser">null</field>

      <property name="activeBrowser" readonly="true">
        <getter>
          <![CDATA[
            if (!this._activeBrowser) {
              const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
              var browsers = this.getElementsByTagNameNS(XUL_NS, "browser");
              for (var i = 0; this._activeBrowser = browsers.item(i); i++)
                if (!this._activeBrowser.hidden)
                  break;
            }
            return this._activeBrowser;
          ]]>
        </getter>
      </property>

      <method name="addProgressListener">
        <body>
          <![CDATA[
            if (this.activeBrowser && !this._addedProgressListener) {
              this.activeBrowser.webProgress
                  .addProgressListener(this, Components.interfaces.nsIWebProgress.NOTIFY_LOCATION |
                                             Components.interfaces.nsIWebProgress.NOTIFY_REFRESH);
              this._addedProgressListener = true;
            }
          ]]>
        </body>
      </method>

      <method name="onLocationChange">
        <parameter name="aWebProgress" />
        <parameter name="aRequest" />
        <parameter name="aLocation" />
        <body>
          <![CDATA[
            var errorPage = aRequest && !Components.isSuccessCode(aRequest.status);
            if (aWebProgress.DOMWindow == this.activeBrowser.contentWindow &&
                (aWebProgress.isLoadingDocument || errorPage)) {
              this.missingPlugins = {};
              this.crashNotified = false;
              if (this.popupCount) {
                this.popupCount = 0;
                this.notifyPopupCountChanged();
              }
              // XXX should be: removeTransientNotifications(true) once bug 303327 is fixed
              this.removeTransientNotifications();
            }
          ]]>
        </body>
      </method>

      <method name="onRefreshAttempted">
        <parameter name="aWebProgress" />
        <parameter name="aURI" />
        <parameter name="aDelay" />
        <parameter name="aSameURI" />
        <body>
          <![CDATA[
            if (this._prefs.getBoolPref("accessibility.blockautorefresh")) {
              let brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
              let refreshButtonText =
                this._stringBundle.GetStringFromName("refreshBlocked.goButton");
              let refreshButtonAccesskey =
                this._stringBundle.GetStringFromName("refreshBlocked.goButton.accesskey");
              let message =
                this._stringBundle.formatStringFromName(aSameURI ? "refreshBlocked.refreshLabel"
                                                                 : "refreshBlocked.redirectLabel",
                                                        [brandShortName], 1);
              let notification = this.getNotificationWithValue("refresh-blocked");
              if (notification) {
                notification.label = message;
              } else {
                let buttons = [{
                  label: refreshButtonText,
                  accessKey: refreshButtonAccesskey,
                  callback: function (aNotification, aButton) {
                    var refreshURI = aNotification.webProgress
                                                  .QueryInterface(Components.interfaces.nsIRefreshURI);
                    refreshURI.forceRefreshURI(aNotification.uri,
                                               aNotification.delay, true);
                  }
                }];
                notification =
                  this.appendNotification(message, "refresh-blocked",
                                          "chrome://communicator/skin/icons/application.png",
                                          this.PRIORITY_INFO_MEDIUM,
                                          buttons);
              }
              // In the case of a meta refresh, the location has already
              // changed. But in the case of an HTTP header refresh, the
              // location changes synchronously after this call returns.
              // Set the persistence to 1 temporarily to stop this from
              // immediately clobbering the location bar (bug 516441),
              // but set the persistence back to 0 as soon as possible.
              setTimeout(function() { notification.persistence = 0; }, 0);
              notification.persistence = 1;
              notification.webProgress = aWebProgress;
              notification.uri = aURI;
              notification.delay = aDelay;
              return false;
            }
            return true;
          ]]>
        </body>
      </method>

      <method name="observe">
        <parameter name="aSubject" />
        <parameter name="aTopic" />
        <parameter name="aData" />
        <body>
          <![CDATA[
            var browser = this.activeBrowser;

            // inactive sidebar panel:
            if (!browser || !browser.docShell)
              return;

            switch (aTopic) {
              case "indexedDB-permissions-prompt":
                var requestor = aSubject.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
                var contentWindow = requestor.getInterface(Components.interfaces.nsIDOMWindow);
                if (contentWindow.top == browser.contentWindow)
                  this.promptIndexedDB(requestor, contentWindow, "permissions");
                break;

              case "indexedDB-quota-prompt":
                var requestor = aSubject.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
                var contentWindow = requestor.getInterface(Components.interfaces.nsIDOMWindow);
                if (contentWindow.top == browser.contentWindow)
                  this.promptIndexedDB(requestor, contentWindow, "quota", aData);
                break;

              case "indexedDB-quota-cancel":
                var requestor = aSubject.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
                var contentWindow = requestor.getInterface(Components.interfaces.nsIDOMWindow);
                if (contentWindow.top == browser.contentWindow)
                  this.cancelIndexedDB(requestor, contentWindow, "quota", aData);
                break;

              case "addon-install-blocked":
                var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo);
                if (installInfo.originatingWindow.top == browser.contentWindow)
                  this.addonInstallBlocked(installInfo);
                break;

              case "addon-install-complete":
                var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo);
                if (installInfo.originatingWindow.top == browser.contentWindow)
                  this.addonInstallComplete(installInfo);
                break;

              case "addon-install-disabled":
                var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo);
                if (installInfo.originatingWindow.top == browser.contentWindow)
                  this.addonInstallDisabled(installInfo);
                break;

              case "addon-install-failed":
                var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo);
                if (installInfo.originatingWindow.top == browser.contentWindow)
                  this.addonInstallFailed(installInfo);
                break;

              case "addon-install-started":
                var installInfo = aSubject.QueryInterface(Components.interfaces.amIWebInstallInfo);
                if (installInfo.originatingWindow.top == browser.contentWindow)
                  this.addonInstallStarted(installInfo);
                break;

              case "dom-storage-warn-quota-exceeded":
                this.checkUsage(aSubject.document.documentURIObject);
                break;

              case "offline-cache-update-completed":
                var doc = browser.contentDocument;
                if (!doc.documentElement)
                  break;

                var manifest = doc.documentElement.getAttribute("manifest");
                if (!manifest)
                  break;

                try {
                  var ioService = Components.classes["@mozilla.org/network/io-service;1"]
                                            .getService(Components.interfaces.nsIIOService);
                  var manifestURI = ioService.newURI(manifest, doc.characterSet, doc.documentURIObject);
                  var cacheUpdate = aSubject.QueryInterface(Components.interfaces.nsIOfflineCacheUpdate);
                  if (manifestURI.equals(cacheUpdate.manifestURI))
                    this.checkUsage(manifestURI);
                } catch (e) {
                  alert(e);
                }
                break;

              case "nsPref:changed":
                if (aData == "privacy.popups.showBrowserMessage") {
                  if (this._prefs.getBoolPref(aData))
                    return;

                  var popupNotification = this.getNotificationWithValue("popup-blocked");
                  if (popupNotification)
                    this.removeNotification(popupNotification);
                }

                if (aData == "plugins.hide_infobar_for_outdated_plugin") {
                  if (!this._prefs.getBoolPref(aData))
                    return;

                  var outdatedNotification = this.getNotificationWithValue("outdated-plugins");
                  if (outdatedNotification)
                    this.removeNotification(outdatedNotification);
                }

                if (aData == "plugins.hide_infobar_for_carbon_failure_plugin") {
                  if (!this._prefs.getBoolPref(aData))
                    return;

                  var carbonfailureNotification = this.getNotificationWithValue("carbon-failure-plugins");
                  if (carbonfailureNotification)
                    this.removeNotification(carbonfailureNotification);
                }

                if (aData == "plugins.hide_infobar_for_missing_plugin") {
                  if (!this._prefs.getBoolPref(aData))
                    return;

                  var missingNotification = this.getNotificationWithValue("missing-plugins");
                  if (missingNotification)
                    this.removeNotification(missingNotification);

                  var blockedNotification = this.getNotificationWithValue("blocked-plugins");
                  if (blockedNotification)
                    this.removeNotification(blockedNotification);
                }

                if (aData == "dom.disable_open_during_load") {
                  // remove notifications when popup blocking has been turned off
                  if (this._prefs.getBoolPref(aData) || !this.popupCount)
                    return;

                  var popupNotification = this.getNotificationWithValue("popup-blocked");
                  if (popupNotification)
                    this.removeNotification(popupNotification);
                  this.popupCount = 0;
                  this.notifyPopupCountChanged();
                }
                break;

              case "perm-changed":
                var permission = aSubject.QueryInterface(Components.interfaces.nsIPermission);
                if (permission.type != "popup" || aData != "added" || !this.popupCount)
                  return;

                try {
                  var hostport = this.activeBrowser.currentURI.hostPort;
                } catch (ex) {
                  // we can't do much without a hostport here
                  return;
                }
                var host = '.' + permission.host;
                hostport = '.' + hostport;

                if (host == hostport.slice(-host.length)) {
                  var popupNotification = this.getNotificationWithValue("popup-blocked");
                  if (popupNotification)
                    this.removeNotification(popupNotification);
                  this.popupCount = 0;
                  this.notifyPopupCountChanged();
                }
                break;
            }
          ]]>
        </body>
      </method>

      <field name="missingPlugins">{}</field>

      <field name="CrashSubmit">null</field>

      <field name="crashNotified">false</field>

      <method name="getPluginInfo">
        <parameter name="pluginElement"/>
        <body>
          <![CDATA[
            var mimetype;
            var pluginsPage;
            if (pluginElement instanceof HTMLAppletElement) {
              mimetype = "application/x-java-vm";
            } else {
              if (pluginElement instanceof HTMLObjectElement) {
                pluginsPage = pluginElement.codebase;
              } else {
                pluginsPage = pluginElement.getAttribute("pluginspage");
                if (pluginsPage) {
                  var doc = pluginElement.ownerDocument;
                  var ioService = Components.classes["@mozilla.org/network/io-service;1"]
                                            .getService(Components.interfaces.nsIIOService);
                  pluginsPage = ioService.newURI(pluginsPage, doc.characterSet, doc.documentURIObject).spec;
                }
              }

              mimetype = pluginElement.QueryInterface(Components.interfaces.nsIObjectLoadingContent)
                                      .actualType;

              if (!mimetype) {
                mimetype = pluginElement.type;
              }
            }

            return {mimetype: mimetype, pluginsPage: pluginsPage};
          ]]>
        </body>
      </method>

      <method name="pluginUnavailable">
        <parameter name="aEvent"/>
        <parameter name="aNotification"/>
        <parameter name="aMessage"/>
        <parameter name="aButtons"/>
        <parameter name="aPref"/>
        <body>
          <![CDATA[
            var plugin = aEvent.target;
            // Since we are expecting also untrusted events, make sure
            // that the target is a plugin.
            if (!(plugin instanceof Components.interfaces.nsIObjectLoadingContent))
              return;

            // Save information on the plugin to give to the plugin finder.
            var pluginInfo = this.getPluginInfo(plugin);
            this.missingPlugins[pluginInfo.mimetype] = pluginInfo;

            // Hide the in-content UI if it's too big. The crashed plugin handler already does this.
            var doc = plugin.ownerDocument;
            var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");
            if (this.isTooSmall(plugin, overlay))
              overlay.style.visibility = "hidden";

            if (this._prefs.getBoolPref(aPref || "plugins.hide_infobar_for_missing_plugin"))
              return;

            var notification;
            var notifications = [
              "blocked-plugins",
              "missing-plugins",
              "carbon-failure-plugins",
              "outdated-plugins",
            ];

            // Remove lower priority notifications.
            for (var i = 0; notifications[i] != aNotification; i++) {
              notification = this.getNotificationWithValue(notifications[i]);
              if (notification)
                notification.close();
            }

            // Skip if an equal or higher priority notification exists.
            while (i < notifications.length) {
              notification = this.getNotificationWithValue(notifications[i++]);
              if (notification)
                return;
            }

            // Now we can display our notification.
            const priority = this.PRIORITY_WARNING_MEDIUM;
            const iconURL = "chrome://mozapps/skin/plugins/pluginGeneric.png";
            this.appendNotification(aMessage, aNotification,
                                    iconURL, priority, aButtons);
          ]]>
        </body>
      </method>

      <method name="openURLPref">
        <parameter name="aPref"/>
        <body>
          <![CDATA[
            var url = this._urlFormatter.formatURLPref(aPref);
            var nsIBrowserDOMWindow = Components.interfaces.nsIBrowserDOMWindow;

            var browserWin;
            var whereToOpen = this._prefs.getIntPref("browser.link.open_external");

            if (whereToOpen != nsIBrowserDOMWindow.OPEN_NEWWINDOW) {
              var windowManager = Components.classes['@mozilla.org/appshell/window-mediator;1']
                                            .getService(Components.interfaces.nsIWindowMediator);
              browserWin = windowManager.getMostRecentWindow("navigator:browser");
            }

            if (!browserWin) {
              var browserURL = "chrome://navigator/content/navigator.xul";
              try {
                browserURL = this._prefs.getCharPref("browser.chromeURL");
              } catch (ex) {}

              window.openDialog(browserURL, "_blank", "chrome,all,dialog=no", url);
            } else {
              if (whereToOpen == nsIBrowserDOMWindow.OPEN_CURRENTWINDOW)
                browserWin.loadURI(url);
              else {
                // new tab
                var browser = browserWin.getBrowser();
                var newTab = browser.addTab(url);
                browser.selectedTab = newTab;
              }
              browserWin.content.focus();
            }
            return true;
          ]]>
        </body>
      </method>

      <method name="makeNicePluginName">
        <parameter name="aName"/>
        <parameter name="aFilename"/>
        <body>
          <![CDATA[
            if (aName == "Shockwave Flash")
              return "Adobe Flash";

            // Clean up the plugin name by stripping off any trailing version numbers
            // or "plugin". EG, "Foo Bar Plugin 1.23_02" --> "Foo Bar"
            var newName = aName.replace(/\bplug-?in\b/i, "").replace(/[\s\d\.\-\_\(\)]+$/, "");
            return newName;
          ]]>
        </body>
      </method>

      <method name="isTooSmall">
        <parameter name="aPlugin"/>
        <parameter name="aOverlay"/>
        <body>
          <![CDATA[
            // Is the <object>'s size too small to hold what we want to show?
            var pluginRect = aPlugin.getBoundingClientRect();
            // XXX bug 446693. The text-shadow on the submitted-report text at
            //     the bottom causes scrollHeight to be larger than it should be.
            return (aOverlay.scrollWidth > pluginRect.width) ||
                   (aOverlay.scrollHeight - 5 > pluginRect.height);
          ]]>
        </body>
      </method>

      <method name="addLinkClickCallback">
        <parameter name="linkNode"/>
        <parameter name="callback"/>
        <body>
          <![CDATA[
            // XXX just doing (callback)(arg) was giving a same-origin error. bug?
            let callbackArgs = Array.slice(arguments, 2);
            linkNode.addEventListener("click",
                                      function(aEvent) {
                                        if (!aEvent.isTrusted)
                                          return;
                                        aEvent.preventDefault();
                                        if (callbackArgs.length == 0)
                                          callbackArgs = [ aEvent ];
                                        callback.apply(this, callbackArgs);
                                      }.bind(this),
                                      true);

            linkNode.addEventListener("keydown",
                                      function(aEvent) {
                                        if (!aEvent.isTrusted)
                                          return;
                                        if (aEvent.keyCode == aEvent.DOM_VK_RETURN)
                                          return;
                                        aEvent.preventDefault();
                                        if (callbackArgs.length == 0)
                                          callbackArgs = [ aEvent ];
                                        callback.apply(this, callbackArgs);
                                      }.bind(this),
                                      true);
                  ]]>
        </body>
      </method>

      <!-- When user clicks try, check if the crash report should be sent in the background -->
      <method name="retryPluginPage">
        <parameter name="status"/>
        <parameter name="submitChk"/>
        <parameter name="pluginDumpID"/>
        <parameter name="browserDumpID"/>
        <body>
          <![CDATA[
            // Check status to make sure we haven't submitted already.
            if (status == "please" && submitChk.checked)
              this.submitReport(pluginDumpID, browserDumpID);
            this.reloadPage();
          ]]>
        </body>
      </method>

      <!-- Callback for user clicking "submit a report" link -->
      <method name="submitReport">
        <parameter name="pluginDumpID"/>
        <parameter name="browserDumpID"/>
        <body>
          <![CDATA[
            this.CrashSubmit.submit(pluginDumpID);
            if (browserDumpID)
              this.CrashSubmit.submit(browserDumpID);
          ]]>
        </body>
      </method>

      <!-- Callback for user clicking a "reload page" link -->
      <method name="reloadPage">
        <body>
          <![CDATA[
            this.activeBrowser.reload();
          ]]>
        </body>
      </method>

      <!-- Callback for user clicking the help icon -->
      <method name="openHelpPage">
        <body>
          <![CDATA[
            //XXX Ratty need in app help here.
            openHelp("plugin-crashed", "chrome://communicator/locale/help/suitehelp.rdf");
          ]]>
        </body>
      </method>

      <method name="showPluginCrashedNotification">
        <parameter name="pluginDumpID"/>
        <parameter name="browserDumpID"/>
        <parameter name="messageString"/>
        <body>
          <![CDATA[
            // If there's already an existing notification bar, don't do anything.
            var notification = this.getNotificationWithValue("plugin-crashed");
            if (notification)
              return;

            // Configure the notification bar
            var priority = this.PRIORITY_WARNING_MEDIUM;
            var iconURL = "chrome://mozapps/skin/plugins/notifyPluginCrashed.png";
            var reloadLabel = this._stringBundle.GetStringFromName("crashedpluginsMessage.reloadButton.label");
            var reloadKey   = this._stringBundle.GetStringFromName("crashedpluginsMessage.reloadButton.accesskey");
            var submitLabel = this._stringBundle.GetStringFromName("crashedpluginsMessage.submitButton.label");
            var submitKey   = this._stringBundle.GetStringFromName("crashedpluginsMessage.submitButton.accesskey");

            var buttons = [{
              label: reloadLabel,
              accessKey: reloadKey,
              popup: null,
              callback: this.reloadPage.bind(this)
            }];

            if (this.CrashSubmit && pluginDumpID) {
              let submitButton = {
                label: submitLabel,
                accessKey: submitKey,
                popup: null,
                callback: this.submitReport.bind(this, pluginDumpID, browserDumpID)
              };
              buttons.push(submitButton);
            }

            var notification = this.appendNotification(messageString, "plugin-crashed",
                                                       iconURL, priority, buttons);

            // Add the "learn more" link.
            var XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
            var link = notification.ownerDocument.createElementNS(XULNS, "label");
            link.className = "text-link";
            link.setAttribute("value", this._stringBundle.GetStringFromName("crashedpluginsMessage.learnMore"));
            this.addLinkClickCallback(link, this.openHelpPage);
            var description = notification.ownerDocument.getAnonymousElementByAttribute(notification, "anonid", "messageText");
            description.appendChild(link);
          ]]>
        </body>
      </method>

      <method name="installMissingPlugins">
        <body>
          <![CDATA[
            window.openDialog("chrome://mozapps/content/plugins/pluginInstallerWizard.xul",
                              "", "chrome,centerscreen,resizable=yes",
                              {plugins: this.missingPlugins, browser: this.activeBrowser});
          ]]>
        </body>
      </method>

      <method name="playSoundForBlockedPopup">
        <body>
          <![CDATA[
            const kCustomSound = 1;
            var playSound = this._prefs.getBoolPref("privacy.popups.sound_enabled");

            if (playSound) {
              var sound = Components.classes["@mozilla.org/sound;1"]
                                    .createInstance(Components.interfaces.nsISound);

              var soundType = this._prefs.getIntPref("privacy.popups.sound_type");
              if (soundType == kCustomSound) {
                var soundUrlSpec = this._prefs.getCharPref("privacy.popups.sound_url");
                var fileHandler = Components.classes["@mozilla.org/network/protocol;1?name=file"]
                                            .getService(Components.interfaces.nsIFileProtocolHandler);
                var file = fileHandler.getFileFromURLSpec(soundUrlSpec);
                if (file.exists()) {
                  var soundUrl = fileHandler.newFileURI(file);
                  sound.play(soundUrl);
                  return;
                }
              }

              // Either a custom sound is selected which does not exist
              // or the system beep was selected, so make the system beep.
              sound.beep();
            }
          ]]>
        </body>
      </method>

      <field name="popupCount">0</field>

      <method name="notifyPopupCountChanged">
        <body>
          <![CDATA[
            var event = document.createEvent("Events");
            event.initEvent("PopupCountChanged", true, true);
            this.dispatchEvent(event);
          ]]>
        </body>
      </method>

      <method name="allowPopupsForSite">
        <parameter name="aEvent"/>
        <body>
          <![CDATA[
            const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
            var uri = this.activeBrowser.currentURI;
            var pm = Components.classes["@mozilla.org/permissionmanager;1"]
                               .getService(nsIPermissionManager);
            pm.add(uri, "popup", nsIPermissionManager.ALLOW_ACTION);

            this.removeCurrentNotification();
          ]]>
        </body>
      </method>

      <method name="offlineAppRequested">
        <parameter name="aDocument"/>
        <body>
          <![CDATA[
            var documentURI = aDocument.documentURIObject;
            const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
            var pm = Components.classes["@mozilla.org/permissionmanager;1"]
                               .getService(nsIPermissionManager);

            if (pm.testExactPermission(documentURI, "offline-app") !=
                nsIPermissionManager.UNKNOWN_ACTION)
              return;

            var host = documentURI.asciiHost;
            var notificationName = "offline-app-requested-" + host;
            var notification = this.getNotificationWithValue(notificationName);
            if (notification)
              notification.documents.push(aDocument);
            else {
              var buttons = [{
                label: this._stringBundle.GetStringFromName("offlineApps.allow"),
                accessKey: this._stringBundle.GetStringFromName("offlineApps.allow.accesskey"),
                callback: function() {
                  pm.add(documentURI, "offline-app", nsIPermissionManager.ALLOW_ACTION);
                  var ioService = Components.classes["@mozilla.org/network/io-service;1"]
                                            .getService(Components.interfaces.nsIIOService);
                  var updateService = Components.classes["@mozilla.org/offlinecacheupdate-service;1"]
                                                .getService(Components.interfaces.nsIOfflineCacheUpdateService);
                  notification.documents.forEach(function(aDocument) {
                    if (!aDocument.documentElement)
                      return;
                    var manifest = aDocument.documentElement.getAttribute("manifest");
                    if (!manifest)
                      return;

                    try {
                      var manifestURI = ioService.newURI(manifest, aDocument.characterSet, aDocument.documentURIObject);
                      updateService.scheduleUpdate(manifestURI, aDocument.documentURIObject, window);
                    } catch (e) {
                    }
                  });
                }
              }, {
                label: this._stringBundle.GetStringFromName("offlineApps.deny"),
                accessKey: this._stringBundle.GetStringFromName("offlineApps.deny.accesskey"),
                callback: function() {
                  pm.add(documentURI, "offline-app", nsIPermissionManager.DENY_ACTION);
                }
              }, {
                label: this._stringBundle.GetStringFromName("offlineApps.later"),
                accessKey: this._stringBundle.GetStringFromName("offlineApps.later.accesskey"),
                callback: function() { /* no-op */ }
              }];

              var messageString = this._stringBundle.formatStringFromName("offlineApps.permissions", [host], 1);
              var priority = this.PRIORITY_INFO_LOW;
              notification = this.appendNotification(messageString, notificationName,
                                                     null, priority, buttons);
              notification.documents = [aDocument];
            }
          ]]>
        </body>
      </method>

      <method name="checkUsage">
        <parameter name="aURI"/>
        <body>
          <![CDATA[
            const nsIPermissionManager = Components.interfaces.nsIPermissionManager;
            var pm = Components.classes["@mozilla.org/permissionmanager;1"]
                               .getService(nsIPermissionManager);
            if (pm.testExactPermission(aURI, "offline-app") ==
                Components.interfaces.nsIOfflineCacheUpdateService.ALLOW_NO_WARN)
              return;

            var host = aURI.asciiHost;
            var usage = Components.classes["@mozilla.org/dom/storagemanager;1"]
                                  .getService(Components.interfaces.nsIDOMStorageManager)
                                  .getUsage(host);

            var ioService = Components.classes["@mozilla.org/network/io-service;1"]
                                      .getService(Components.interfaces.nsIIOService);
            var cacheService = Components.classes["@mozilla.org/network/application-cache-service;1"]
                                         .getService(Components.interfaces.nsIApplicationCacheService);
            cacheService.getGroups().forEach(function(aGroup) {
              var uri = ioService.newURI(aGroup, null, null);
              if (uri.asciiHost == host)
                usage += cacheService.getActiveCache(aGroup).usage;
            });
            var warnQuota = this._prefs.getIntPref("offline-apps.quota.warn");
            if (usage < warnQuota * 1024)
              return;

            var message = this._stringBundle.formatStringFromName("offlineApps.quota", [host, warnQuota / 1024], 2);
              var priority = this.PRIORITY_WARNING_MEDIUM;
            this.appendNotification(message, "offline-app-usage", null,
                                    priority, null);
            pm.add(aURI, "offline-app",
                   Components.interfaces.nsIOfflineCacheUpdateService.ALLOW_NO_WARN);
          ]]>
        </body>
      </method>

      <method name="showRightsNotification">
        <body>
          <![CDATA[
            var rightsBundle = Components.classes["@mozilla.org/intl/stringbundle;1"]
                                         .getService(Components.interfaces.nsIStringBundleService)
                                         .createBundle("chrome://branding/locale/aboutRights.properties");
            var buttonLabel = rightsBundle.GetStringFromName("buttonLabel");
            var buttonAccessKey = rightsBundle.GetStringFromName("buttonAccessKey");
            var productName = this._brandStringBundle.GetStringFromName("brandFullName");
            var notifyRightsText = rightsBundle.formatStringFromName("notifyRightsText", [productName], 1);

            var buttons = [{
              label: buttonLabel,
              accessKey: buttonAccessKey,
              popup: null,
              callback: function (aNotificationBox, aButton) {
                var browser = document.getBindingParent(aNotificationBox);
                browser.addTab("about:rights", { focusNewTab: true });
              }
            }];
            var box = this.appendNotification(notifyRightsText, "about-rights",
                                              null, this.PRIORITY_INFO_LOW,
                                              buttons);
            box.persistence = 3; // arbitrary number, just so bar sticks around for a bit
          ]]>
        </body>
      </method>

      <method name="showPlacesLockedWarning">
        <body>
          <![CDATA[
            var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
            var message = this._stringBundle.formatStringFromName("lockPrompt.text", [brandShortName], 1);
            var buttons = [{
              label: this._stringBundle.GetStringFromName("lockPromptInfoButton.label"),
              accessKey: this._stringBundle.GetStringFromName("lockPromptInfoButton.accesskey"),
              popup: null,
              callback: function() {
                openHelp("places-locked", "chrome://communicator/locale/help/suitehelp.rdf");
              }
            }];
            var box = this.appendNotification(message, "places-locked", null,
                                              this.PRIORITY_CRITICAL_MEDIUM,
                                              buttons);
            box.persistence = -1; // until user closes it
          ]]>
        </body>
      </method>

      <method name="showUpdateWarning">
        <body>
          <![CDATA[
            var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
            var message = this._stringBundle.formatStringFromName("updatePrompt.text", [brandShortName], 1);
            var buttons = [{
              label: this._stringBundle.GetStringFromName("updatePromptCheckButton.label"),
              accessKey: this._stringBundle.GetStringFromName("updatePromptCheckButton.accesskey"),
              popup: null,
              callback: function() {
                Components.classes["@mozilla.org/updates/update-prompt;1"]
                          .createInstance(Components.interfaces.nsIUpdatePrompt)
                          .checkForUpdates();
              }
            }];
            var box = this.appendNotification(message, "update-warning", null,
                                              this.PRIORITY_CRITICAL_MEDIUM,
                                              buttons);
            box.persistence = -1; // until user closes it
          ]]>
        </body>
      </method>

      <method name="removeNotifications">
        <parameter name="aNotifications"/>
        <body>
          <![CDATA[
            aNotifications.forEach(function(value) {
              var box = this.getNotificationWithValue(value);
              if (box)
                this.removeNotification(box);
            }, this);
          ]]>
        </body>
      </method>

      <method name="lwthemeInstallRequest">
        <parameter name="aHost"/>
        <parameter name="aCallback"/>
        <body>
          <![CDATA[
            var message = this._stringBundle.formatStringFromName("lwthemeInstallRequest.message", [aHost], 1);
            var buttons = [{
              label: this._stringBundle.GetStringFromName("lwthemeInstallRequest.allowButton"),
              accessKey: this._stringBundle.GetStringFromName("lwthemeInstallRequest.allowButton.accesskey"),
              popup: null,
              callback: aCallback
            }];
            var box = this.appendNotification(message,
                                              "lwtheme-install-request", null,
                                              this.PRIORITY_INFO_MEDIUM,
                                              buttons);
            box.persistence = 1;
          ]]>
        </body>
      </method>

      <method name="lwthemeInstallNotification">
        <parameter name="aCallback"/>
        <body>
          <![CDATA[
            var message = this._stringBundle.GetStringFromName("lwthemeInstallNotification.message");
            var buttons = [{
              label: this._stringBundle.GetStringFromName("lwthemeInstallNotification.undoButton"),
              accessKey: this._stringBundle.GetStringFromName("lwthemeInstallNotification.undoButton.accesskey"),
              callback: aCallback
            }, {
              label: this._stringBundle.GetStringFromName("lwthemeInstallNotification.manageButton"),
              accessKey: this._stringBundle.GetStringFromName("lwthemeInstallNotification.manageButton.accesskey"),
              callback: function() {
                window.toEM("addons://list/theme");
              }
            }];
            var box = this.appendNotification(message,
                                              "lwtheme-install-notification",
                                              null, this.PRIORITY_INFO_MEDIUM,
                                              buttons);
            box.persistence = 1;
            box.timeout = Date.now() + 20000; // 20 seconds
          ]]>
        </body>
      </method>

      <method name="lwthemeNeedsRestart">
        <parameter name="aNewThemeName"/>
        <body>
          <![CDATA[
            var message = this._stringBundle.formatStringFromName("lwthemeNeedsRestart.message", [aNewThemeName], 1);
            var buttons = [{
              label: this._stringBundle.GetStringFromName("lwthemeNeedsRestart.restartButton"),
              accessKey: this._stringBundle.GetStringFromName("lwthemeNeedsRestart.restartButton.accesskey"),
              popup: null,
              callback: function() {
                Application.restart();
              }
            }];
            var box = this.appendNotification(message,
                                              "lwtheme-install-notification",
                                               null,this.PRIORITY_INFO_MEDIUM,
                                              buttons);
            box.persistence = 1;
            box.timeout = Date.now() + 20000; // 20 seconds
          ]]>
        </body>
      </method>

      <method name="showGeolocationPrompt">
        <parameter name="file"/>
        <parameter name="site"/>
        <parameter name="image"/>
        <parameter name="allowCallback"/>
        <parameter name="cancelCallback"/>
        <body>
          <![CDATA[
            var type = "geolocation";
            if (this.getNotificationWithValue(type))
              return;

            var buttons = [{
              label: this._stringBundle.GetStringFromName(type + ".shareLocation"),
              accessKey: this._stringBundle.GetStringFromName(type + ".shareLocation.accesskey"),
              popup: null,
              callback: function (aNotificationBox, aButton) {
                allowCallback(aNotificationBox.checkbox.checked);
              }
            }, {
              label: this._stringBundle.GetStringFromName(type + ".dontShareLocation"),
              accessKey: this._stringBundle.GetStringFromName(type + ".dontShareLocation.accesskey"),
              popup: null,
              callback: function (aNotificationBox, aButton) {
                cancelCallback(aNotificationBox.checkbox.checked);
              }
            }];
            var message;
            if (site)
              message = this._stringBundle
                            .formatStringFromName(type + ".siteWantsToKnow",
                                                  [site], 1);
            else
              message = this._stringBundle
                            .formatStringFromName(type + ".fileWantsToKnow",
                                                  [file], 1);
            var box = this.appendNotification(message, type, image,
                                              this.PRIORITY_INFO_HIGH, buttons);
            // Create the checkbox on a timeout to allow the XBL to bind first.
            setTimeout(function(stringBundle, formatter) {
              if (site) {
                box.checkbox = document.createElement("checkbox");
                box.checkbox.className = "rememberChoice";
                box.checkbox.setAttribute("label", stringBundle.GetStringFromName(type + ".remember"));
                box.appendChild(box.checkbox);
              }
              var link = document.createElement("label");
              link.className = "text-link";
              link.setAttribute("value", stringBundle.GetStringFromName(type + ".learnMore"));

              link.setAttribute("href", formatter.formatURLPref("browser.geolocation.warning.infoURL"));

              document.getAnonymousElementByAttribute(box, "anonid", "messageText").appendChild(link);
            }, 0, this._stringBundle, this._urlFormatter);

            // Create a dummy checkbox so file requests don't try to remember.
            box.checkbox = { checked: false };
          ]]>
        </body>
      </method>

      <method name="promptIndexedDB">
        <parameter name="aRequestor"/>
        <parameter name="aWindow"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body>
          <![CDATA[
            var host = aWindow.document.documentURIObject.asciiHost;
            var message = this._stringBundle.formatStringFromName("offlineApps." + aTopic, [host, aData], 2);
            var observer = aRequestor.getInterface(Components.interfaces.nsIObserver);
            var nsIPermissionManager = Components.interfaces.nsIPermissionManager;
            var buttons = [{
              label: this._stringBundle.GetStringFromName("offlineApps.allow"),
              accessKey: this._stringBundle.GetStringFromName("offlineApps.allow.accesskey"),
              popup: null,
              callback: function allowIndexedDB() {
                clearTimeout(box.timeout);
                observer.observe(null, "indexedDB-" + aTopic + "-response",
                                 nsIPermissionManager.ALLOW_ACTION);
              }
            }, {
              label: this._stringBundle.GetStringFromName("offlineApps.later"),
              accessKey: this._stringBundle.GetStringFromName("offlineApps.later.accesskey"),
              popup: null,
              callback: function laterIndexedDB() {
                clearTimeout(box.timeout);
                observer.observe(null, "indexedDB-" + aTopic + "-response",
                                 nsIPermissionManager.UNKNOWN_ACTION);
              }
            }, {
              label: this._stringBundle.GetStringFromName("offlineApps.deny"),
              accessKey: this._stringBundle.GetStringFromName("offlineApps.deny.accesskey"),
              popup: null,
              callback: function denyIndexedDB() {
                clearTimeout(box.timeout);
                observer.observe(null, "indexedDB-" + aTopic + "-response",
                                 nsIPermissionManager.DENY_ACTION);
              }
            }];
            var box = this.appendNotification(message, "indexedDB-" + aTopic + "-prompt", null,
                                              this.PRIOITY_INFO_HIGH, buttons);
            box.timeout = setTimeout(function() {
              observer.observe(null, "indexedDB-" + aTopic + "-response",
                               nsIPermissionManager.UNKNOWN_ACTION);
              if (box.parentNode)
                box.parentNode.removeNotification(box);
            }, 300000); // 5 minutes
          ]]>
        </body>
      </method>

      <method name="cancelIndexedDB">
        <parameter name="aRequestor"/>
        <parameter name="aWindow"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body>
          <![CDATA[
            var popupNotification = this.getNotificationWithValue("indexedDB-" + aTopic + "-prompt");
            if (popupNotification) {
              clearTimeout(popupNotification.timeout);
              this.removeNotification(popupNotification);
            }

            var observer = aRequestor.getInterface(Components.interfaces.nsIObserver);
            observer.observe(null, "indexedDB-" + aTopic + "-response",
                             nsIPermissionManager.UNKNOWN_ACTION);
          ]]>
        </body>
      </method>

      <method name="addonInstallBlocked">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var host;
            try {
              // this fails with nsSimpleURIs like data: URIs
              host = installInfo.originatingURI.host;
            } catch (ex) {
              host = this._stringBundle.GetStringFromName("xpinstallHostNotAvailable");
            }
            var notificationName = "addon-install-blocked";
            var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
            var messageString = this._stringBundle.formatStringFromName("xpinstallPromptWarning",
                                                                        [brandShortName, host], 2);
            var buttons = [{
              label: this._stringBundle.GetStringFromName("xpinstallPromptInstallButton"),
              accessKey: this._stringBundle.GetStringFromName("xpinstallPromptInstallButton.accesskey"),
              popup: null,
              callback: function allowInstall() {
                installInfo.install();
                return false;
              }
            }];

            if (!this.getNotificationWithValue(notificationName)) {
              var priority = this.PRIORITY_WARNING_MEDIUM;
              var iconURL = "chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png";
              this.appendNotification(messageString, notificationName,
                                      iconURL, priority, buttons);
            }
          ]]>
        </body>
      </method>

      <method name="addonInstallCancelled">
        <parameter name="installInfo"/>
        <parameter name="sourceURI"/>
        <body>
          <![CDATA[
            var tmp = {};
            Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);
            var notificationName = "addon-install-cancelled";
            var messageString = this._stringBundle.GetStringFromName("addonDownloadCancelled");
            messageString = tmp.PluralForm.get(installInfo.installs.length, messageString);
            var buttons = [{
              label: this._stringBundle.GetStringFromName("addonDownloadRestartButton"),
              accessKey: this._stringBundle.GetStringFromName("addonDownloadRestartButton.accesskey"),
              popup: null,
              callback: function() {
                var weblistener = Components.classes["@mozilla.org/addons/web-install-listener;1"]
                                            .getService(Components.interfaces.amIWebInstallListener);
                if (weblistener.onWebInstallRequested(installInfo.originatingWindow, sourceURI,
                                                      installInfo.installs, installInfo.installs.length)) {
                  installs.forEach(function(aInstall) {
                    aInstall.install();
                  });
                }
              }
            }];
            var priority = this.PRIORITY_INFO_MEDIUM;
            var iconURL = "chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png";
            this.appendNotification(messageString, notificationName,
                                    iconURL, priority, buttons);

            installInfo.installs.every(function(aInstall) {
              aInstall.cancel();
            });
            return true; // the downloading notification closed automatically
          ]]>
        </body>
      </method>

      <method name="addonInstallComplete">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var tmp = {};
            Components.utils.import("resource://gre/modules/AddonManager.jsm", tmp);
            Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);

            var notificationName = "addon-install-complete"
            var addonNotification = this.getNotificationWithValue(notificationName);
            if (addonNotification)
              this.removeNotification(addonNotification);

            var buttons = [];
            var messageString;
            if (installInfo.installs.some(function(install)
                  install.addon.pendingOperations &
                  tmp.AddonManager.PENDING_INSTALL)) {
              messageString = this._stringBundle.GetStringFromName("addonsInstalledNeedsRestart");
              buttons.push({
                label: this._stringBundle.GetStringFromName("addonInstallRestartButton"),
                accessKey: this._stringBundle.GetStringFromName("addonInstallRestartButton.accesskey"),
                callback: function () {
                  Application.restart();
                }
              });
            } else {
              messageString = this._stringBundle.GetStringFromName("addonsInstalled");
            }

            if ("toEM" in window) {
              buttons.push({
                label: this._stringBundle.GetStringFromName("addonInstallManageButton"),
                accessKey: this._stringBundle.GetStringFromName("addonInstallManageButton.accesskey"),
                callback: function() {
                  window.toEM("addons://list/extension");
                }
              });
            }

            var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
            messageString = tmp.PluralForm.get(installInfo.installs.length, messageString)
                                          .replace("#1", installInfo.installs[0].name)
                                          .replace("#2", installInfo.installs.length)
                                          .replace("#3", brandShortName);
            var priority = this.PRIORITY_WARNING_MEDIUM;
            var iconURL = "chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png";
            this.appendNotification(messageString, notificationName,
                                    iconURL, priority, buttons);
          ]]>
        </body>
      </method>

      <method name="addonInstallDisabled">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var messageString, buttons;
            var prefBranch = this._prefs; // used by editPrefs callback

            var notificationName = "addon-install-disabled";
            if (prefBranch.prefIsLocked("xpinstall.enabled")) {
              messageString = this._stringBundle.GetStringFromName("xpinstallDisabledMessageLocked");
              buttons = [];
            } else {
              messageString = this._stringBundle.GetStringFromName("xpinstallDisabledMessage");
              buttons = [{
                label: this._stringBundle.GetStringFromName("xpinstallDisabledButton"),
                accessKey: this._stringBundle.GetStringFromName("xpinstallDisabledButton.accesskey"),
                popup: null,
                callback: function editPrefs() {
                  prefBranch.setBoolPref("xpinstall.enabled", true);
                  return false;
                }
              }];
            }

            if (!this.getNotificationWithValue(notificationName)) {
              var priority = this.PRIORITY_WARNING_MEDIUM;
              var iconURL = "chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png";
              this.appendNotification(messageString, notificationName,
                                      iconURL, priority, buttons);
            }
          ]]>
        </body>
      </method>

      <method name="addonInstallFailed">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var notificationName = "addon-install-failed";
            if (!this.getNotificationWithValue(notificationName)) {
              var host;
              try {
                // this fails with nsSimpleURIs like data: URIs
                host = installInfo.originatingURI.host;
              } catch (ex) {
                host = this._stringBundle.GetStringFromName("xpinstallHostNotAvailable");
              }

              var error = "addonErrorIncompatible";
              var name = installInfo.installs[0].name;
              installInfo.installs.some(function(install) {
                if (install.error) {
                  name = install.name;
                  error = "addonError" + install.error;
                  return true;
                }
                if (install.addon.blocklistState ==
                    Components.interfaces.nsIBlocklistService.STATE_BLOCKED) {
                  name = install.name;
                  error = "addonErrorBlocklisted";
                }
                return false;
              });

              var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
              var version = Components.classes["@mozilla.org/xre/app-info;1"]
                                      .getService(Components.interfaces.nsIXULAppInfo).version;
              var messageString = this._stringBundle.GetStringFromName(error)
                                                    .replace("#1", name)
                                                    .replace("#2", host)
                                                    .replace("#3", brandShortName)
                                                    .replace("#4", version);

              var priority = this.PRIORITY_WARNING_MEDIUM;
              var iconURL = "chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png";
              this.appendNotification(messageString, notificationName,
                                      iconURL, priority, []);
            }
          ]]>
        </body>
      </method>

      <method name="addonInstallStarted">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var tmp = {};
            Components.utils.import("resource://gre/modules/AddonManager.jsm", tmp);
            if (installInfo.installs.every(function(aInstall) {
                  return aInstall.state == tmp.AddonManager.STATE_DOWNLOADED;
                }))
              return;

            var sourceURI = installInfo.originatingWindow.document.documentURIObject;
            Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);
            var notificationName = "addon-install-started";
            var messageString = this._stringBundle.GetStringFromName("addonDownloading");
            messageString = tmp.PluralForm.get(installInfo.installs.length, messageString);
            var buttons = [{
              label: this._stringBundle.GetStringFromName("addonDownloadCancelButton"),
              accessKey: this._stringBundle.GetStringFromName("addonDownloadCancelButton.accesskey"),
              popup: null,
              callback: this.addonInstallCancelled.bind(this, installInfo, sourceURI)
            }];
            var priority = this.PRIORITY_INFO_MEDIUM;
            var iconURL = "chrome://mozapps/skin/xpinstall/xpinstallItemGeneric.png";
            var box = this.appendNotification(messageString, notificationName,
                                              iconURL, priority, buttons);
            box.installInfo = installInfo;
            installInfo.installs.forEach(function(aInstall) {
              aInstall.addListener(box);
            });
          ]]>
        </body>
      </method>

      <constructor>
        <![CDATA[
          var os = Components.classes["@mozilla.org/observer-service;1"]
                             .getService(Components.interfaces.nsIObserverService);
          os.addObserver(this, "indexedDB-permissions-prompt", false);
          os.addObserver(this, "indexedDB-quota-prompt", false);
          os.addObserver(this, "indexedDB-quota-cancel", false);
          os.addObserver(this, "addon-install-blocked", false);
          os.addObserver(this, "addon-install-complete", false);
          os.addObserver(this, "addon-install-disabled", false);
          os.addObserver(this, "addon-install-failed", false);
          os.addObserver(this, "addon-install-started", false);
          os.addObserver(this, "dom-storage-warn-quota-exceeded", false);
          os.addObserver(this, "offline-cache-update-completed", false);
          os.addObserver(this, "perm-changed", false);

          this._prefs.addObserver("plugins.hide_infobar_for_outdated_plugin", this, false);
          this._prefs.addObserver("plugins.hide_infobar_for_carbon_failure_plugin", this, false);
          this._prefs.addObserver("plugins.hide_infobar_for_missing_plugin", this, false);
          this._prefs.addObserver("privacy.popups.showBrowserMessage", this, false);
          this._prefs.addObserver("dom.disable_open_during_load", this, false);

          this.addProgressListener();

          if ("nsICrashReporter" in Components.interfaces)
            Components.utils.import("resource://gre/modules/CrashSubmit.jsm", this);
        ]]>
      </constructor>

      <destructor>
        <![CDATA[
          this.destroy();
        ]]>
      </destructor>

      <field name="mDestroyed">false</field>

      <!-- This is necessary because the destructor doesn't always get called when
            we are removed from a tabbrowser. This will be explicitly called by tabbrowser -->
      <method name="destroy">
        <body>
          <![CDATA[
            if (this.mDestroyed)
              return;
            this.mDestroyed = true;

            if (this._addedProgressListener) {
              this.activeBrowser.webProgress.removeProgressListener(this);
              this._addedProgressListener = false;
            }

            this._activeBrowser = null;
            var os = Components.classes["@mozilla.org/observer-service;1"]
                                .getService(Components.interfaces.nsIObserverService);
            try {
              os.removeObserver(this, "indexedDB-permissions-prompt");
            } catch (ex) {}
            try {
              os.removeObserver(this, "indexedDB-quota-prompt");
            } catch (ex) {}
            try {
              os.removeObserver(this, "indexedDB-quota-cancel");
            } catch (ex) {}
            try {
              os.removeObserver(this, "addon-install-blocked");
            } catch (ex) {}
            try {
              os.removeObserver(this, "addon-install-complete");
            } catch (ex) {}
            try {
              os.removeObserver(this, "addon-install-disabled");
            } catch (ex) {}
            try {
              os.removeObserver(this, "addon-install-failed");
            } catch (ex) {}
            try {
              os.removeObserver(this, "addon-install-started");
            } catch (ex) {}
            try {
              os.removeObserver(this, "dom-storage-warn-quota-exceeded");
            } catch (ex) {}
            try {
              os.removeObserver(this, "offline-cache-update-completed");
            } catch (ex) {}
            try {
              os.removeObserver(this, "perm-changed");
            } catch (ex) {}

            try {
              this._prefs.removeObserver("plugins.hide_infobar_for_outdated_plugin", this);
            } catch (ex) {}
            try {
              this._prefs.removeObserver("plugins.hide_infobar_for_carbon_failure_plugin", this);
            } catch (ex) {}
            try {
              this._prefs.removeObserver("plugins.hide_infobar_for_missing_plugin", this);
            } catch (ex) {}
            try {
              this._prefs.removeObserver("privacy.popups.showBrowserMessage", this);
            } catch (ex) {}
            try {
              this._prefs.removeObserver("dom.disable_open_during_load", this);
            } catch (ex) {}
          ]]>
        </body>
      </method>
    </implementation>

    <handlers>
      <handler event="DOMUpdatePageReport" phase="capturing">
        <![CDATA[
          var browser = this.activeBrowser;
          if (!browser.pageReport)
            return;

          // this.popupCount can be 0, while browser.pageReport has not been cleared.
          if (!this.popupCount && browser.pageReport.length > 1) {
            this.popupCount = browser.pageReport.length;
          } else {
            this.popupCount++;
          }
          this.playSoundForBlockedPopup();
          this.notifyPopupCountChanged();

          // Check for duplicates and remove the old occurence of this url,
          // to update the features.
          var lastItemPlace = browser.pageReport.length - 1;
          var lastItem = browser.pageReport[lastItemPlace];
          for (var i = 0; i < lastItemPlace; i++) {
            if (browser.pageReport[i].popupWindowURI.equals(lastItem.popupWindowURI)) {
              browser.pageReport.splice(i, 1);
              break;
            }
          }

          // Limit the length of the menu to some reasonable size.
          // We only add one item every time in browser.xml, so no need for more complex stuff.
          if (browser.pageReport.length > 100)
            browser.pageReport.shift();

          if (this._prefs.getBoolPref("privacy.popups.showBrowserMessage"))
          {
            var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
            var message;
            if (this.popupCount > 1)
              message = this._stringBundle.formatStringFromName("popupWarningMultiple", [brandShortName, this.popupCount], 2);
            else
              message = this._stringBundle.formatStringFromName("popupWarning", [brandShortName], 1);

            var notification = this.getNotificationWithValue("popup-blocked");
            if (notification) {
              notification.label = message;
            } else {
              var popupButtonText = this._stringBundle.GetStringFromName("popupWarningButton");
              var popupButtonAccesskey = this._stringBundle.GetStringFromName("popupWarningButton.accesskey");
              var buttons = [{
                label: popupButtonText,
                accessKey: popupButtonAccesskey,
                popup: "popupNotificationMenu",
                callback: null
              }];

              const priority = this.PRIORITY_WARNING_MEDIUM;
              this.appendNotification(message, "popup-blocked",
                                      "chrome://navigator/skin/icons/popup-blocked.png",
                                      priority, buttons);
            }
          }
        ]]>
      </handler>

      <handler event="PluginCrashed">
        <![CDATA[
          var plugin = event.target;
          // Since we are expecting also untrusted events, make sure
          // that the target is a plugin
          if (!(plugin instanceof Components.interfaces.nsIObjectLoadingContent))
            return;

          if (!(event instanceof Components.interfaces.nsIDOMDataContainerEvent))
            return;

          var submittedReport = event.getData("submittedCrashReport");
          var doPrompt        = true; // XXX followup to get via gCrashReporter;
          var submitReports   = false;
          var pluginName      = event.getData("pluginName");
          var pluginFilename  = event.getData("pluginFilename");
          var pluginDumpID    = event.getData("pluginDumpID");
          var browserDumpID   = event.getData("browserDumpID");

          // Remap the plugin name to a more user-presentable form.
          pluginName = this.makeNicePluginName(pluginName, pluginFilename);

          // Force a style flush, so that we ensure our binding is attached.
          plugin.clientTop;

          // Configure the crashed-plugin placeholder.
          var doc = plugin.ownerDocument;
          var overlay = doc.getAnonymousElementByAttribute(plugin, "class", "mainBox");

          var status;
          var statusDiv = doc.getAnonymousElementByAttribute(plugin, "class", "submitStatus");
          var submitChk = doc.getAnonymousElementByAttribute(plugin, "class", "pleaseSubmitCheckbox");
          var cr = Components.classes["@mozilla.org/toolkit/crash-reporter;1"]
                             .getService(Components.interfaces.nsICrashReporter);

          if (this.CrashSubmit) {
            submitReports = cr.submitReports;
            // Determine which message to show regarding crash reports.
            if (submittedReport) { // submitReports && !doPrompt, handled in observer
              status = "submitted";
            }
            else if (!submitReports && !doPrompt) {
              status = "noSubmit";
            }
            else { // doPrompt
              // link submit checkbox to the gCrashReporter submitReports preference.
              submitChk.checked = submitReports;

              this.addLinkClickCallback(submitChk, function() {
                cr.submitReports = submitChk.checked;
              });

              status = "please";
            }

            // If we're showing the checkbox to trigger report submission, we'll want
            // to be able to update all the instances of the UI for this crash when
            // one instance of the checkbox is modified or the status is updated.
            if (doPrompt) {
              let observer = {
                QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver,
                                                       Components.interfaces.nsISupportsWeakReference]),
                observe: function(subject, topic, data) {
                  let propertyBag = subject;
                  if (!(propertyBag instanceof Components.interfaces.nsIPropertyBag2))
                    return;
                  // Ignore notifications for other crashes.
                  if (propertyBag.get("minidumpID") != pluginDumpID)
                    return;
                  statusDiv.setAttribute("status", data);
                },

                handleEvent : function(event) {
                  // Not expected to be called, just here for the closure.
                }
              };

              let submitReportsPrefObserver = {
                QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsIObserver,
                                                       Components.interfaces.nsISupportsWeakReference]),
                observe: function(subject, topic, data) {
                  submitChk.checked = cr.submitReports;
                },

                handleEvent : function(event) {
                  // Not expected to be called, just here for the closure.
                }
              };

              // Use a weak reference, so we don't have to remove it...
              var os = Components.classes["@mozilla.org/observer-service;1"]
                                 .getService(Components.interfaces.nsIObserverService);
              os.addObserver(observer, "crash-report-status", true);
              os.addObserver(submitReportsPrefObserver, "submit-reports-pref-changed", true);
              // ...alas, now we need something to hold a strong reference to prevent
              // it from being GC. But I don't want to manually manage the reference's
              // lifetime (which should be no greater than the page).
              // Clever solution? Use a closure with an event listener on the statusDiv.
              // When it goes away, so do the listener references and the closure.
              statusDiv.addEventListener("mozCleverClosureHack", observer, false);
              submitChk.addEventListener("mozCleverClosureHack", submitReportsPrefObserver, false);
            }
          }

          // If we don't have a minidumpID, we can't (or didn't) submit anything.
          // This can happen if the plugin is killed from the task manager.
          if (!pluginDumpID) {
            status = "noReport";
          }

          statusDiv.setAttribute("status", status);

          var bottomLinks = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgBottomLinks");
          bottomLinks.style.display = "block";
          var helpIcon = doc.getAnonymousElementByAttribute(plugin, "class", "helpIcon");
          this.addLinkClickCallback(helpIcon, this.openHelpPage);

          var messageString = this._stringBundle.formatStringFromName("crashedpluginsMessage.title", [pluginName], 1);
          var crashText = doc.getAnonymousElementByAttribute(plugin, "class", "msg msgCrashed");
          crashText.textContent = messageString;

          var link = doc.getAnonymousElementByAttribute(plugin, "class", "reloadLink");
          if (this.CrashSubmit)
            this.addLinkClickCallback(link, this.retryPluginPage, status, submitChk, pluginDumpID, browserDumpID);
          else
            this.addLinkClickCallback(link, this.reloadPage);

          // Is the <object>'s size too small to hold what we want to show?
          if (this.isTooSmall(plugin, overlay)) {
            // Hide the overlay's contents. Use visibility style, so that it
            // doesn't collapse down to 0x0.
            overlay.style.visibility = "hidden";
            // If another plugin on the page was large enough to show our UI, we
            // don't want to show a notification bar.
            if (!this.crashNotified)
              this.showPluginCrashedNotification(pluginDumpID, browserDumpID, messageString);
          }
          else {
            // If a previous plugin on the page was too small and resulted in
            // adding a notification bar, then remove it because this plugin
            // instance it big enough to serve as in-content notification.
            var notification = this.getNotificationWithValue("plugin-crashed");
            if (notification)
              this.removeNotification(notification, true);
            this.crashNotified = true;
          }
        ]]>
      </handler>

      <handler event="PluginOutdated">
        <![CDATA[
          var message = this._stringBundle.GetStringFromName("outdatedpluginsMessage.title");
          var buttons = [{
            label: this._stringBundle.GetStringFromName("outdatedpluginsMessage.button.label"),
            accessKey: this._stringBundle.GetStringFromName("outdatedpluginsMessage.button.accesskey"),
            popup: null,
            callback: this.openURLPref.bind(this, "plugins.update.url")
          }];

          this.pluginUnavailable(event, "outdated-plugins", message, buttons,
                                 "plugins.hide_infobar_for_outdated_plugin");
        ]]>
      </handler>

      <handler event="npapi-carbon-event-model-failure">
        <![CDATA[
          function callback() {
            // Notify all windows that an application quit has been requested.
            var cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
                                       .createInstance(Components.interfaces.nsISupportsPRBool);
            var os = Components.classes["@mozilla.org/observer-service;1"]
                               .getService(Components.interfaces.nsIObserverService);
            os.notifyObservers(cancelQuit, "quit-application-requested", null);

            // Something aborted the quit process.
            if (cancelQuit.data)
              return;

            var nsIAppStartup = Components.interfaces.nsIAppStartup;
            Components.classes["@mozilla.org/toolkit/app-startup;1"]
                      .getService(nsIAppStartup)
                      .quit(nsIAppStartup.eAttemptQuit |
                            nsIAppStartup.eRestart |
                            nsIAppStartup.eRestarti386);
          }

          var notification = "carbon-failure-plugins";
          var pref = "plugins.hide_infobar_for_carbon_failure_plugin";
          if ("@mozilla.org/xpcom/mac-utils;1" in Components.classes &&
              !Components.classes["@mozilla.org/xpcom/mac-utils;1"]
                         .getService(Components.interfaces.nsIMacUtils)
                         .isUniversalBinary) {
            pref = null;
            notification = "missing-plugins";
            callback = this.openURLPref.bind(this, "plugins.update.url");
          }

          var prefix = notification.replace(/-/g, "");
          var message = this._stringBundle.GetStringFromName(prefix + "Message.title");
          var buttons = [{
            label: this._stringBundle.GetStringFromName(prefix + "Message.button.label"),
            accessKey: this._stringBundle.GetStringFromName(prefix + "Message.button.accesskey"),
            popup: null,
            callback: callback
          }];

          this.pluginUnavailable(event, notification, message, buttons, pref);
        ]]>
      </handler>

      <handler event="PluginNotFound">
        <![CDATA[
          var plugin  = event.target;
          // For broken non-object plugin tags, register a click handler so
          // that the user can click the plugin replacement to get the new
          // plugin. Object tags can, and often do, deal with that themselves,
          // so don't stomp on the page developers toes.
          if (plugin instanceof Components.interfaces.nsIObjectLoadingContent &&
              !(plugin instanceof HTMLObjectElement)) {
            // Force a style flush, so that we ensure our binding is attached.
            plugin.clientTop;
            // We don't yet check to see if there's actually an installer available.
            var doc = plugin.ownerDocument;
            var installStatus = doc.getAnonymousElementByAttribute(plugin, "class", "installStatus");
            installStatus.setAttribute("status", "ready");
            var iconStatus = doc.getAnonymousElementByAttribute(plugin, "class", "icon");
            iconStatus.setAttribute("status", "ready");
            var installLink = doc.getAnonymousElementByAttribute(plugin, "class", "installPluginLink");
            this.addLinkClickCallback(installLink, installMissingPlugins);
          }

          var messageString = this._stringBundle.GetStringFromName("missingpluginsMessage.title");
          var buttons = [{
            label: this._stringBundle.GetStringFromName("missingpluginsMessage.button.label"),
            accessKey: this._stringBundle.GetStringFromName("missingpluginsMessage.button.accesskey"),
            popup: null,
            callback: this.installMissingPlugins.bind(this)
          }];

          this.pluginUnavailable(event, "missing-plugins", messageString, buttons);
        ]]>
      </handler>

      <handler event="PluginBlocklisted">
        <![CDATA[
          var messageString = this._stringBundle.GetStringFromName("blockedpluginsMessage.title");
          var buttons = [{
            label: this._stringBundle.GetStringFromName("blockedpluginsMessage.infoButton.label"),
            accessKey: this._stringBundle.GetStringFromName("blockedpluginsMessage.infoButton.accesskey"),
            popup: null,
            callback: this.openURLPref.bind(this, "extensions.blocklist.detailsURL")
          }, {
            label: this._stringBundle.GetStringFromName("blockedpluginsMessage.searchButton.label"),
            accessKey: this._stringBundle.GetStringFromName("blockedpluginsMessage.searchButton.accesskey"),
            popup: null,
            callback: this.installMissingPlugins.bind(this)
          }];

          this.pluginUnavailable(event, "blocked-plugins", messageString, buttons);
        ]]>
      </handler>

      <handler event="PluginDisabled">
        <![CDATA[
          var plugin  = event.target;
          // Since we are expecting also untrusted events, make sure
          // that the target is a plugin
          if (plugin instanceof Components.interfaces.nsIObjectLoadingContent &&
              "toEM" in window) {
            // Force a style flush, so that we ensure our binding is attached.
            plugin.clientTop;
            var doc = plugin.ownerDocument;
            var manageLink = doc.getAnonymousElementByAttribute(plugin, "class", "managePluginsLink");
            this.addLinkClickCallback(manageLink, window.toEM, "addons://list/plugin");
          }
        ]]>
      </handler>

      <handler event="NewPluginInstalled">
        <![CDATA[
          this.missingPlugins = {};

          // clean up the UI after a new plugin has been installed.
          var notification = this.getNotificationWithValue("missing-plugins");
          if (notification)
            this.removeNotification(notification);

          // reload the browser to make the new plugin show.
          this.activeBrowser.reload();
        ]]>
      </handler>

      <handler event="MozApplicationManifest">
        <![CDATA[
          if (!this._prefs.getBoolPref("browser.offline-apps.notify"))
            return;

          try {
            if (this._prefs.getBoolPref("offline-apps.allow_by_default"))
              return;
          } catch (e) {
          }

          this.offlineAppRequested(event.originalTarget);
        ]]>
      </handler>
    </handlers>
  </binding>

  <binding id="popup-notification"
           extends="chrome://communicator/content/bindings/notification.xml#browser-notificationbox">
    <implementation>
      <method name="removeNotifications">
        <parameter name="aNotifications"/>
        <body>
          <![CDATA[
            aNotifications.forEach(function(value) {
              var notification = PopupNotifications.getNotification(value);
              if (notification)
                PopupNotifications.remove(notification);
            });
          ]]>
        </body>
      </method>

      <method name="lwthemeInstallRequest">
        <parameter name="aHost"/>
        <parameter name="aCallback"/>
        <body>
          <![CDATA[
            var message = this._stringBundle.formatStringFromName("lwthemeInstallRequest.message", [aHost], 1);
            var action = {
              label: this._stringBundle.GetStringFromName("lwthemeInstallRequest.allowButton"),
              accessKey: this._stringBundle.GetStringFromName("lwthemeInstallRequest.allowButton.accesskey"),
              callback: aCallback
            };
            PopupNotifications.show(this.activeBrowser,
                                    "lwtheme-install-request", message,
                                    "addons-notification-icon", action);
          ]]>
        </body>
      </method>

      <method name="lwthemeInstallNotification">
        <parameter name="aCallback"/>
        <body>
          <![CDATA[
            var message = this._stringBundle.GetStringFromName("lwthemeInstallNotification.message");
            var mainAction = {
              label: this._stringBundle.GetStringFromName("lwthemeInstallNotification.undoButton"),
              accessKey: this._stringBundle.GetStringFromName("lwthemeInstallNotification.undoButton.accesskey"),
              callback: aCallback
            };
            var secondaryActions = [{
              label: this._stringBundle.GetStringFromName("lwthemeInstallNotification.manageButton"),
              accessKey: this._stringBundle.GetStringFromName("lwthemeInstallNotification.manageButton.accesskey"),
              callback: function() {
                window.toEM("addons://list/theme");
              }
            }];
            var options = {
              timeout: Date.now() + 20000 // 20 seconds
            };
            PopupNotifications.show(this.activeBrowser,
                                    "lwtheme-install-notification", message,
                                    "addons-notification-icon", mainAction,
                                    secondaryActions, options);
          ]]>
        </body>
      </method>

      <method name="lwthemeNeedsRestart">
        <parameter name="aNewThemeName"/>
        <body>
          <![CDATA[
            var message = this._stringBundle.formatStringFromName("lwthemeNeedsRestart.message", [aNewThemeName], 1);
            var action = {
              label: this._stringBundle.GetStringFromName("lwthemeNeedsRestart.restartButton"),
              accessKey: this._stringBundle.GetStringFromName("lwthemeNeedsRestart.restartButton.accesskey"),
              callback: function() {
                Application.restart();
              }
            };
            var options = {
              timeout: Date.now() + 20000 // 20 seconds
            };
            PopupNotifications.show(this.activeBrowser,
                                    "lwtheme-install-notification", message,
                                    "addons-notification-icon", action, null,
                                    options);
          ]]>
        </body>
      </method>

      <method name="showGeolocationPrompt">
        <parameter name="file"/>
        <parameter name="site"/>
        <parameter name="image"/>
        <parameter name="allowCallback"/>
        <parameter name="cancelCallback"/>
        <body>
          <![CDATA[
            var type = "geolocation";
            var mainAction = {
              label: this._stringBundle.GetStringFromName(type + ".shareLocation"),
              accessKey: this._stringBundle.GetStringFromName(type + ".shareLocation.accesskey"),
              callback: function(aNotification) {
                allowCallback(false);
              }
            };

            var secondaryActions = [{
              label: this._stringBundle.GetStringFromName(type + ".dontShareLocation"),
              accessKey: this._stringBundle.GetStringFromName(type + ".dontShareLocation.accesskey"),
              callback: function (aNotification) {
                cancelCallback(false);
              }
            }, {
              label: this._stringBundle.GetStringFromName(type + ".neverShareLocation"),
              accessKey: this._stringBundle.GetStringFromName(type + ".neverShareLocation.accesskey"),
              callback: function (aNotification) {
                cancelCallback(true);
              }
            }, {
              label: this._stringBundle.GetStringFromName(type + ".alwaysShareLocation"),
              accessKey: this._stringBundle.GetStringFromName(type + ".alwaysShareLocation.accesskey"),
              callback: function (aNotification) {
                allowCallback(true);
              }
            }];

            var message;
            if (site) {
              message = this._stringBundle
                            .formatStringFromName(type + ".siteWantsToKnow",
                                                  [site], 1);
            } else {
              message = this._stringBundle
                            .formatStringFromName(type + ".fileWantsToKnow",
                                                  [file], 1);
              secondaryActions.length = 1;
            }

            var options = {
              value: this._stringBundle.GetStringFromName(type + ".learnMore"),
              href: this._urlFormatter.formatURLPref("browser." + type + ".warning.infoURL")
            };

            PopupNotifications.show(this.activeBrowser,
                                    "geolocation", message,
                                    "geo-notification-icon", mainAction,
                                    secondaryActions, options);
          ]]>
        </body>
      </method>

      <method name="promptIndexedDB">
        <parameter name="aRequestor"/>
        <parameter name="aWindow"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body>
          <![CDATA[
            var host = aWindow.document.documentURIObject.asciiHost;
            var message = this._stringBundle.formatStringFromName("offlineApps." + aTopic, [host, aData], 2);
            var observer = aRequestor.getInterface(Components.interfaces.nsIObserver);
            var nsIPermissionManager = Components.interfaces.nsIPermissionManager;
            var mainAction = {
              label: this._stringBundle.GetStringFromName("offlineApps.allow"),
              accessKey: this._stringBundle.GetStringFromName("offlineApps.allow.accesskey"),
              callback: function allowIndexedDB() {
                clearTimeout(notification.timeout);
                observer.observe(null, "indexedDB-" + aTopic + "-response",
                                 nsIPermissionManager.ALLOW_ACTION);
              }
            };
            var secondaryActions = [{
              label: this._stringBundle.GetStringFromName("offlineApps.deny"),
              accessKey: this._stringBundle.GetStringFromName("offlineApps.deny.accesskey"),
              callback: function denyIndexedDB() {
                clearTimeout(notification.timeout);
                observer.observe(null, "indexedDB-" + aTopic + "-response",
                                 nsIPermissionManager.DENY_ACTION);
              }
            }];
            function notificationTimedOut() {
              observer.observe(null, "indexedDB-" + aTopic + "-response",
                               nsIPermissionManager.UNKNOWN_ACTION);
              notification.remove();
            }
            var options = {
              eventCallback: function(state) {
                if (notification) {
                  // Always clear the timeout up front. If the doorhanger was
                  // temporarily dismissed, we'll set a new 30 second timeout
                  // to automatically cancel the request. If the doorhanger
                  // gets redisplayed we don't want it to time out unless it
                  // gets dismissed again. And if the doorhanger gets removed
                  // then we aren't interested in it any more.
                  clearTimeout(timeout);
                  if (state == "dismissed")
                    timeout = setTimeout(notificationTimedOut, 30000);
                }
              }
            };
            var notification =
                PopupNotifications.show(this.activeBrowser,
                                        "indexedDB-" + aTopic + "-prompt",
                                        message, "indexedDB-notification-icon",
                                        mainAction, secondaryActions, options);
            var timeout = setTimeout(notificationTimedOut, 300000); // 5 minutes
          ]]>
        </body>
      </method>

      <method name="cancelIndexedDB">
        <parameter name="aRequestor"/>
        <parameter name="aWindow"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body>
          <![CDATA[
            var popupNotification = PopupNotifications.getNotification("indexedDB-" + aTopic + "-prompt", this.activeBrowser);
            if (popupNotification)
              popupNotification.remove(); // eventCallback clears the timeout

            var observer = aRequestor.getInterface(Components.interfaces.nsIObserver);
            observer.observe(null, "indexedDB-" + aTopic + "-response",
                             nsIPermissionManager.UNKNOWN_ACTION);
          ]]>
        </body>
      </method>

      <method name="addonInstallBlocked">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var host;
            try {
              // this fails with nsSimpleURIs like data: URIs
              host = installInfo.originatingURI.host;
            } catch (ex) {
              host = this._stringBundle.GetStringFromName("xpinstallHostNotAvailable");
            }
            var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
            var messageString = this._stringBundle.formatStringFromName("xpinstallPromptWarning",
                                                                        [brandShortName, host], 2);
            var action = {
              label: this._stringBundle.GetStringFromName("xpinstallPromptInstallButton"),
              accessKey: this._stringBundle.GetStringFromName("xpinstallPromptInstallButton.accesskey"),
              callback: function allowInstall() {
                installInfo.install();
                return false;
              }
            };

            // Make notifications persist a minimum of 30 seconds
            var options = {
              timeout: Date.now() + 30000
            };
            PopupNotifications.show(this.activeBrowser,
                                    "addon-install-blocked", messageString,
                                    "addons-notification-icon", action,
                                    null, options);
          ]]>
        </body>
      </method>

      <method name="addonInstallCancelled">
        <parameter name="installInfo"/>
        <parameter name="sourceURI"/>
        <body>
          <![CDATA[
            var tmp = {};
            Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);
            var messageString = this._stringBundle.GetStringFromName("addonDownloadCancelled");
            messageString = tmp.PluralForm.get(installInfo.installs.length, messageString);
            var action = {
              label: this._stringBundle.GetStringFromName("addonDownloadRestartButton"),
              accessKey: this._stringBundle.GetStringFromName("addonDownloadRestartButton.accesskey"),
              popup: null,
              callback: function() {
                var weblistener = Components.classes["@mozilla.org/addons/web-install-listener;1"]
                                            .getService(Components.interfaces.amIWebInstallListener);
                if (weblistener.onWebInstallRequested(installInfo.originatingWindow, sourceURI,
                                                      installInfo.installs, installInfo.installs.length)) {
                  installs.forEach(function(aInstall) {
                    aInstall.install();
                  });
                }
              }
            };
            PopupNotifications.show(this.activeBrowser,
                                    "addon-install-cancelled", messageString,
                                    "addons-notification-icon", action);

            installInfo.installs.every(function(aInstall) {
              aInstall.cancel();
            });
          ]]>
        </body>
      </method>

      <method name="addonInstallComplete">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var tmp = {};
            Components.utils.import("resource://gre/modules/AddonManager.jsm", tmp);
            Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);

            var messageString;
            var mainAction = null;
            var secondaryActions = null;

            if ("toEM" in window) {
              mainAction = {
                label: this._stringBundle.GetStringFromName("addonInstallManageButton"),
                accessKey: this._stringBundle.GetStringFromName("addonInstallManageButton.accesskey"),
                callback: function() {
                  window.toEM("addons://list/extension");
                }
              };
            }

            if (installInfo.installs.some(function(install)
                  install.addon.pendingOperations &
                  tmp.AddonManager.PENDING_INSTALL)) {
              messageString = this._stringBundle.GetStringFromName("addonsInstalledNeedsRestart");
              if (mainAction)
                secondaryActions = [mainAction];
              mainAction = {
                label: this._stringBundle.GetStringFromName("addonInstallRestartButton"),
                accessKey: this._stringBundle.GetStringFromName("addonInstallRestartButton.accesskey"),
                callback: function () {
                  Application.restart();
                }
              };
            } else {
              messageString = this._stringBundle.GetStringFromName("addonsInstalled");
            }

            var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
            messageString = tmp.PluralForm.get(installInfo.installs.length, messageString)
                                          .replace("#1", installInfo.installs[0].name)
                                          .replace("#2", installInfo.installs.length)
                                          .replace("#3", brandShortName);

            // Make notifications persist a minimum of 30 seconds
            var options = {
              timeout: Date.now() + 30000
            };
            PopupNotifications.show(this.activeBrowser,
                                    "addon-install-complete", messageString,
                                    "addons-notification-icon", mainAction,
                                    secondaryActions, options);
          ]]>
        </body>
      </method>

      <method name="addonInstallDisabled">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var messageString;
            var action = null;
            var prefBranch = this._prefs; // used by editPrefs callback

            if (prefBranch.prefIsLocked("xpinstall.enabled"))
              messageString = this._stringBundle.GetStringFromName("xpinstallDisabledMessageLocked");
            else {
              messageString = this._stringBundle.GetStringFromName("xpinstallDisabledMessage");
              action = {
                label: this._stringBundle.GetStringFromName("xpinstallDisabledButton"),
                accessKey: this._stringBundle.GetStringFromName("xpinstallDisabledButton.accesskey"),
                callback: function editPrefs() {
                  prefBranch.setBoolPref("xpinstall.enabled", true);
                }
              };
            }

            // Make notifications persist a minimum of 30 seconds
            var options = {
              timeout: Date.now() + 30000
            };
            PopupNotifications.show(this.activeBrowser,
                                    "addon-install-disabled", messageString,
                                    "addons-notification-icon", action,
                                    null, options);
          ]]>
        </body>
      </method>

      <method name="addonInstallFailed">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var host;
            try {
              // this fails with nsSimpleURIs like data: URIs
              host = installInfo.originatingURI.host;
            } catch (ex) {
              host = this._stringBundle.GetStringFromName("xpinstallHostNotAvailable");
            }

            var error = "addonErrorIncompatible";
            var name = installInfo.installs[0].name;
            installInfo.installs.some(function(install) {
              if (install.error) {
                name = install.name;
                error = "addonError" + install.error;
                return true;
              }
              if (install.addon.blocklistState ==
                  Components.interfaces.nsIBlocklistService.STATE_BLOCKED) {
                name = install.name;
                error = "addonErrorBlocklisted";
              }
              return false;
            });

            var brandShortName = this._brandStringBundle.GetStringFromName("brandShortName");
            var version = Components.classes["@mozilla.org/xre/app-info;1"]
                                    .getService(Components.interfaces.nsIXULAppInfo).version;
            var messageString = this._stringBundle.GetStringFromName(error)
                                                  .replace("#1", name)
                                                  .replace("#2", host)
                                                  .replace("#3", brandShortName)
                                                  .replace("#4", version);

            // Make notifications persist a minimum of 30 seconds
            var options = {
              timeout: Date.now() + 30000
            };
            PopupNotifications.show(this.activeBrowser,
                                    "addon-install-failed", messageString,
                                    "addons-notification-icon", null,
                                    null, options);
          ]]>
        </body>
      </method>

      <method name="addonInstallStarted">
        <parameter name="installInfo"/>
        <body>
          <![CDATA[
            var tmp = {};
            Components.utils.import("resource://gre/modules/AddonManager.jsm", tmp);
            if (installInfo.installs.every(function(aInstall) {
                  return aInstall.state == tmp.AddonManager.STATE_DOWNLOADED;
                }))
              return;

            var sourceURI = installInfo.originatingWindow.document.documentURIObject;
            Components.utils.import("resource://gre/modules/PluralForm.jsm", tmp);
            var messageString = this._stringBundle.GetStringFromName("addonDownloading");
            messageString = tmp.PluralForm.get(installInfo.installs.length, messageString);
            var action = {
              label: this._stringBundle.GetStringFromName("addonDownloadCancelButton"),
              accessKey: this._stringBundle.GetStringFromName("addonDownloadCancelButton.accesskey"),
              callback: this.addonInstallCancelled.bind(this, installInfo, sourceURI)
            };
            var options = {
              installInfo: installInfo
            };
            PopupNotifications.show(this.activeBrowser,
                                    "addon-install-started", messageString,
                                    "addons-notification-icon", action,
                                    null, options);
          ]]>
        </body>
      </method>

    </implementation>
  </binding>

  <binding id="addon-progress-notification"
           extends="chrome://global/content/bindings/notification.xml#notification">
    <content>
      <xul:hbox class="notification-inner outset" flex="1" xbl:inherits="type">
        <xul:hbox anonid="details" align="center" flex="1"
                  oncommand="this.parentNode.parentNode._doButtonCommand(event);">
          <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image"/>
          <xul:description anonid="messageText" class="messageText" xbl:inherits="xbl:text=label"/>
          <xul:progressmeter mode="undetermined" xbl:inherits="mode,value=progress"/>
          <xul:label flex="1" xbl:inherits="value=status"/>
          <children/>
        </xul:hbox>
        <xul:toolbarbutton ondblclick="event.stopPropagation();"
                           class="messageCloseButton tabbable"
                           xbl:inherits="hidden=hideclose"
                           tooltiptext="&closeNotification.tooltip;"
                           oncommand="document.getBindingParent(this).close();"/>
      </xul:hbox>
    </content>

    <implementation>
      <destructor>
        <![CDATA[
          this.installInfo.installs.forEach(function(aInstall) {
            aInstall.removeListener(this);
          }, this);
        ]]>
      </destructor>

      <method name="updateProgress">
        <body>
          <![CDATA[
            var count = 0;
            var progress = 0;
            var max = 0;

            var tmp = {};
            Components.utils.import("resource://gre/modules/AddonManager.jsm", tmp);
            this.installInfo.installs.forEach(function(aInstall) {
              if (aInstall.maxProgress < 0)
                max = -1;
              else if (max >= 0)
                max += aInstall.maxProgress;
              progress += aInstall.progress;
              if (aInstall.state < tmp.AddonManager.STATE_DOWNLOADED)
                count++;
            });

            if (max < 0)
              this.setAttribute("mode", "undetermined");
            else {
              this.setAttribute("mode", "determined");
              this.setAttribute("progress", progress * 100 / max);
            }

            var now = Date.now();
            if (!this.startTime) {
              this.startTime = now;
              this.lastUpdate = now - 750;
              this.lastSeconds = null;
            }

            if (progress == max || now - this.lastUpdate >= 750) {
              this.lastUpdate = now;
              var elapsed = (now - this.startTime) / 1000;
              var rate = elapsed && progress / elapsed;
              Components.utils.import("resource://gre/modules/DownloadUtils.jsm", tmp);
              var status;
              [status, this.lastSeconds] = tmp.DownloadUtils.getDownloadStatus(progress, max, rate, this.lastSeconds);
              this.setAttribute("status", status);
            }

            if (!count)
              this.close();
          ]]>
        </body>
      </method>

      <method name="onDownloadProgress">
        <body>
          <![CDATA[
            this.updateProgress();
          ]]>
        </body>
      </method>

      <method name="onDownloadFailed">
        <body>
          <![CDATA[
            this.updateProgress();
          ]]>
        </body>
      </method>

      <method name="onDownloadCancelled">
        <body>
          <![CDATA[
            this.updateProgress();
          ]]>
        </body>
      </method>

      <method name="onDownloadEnded">
        <body>
          <![CDATA[
            this.updateProgress();
          ]]>
        </body>
      </method>
    </implementation>
  </binding>

  <binding id="sidebar-notification"
           extends="chrome://global/content/bindings/notification.xml#notification">
    <content>
      <xul:vbox class="notification-inner outset" flex="1" xbl:inherits="type">
        <xul:hbox align="center">
          <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image"/>
          <xul:arrowscrollbox orient="horizontal" flex="1" pack="end"
                              oncommand="document.getBindingParent(this)._doButtonCommand(event);">
            <children/>
          </xul:arrowscrollbox>
          <xul:toolbarbutton ondblclick="event.stopPropagation();"
                             class="messageCloseButton tabbable"
                             xbl:inherits="hidden=hideclose"
                             tooltiptext="&closeNotification.tooltip;"
                             oncommand="document.getBindingParent(this).close();"/>
        </xul:hbox>
        <xul:description anonid="messageText" class="messageText" flex="1" xbl:inherits="xbl:text=label"/>
      </xul:vbox>
    </content>
  </binding>

  <binding id="sidebar-addon-progress-notification"
           extends="chrome://communicator/content/bindings/notification.xml#addon-progress-notification">
    <content>
      <xul:vbox class="notification-inner outset" flex="1" xbl:inherits="type">
        <xul:hbox align="center">
          <xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image"/>
          <xul:arrowscrollbox orient="horizontal" flex="1" pack="end"
                              oncommand="document.getBindingParent(this)._doButtonCommand(event);">
            <children/>
          </xul:arrowscrollbox>
          <xul:toolbarbutton ondblclick="event.stopPropagation();"
                             class="messageCloseButton tabbable"
                             xbl:inherits="hidden=hideclose"
                             tooltiptext="&closeNotification.tooltip;"
                             oncommand="document.getBindingParent(this).close();"/>
        </xul:hbox>
        <xul:description anonid="messageText" class="messageText" flex="1" xbl:inherits="xbl:text=label"/>
        <xul:progressmeter mode="undetermined" xbl:inherits="mode,value=progress"/>
        <xul:label xbl:inherits="value=status"/>
      </xul:vbox>
    </content>
  </binding>

  <binding id="geolocation-popup-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
    <content align="start">
      <xul:image class="popup-notification-icon" xbl:inherits="popupid"/>
      <xul:vbox flex="1">
        <xul:description class="popup-notification-description"
                         xbl:inherits="xbl:text=label"/>
        <xul:hbox class="popup-notification-button-container" align="center">
          <xul:label class="text-link" xbl:inherits="value,href"/>
          <xul:spacer flex="1"/>
          <xul:button anonid="button"
                      class="popup-notification-menubutton"
                      type="menu-button"
                      xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey">
            <xul:menupopup anonid="menupopup"
                           xbl:inherits="oncommand=menucommand">
              <children/>
              <xul:menuitem class="menuitem-iconic popup-notification-closeitem"
                            label="&closeNotificationItem.label;"
                            xbl:inherits="oncommand=closeitemcommand"/>
            </xul:menupopup>
          </xul:button>
        </xul:hbox>
        <xul:label xbl:inherits="value=status"/>
      </xul:vbox>
      <xul:vbox pack="start">
        <xul:toolbarbutton anonid="closebutton"
                           class="messageCloseButton popup-notification-closebutton tabbable"
                           xbl:inherits="oncommand=closebuttoncommand"
                           tooltiptext="&closeNotification.tooltip;"/>
      </xul:vbox>
    </content>

    <implementation>
      <constructor>
        <![CDATA[
          this.setAttribute("value", this.notification.options.value);
          this.setAttribute("href", this.notification.options.href);
        ]]>
      </constructor>
    </implementation>
  </binding>

  <binding id="addon-progress-popup-notification" extends="chrome://global/content/bindings/notification.xml#popup-notification">
    <content align="start">
      <xul:image class="popup-notification-icon" xbl:inherits="popupid"/>
      <xul:vbox flex="1">
        <xul:description class="popup-notification-description addon-progress-description"
                         xbl:inherits="xbl:text=label"/>
        <xul:hbox class="popup-notification-button-container" align="center">
          <xul:progressmeter mode="undetermined"
                             xbl:inherits="mode,value=progress"/>
          <xul:spacer flex="1"/>
          <xul:button anonid="button"
                      class="popup-notification-menubutton"
                      type="menu-button"
                      xbl:inherits="oncommand=buttoncommand,label=buttonlabel,accesskey=buttonaccesskey">
            <xul:menupopup anonid="menupopup"
                           xbl:inherits="oncommand=menucommand">
              <children/>
              <xul:menuitem class="menuitem-iconic popup-notification-closeitem"
                            label="&closeNotificationItem.label;"
                            xbl:inherits="oncommand=closeitemcommand"/>
            </xul:menupopup>
          </xul:button>
        </xul:hbox>
        <xul:label xbl:inherits="xbl:text=status"/>
      </xul:vbox>
      <xul:vbox pack="start">
        <xul:toolbarbutton anonid="closebutton"
                           class="messageCloseButton popup-notification-closebutton tabbable"
                           xbl:inherits="oncommand=closebuttoncommand"
                           tooltiptext="&closeNotification.tooltip;"/>
      </xul:vbox>
    </content>

    <implementation>
      <constructor>
        <![CDATA[
          this.installInfo = this.notification.options.installInfo;
          this.installInfo.installs.forEach(function(aInstall) {
            aInstall.addListener(this);
          }, this);
        ]]>
      </constructor>

      <destructor>
        <![CDATA[
          this.installInfo.installs.forEach(function(aInstall) {
            aInstall.removeListener(this);
          }, this);
        ]]>
      </destructor>

      <method name="updateProgress">
        <body>
          <![CDATA[
            var count = 0;
            var progress = 0;
            var max = 0;

            var tmp = {};
            Components.utils.import("resource://gre/modules/AddonManager.jsm", tmp);
            this.installInfo.installs.forEach(function(aInstall) {
              if (aInstall.maxProgress < 0)
                max = -1;
              else if (max >= 0)
                max += aInstall.maxProgress;
              progress += aInstall.progress;
              if (aInstall.state < tmp.AddonManager.STATE_DOWNLOADED)
                count++;
            });

            if (max < 0)
              this.setAttribute("mode", "undetermined");
            else {
              this.setAttribute("mode", "determined");
              this.setAttribute("progress", progress * 100 / max);
            }

            var now = Date.now();
            if (!this.startTime) {
              this.startTime = now;
              this.lastUpdate = now - 750;
              this.lastSeconds = null;
            }

            if (progress == max || now - this.lastUpdate >= 750) {
              this.lastUpdate = now;
              var elapsed = (now - this.startTime) / 1000;
              var rate = elapsed && progress / elapsed;
              Components.utils.import("resource://gre/modules/DownloadUtils.jsm", tmp);
              var status;
              [status, this.lastSeconds] = tmp.DownloadUtils.getDownloadStatus(progress, max, rate, this.lastSeconds);
              this.setAttribute("status", status);
            }

            if (!count)
              PopupNotifications.remove(this.notification);
          ]]>
        </body>
      </method>

      <method name="onDownloadProgress">
        <body>
          <![CDATA[
            this.updateProgress();
          ]]>
        </body>
      </method>

      <method name="onDownloadFailed">
        <body>
          <![CDATA[
            this.updateProgress();
          ]]>
        </body>
      </method>

      <method name="onDownloadCancelled">
        <body>
          <![CDATA[
            this.updateProgress();
          ]]>
        </body>
      </method>

      <method name="onDownloadEnded">
        <body>
          <![CDATA[
            this.updateProgress();
          ]]>
        </body>
      </method>
    </implementation>
  </binding>
</bindings>
