Bug 538331 - On update perform action based upon the update metadata. r=dietrich, r=dtownsend, r=gavin, ui-r=beltzner
authorRobert Strong <robert.bugzilla@gmail.com>
Tue, 06 Apr 2010 19:49:23 -0700
changeset 40527 9996ac775114cd3dcef81c9095772f5cf0132c16
parent 40526 8e6ef255d9d6a9cf369af9a8aa60a2378441cb30
child 40528 8722c089c3e6ac924aab59828f01306927fbf286
push id12649
push userrstrong@mozilla.com
push dateWed, 07 Apr 2010 02:50:17 +0000
treeherdermozilla-central@e0321787e483 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdietrich, dtownsend, gavin, beltzner
bugs538331
milestone1.9.3a4pre
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
Bug 538331 - On update perform action based upon the update metadata. r=dietrich, r=dtownsend, r=gavin, ui-r=beltzner
browser/components/Makefile.in
browser/components/nsBrowserContentHandler.js
browser/components/nsBrowserGlue.js
browser/components/test/Makefile.in
browser/components/test/browser/Makefile.in
browser/components/test/browser/browser_bug538331.js
browser/locales/en-US/chrome/browser/browser.properties
--- a/browser/components/Makefile.in
+++ b/browser/components/Makefile.in
@@ -80,15 +80,19 @@ ifndef WINCE
 PARALLEL_DIRS += wintaskbar
 endif
 endif
 
 ifdef MOZ_SAFE_BROWSING
 PARALLEL_DIRS += safebrowsing
 endif
 
+ifdef ENABLE_TESTS
+DIRS += test
+endif
+
 DIRS += build
 
 ifdef MOZILLA_OFFICIAL
 DEFINES += -DOFFICIAL_BUILD=1
 endif
 
 include $(topsrcdir)/config/rules.mk
--- a/browser/components/nsBrowserContentHandler.js
+++ b/browser/components/nsBrowserContentHandler.js
@@ -15,16 +15,17 @@
 #
 # The Initial Developer of the Original Code is
 # Benjamin Smedberg <benjamin@smedbergs.us>
 #
 # Portions created by the Initial Developer are Copyright (C) 2004
 # the Initial Developer. All Rights Reserved.
 #
 # Contributor(s):
+#   Robert Strong <robert.bugzilla@gmail.com>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either the GNU General Public License Version 2 or later (the "GPL"), or
 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -144,16 +145,50 @@ function needHomepageOverride(prefb) {
     
     prefb.setCharPref("browser.startup.homepage_override.mstone", mstone);
     return (savedmstone ? OVERRIDE_NEW_MSTONE : OVERRIDE_NEW_PROFILE);
   }
 
   return OVERRIDE_NONE;
 }
 
+/**
+ * Gets the override page for the first run after the application has been
+ * updated.
+ * @param  defaultOverridePage
+ *         The default override page.
+ * @return The override page.
+ */
+function getPostUpdateOverridePage(defaultOverridePage) {
+  var um = Components.classes["@mozilla.org/updates/update-manager;1"]
+                     .getService(Components.interfaces.nsIUpdateManager);
+  try {
+    // If the updates.xml file is deleted then getUpdateAt will throw.
+    var update = um.getUpdateAt(0)
+                   .QueryInterface(Components.interfaces.nsIPropertyBag);
+  } catch (e) {
+    // This should never happen.
+    Components.utils.reportError("Unable to find update: " + e);
+    return defaultOverridePage;
+  }
+
+  let actions = update.getProperty("actions");
+  // When the update doesn't specify actions fallback to the original behavior
+  // of displaying the default override page.
+  if (!actions)
+    return defaultOverridePage;
+
+  // The existence of silent or the non-existence of showURL in the actions both
+  // mean that an override page should not be displayed.
+  if (actions.indexOf("silent") != -1 || actions.indexOf("showURL") == -1)
+    return "";
+
+  return update.getProperty("openURL") || defaultOverridePage;
+}
+
 // Copies a pref override file into the user's profile pref-override folder,
 // and then tells the pref service to reload its default prefs.
 function copyPrefOverride() {
   try {
     var fileLocator = Components.classes["@mozilla.org/file/directory_service;1"]
                                 .getService(Components.interfaces.nsIProperties);
     const NS_APP_EXISTING_PREF_OVERRIDE = "ExistingPrefOverride";
     var prefOverride = fileLocator.get(NS_APP_EXISTING_PREF_OVERRIDE,
@@ -531,18 +566,20 @@ var nsBrowserContentHandler = {
           copyPrefOverride();
 
           // Check whether we have a session to restore. If we do, we assume
           // that this is an "update" session.
           var ss = Components.classes["@mozilla.org/browser/sessionstartup;1"]
                              .getService(Components.interfaces.nsISessionStartup);
           haveUpdateSession = ss.doRestore();
           overridePage = formatter.formatURLPref("startup.homepage_override_url");
+          if (prefb.prefHasUserValue("app.update.postupdate"))
+            overridePage = getPostUpdateOverridePage(overridePage);
           break;
-    }
+      }
     } catch (ex) {}
 
     // formatURLPref might return "about:blank" if getting the pref fails
     if (overridePage == "about:blank")
       overridePage = "";
 
     var startPage = "";
     try {
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -21,16 +21,17 @@
 # Contributor(s):
 #   Giorgio Maone <g.maone@informaction.com>
 #   Seth Spitzer <sspitzer@mozilla.com>
 #   Asaf Romano <mano@mozilla.com>
 #   Marco Bonardo <mak77@bonardo.net>
 #   Dietrich Ayala <dietrich@mozilla.com>
 #   Ehsan Akhgari <ehsan.akhgari@gmail.com>
 #   Nils Maier <maierman@web.de>
+#   Robert Strong <robert.bugzilla@gmail.com>
 #
 # Alternatively, the contents of this file may be used under the terms of
 # either the GNU General Public License Version 2 or later (the "GPL"), or
 # the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 # in which case the provisions of the GPL or the LGPL are applicable instead
 # of those above. If you wish to allow use of your version of this file only
 # under the terms of either the GPL or the LGPL, and not to allow others to
 # use your version of this file under the terms of the MPL, indicate your
@@ -212,16 +213,23 @@ BrowserGlue.prototype = {
         break;
       case "bookmarks-restore-success":
       case "bookmarks-restore-failed":
         this._observerService.removeObserver(this, "bookmarks-restore-success");
         this._observerService.removeObserver(this, "bookmarks-restore-failed");
         if (topic == "bookmarks-restore-success" && data == "html-initial")
           this.ensurePlacesDefaultQueriesInitialized();
         break;
+      case "browser-glue-test": // used by tests
+        if (data == "post-update-notification") {
+          if (this._prefs.prefHasUserValue("app.update.postupdate"))
+            this._showUpdateNotification();
+          break;
+        }
+        break;
     }
   }, 
 
   // initialization (called on application startup) 
   _init: function BG__init() {
     // observer registration
     const osvr = this._observerService;
     osvr.addObserver(this, "xpcom-shutdown", false);
@@ -334,16 +342,20 @@ BrowserGlue.prototype = {
   },
 
   // Browser startup complete. All initial windows have opened.
   _onBrowserStartup: function BG__onBrowserStartup() {
     // Show about:rights notification, if needed.
     if (this._shouldShowRights())
       this._showRightsNotification();
 
+    // Show update notification, if needed.
+    if (this._prefs.prefHasUserValue("app.update.postupdate"))
+      this._showUpdateNotification();
+
     // If new add-ons were installed during startup open the add-ons manager.
     if (this._prefs.prefHasUserValue(PREF_EM_NEW_ADDONS_LIST)) {
       var args = Cc["@mozilla.org/supports-array;1"].
                  createInstance(Ci.nsISupportsArray);
       var str = Cc["@mozilla.org/supports-string;1"].
                 createInstance(Ci.nsISupportsString);
       str.data = "";
       args.AppendElement(str);
@@ -571,17 +583,133 @@ BrowserGlue.prototype = {
 
     // Set pref to indicate we've shown the notification.
     var currentVersion = this._prefs.getIntPref("browser.rights.version");
     this._prefs.setBoolPref("browser.rights." + currentVersion + ".shown", true);
 
     var box = notifyBox.appendNotification(notifyRightsText, "about-rights", null, notifyBox.PRIORITY_INFO_LOW, buttons);
     box.persistence = 3; // arbitrary number, just so bar sticks around for a bit
   },
-  
+
+  _showUpdateNotification: function BG__showUpdateNotification() {
+    this._prefs.clearUserPref("app.update.postupdate");
+
+    var um = Cc["@mozilla.org/updates/update-manager;1"].
+             getService(Ci.nsIUpdateManager);
+    try {
+      // If the updates.xml file is deleted then getUpdateAt will throw.
+      var update = um.getUpdateAt(0).QueryInterface(Ci.nsIPropertyBag);
+    }
+    catch (e) {
+      // This should never happen.
+      Cu.reportError("Unable to find update: " + e);
+      return;
+    }
+
+    var actions = update.getProperty("actions");
+    if (!actions || actions.indexOf("silent") != -1)
+      return;
+
+    var formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].
+                    getService(Ci.nsIURLFormatter);
+    var browserBundle = this._bundleService.
+                        createBundle("chrome://browser/locale/browser.properties");
+    var brandBundle = this._bundleService.
+                      createBundle("chrome://branding/locale/brand.properties");
+    var appName = brandBundle.GetStringFromName("brandShortName");
+
+    function getNotifyString(aPropData) {
+      var propValue = update.getProperty(aPropData.propName);
+      if (!propValue) {
+        if (aPropData.prefName)
+          propValue = formatter.formatURLPref(aPropData.prefName);
+        else if (aPropData.stringParams)
+          propValue = browserBundle.formatStringFromName(aPropData.stringName,
+                                                         aPropData.stringParams,
+                                                         aPropData.stringParams.length);
+        else
+          propValue = browserBundle.GetStringFromName(aPropData.stringName);
+      }
+      return propValue;
+    }
+
+    if (actions.indexOf("showNotification") != -1) {
+      let text = getNotifyString({propName: "notificationText",
+                                  stringName: "puNotifyText",
+                                  stringParams: [appName]});
+      let url = getNotifyString({propName: "notificationURL",
+                                 prefName: "startup.homepage_override_url"});
+      let label = getNotifyString({propName: "notificationButtonLabel",
+                                   stringName: "pu.notifyButton.label"});
+      let key = getNotifyString({propName: "notificationButtonAccessKey",
+                                 stringName: "pu.notifyButton.accesskey"});
+
+      let win = this.getMostRecentBrowserWindow();
+      let browser = win.gBrowser; // for closure in notification bar callback
+      let notifyBox = browser.getNotificationBox();
+
+      let buttons = [
+                      {
+                        label:     label,
+                        accessKey: key,
+                        popup:     null,
+                        callback: function(aNotificationBar, aButton) {
+                          browser.selectedTab = browser.addTab(url);
+                        }
+                      }
+                    ];
+
+      let box = notifyBox.appendNotification(text, "post-update-notification",
+                                             null, notifyBox.PRIORITY_INFO_LOW,
+                                             buttons);
+      box.persistence = 3;
+    }
+
+    if (actions.indexOf("showAlert") == -1)
+      return;
+
+    let notifier;
+    try {
+      notifier = Cc["@mozilla.org/alerts-service;1"].
+                 getService(Ci.nsIAlertsService);
+    }
+    catch (e) {
+      // nsIAlertsService is not available for this platform
+      return;
+    }
+
+    let title = getNotifyString({propName: "alertTitle",
+                                 stringName: "puAlertTitle",
+                                 stringParams: [appName]});
+    let text = getNotifyString({propName: "alertText",
+                                stringName: "puAlertText",
+                                stringParams: [appName]});
+    let url = getNotifyString({propName: "alertURL",
+                               prefName: "startup.homepage_override_url"});
+
+    var self = this;
+    function clickCallback(subject, topic, data) {
+      // This callback will be called twice but only once with this topic
+      if (topic != "alertclickcallback")
+        return;
+      let win = self.getMostRecentBrowserWindow();
+      let browser = win.gBrowser;
+      browser.selectedTab = browser.addTab(data);
+    }
+
+    try {
+      // This will throw NS_ERROR_NOT_AVAILABLE if the notification cannot
+      // be displayed per the idl.
+      notifier.showAlertNotification("post-update-notification", title, text,
+                                     true, url, clickCallback);
+    }
+    catch (e) {
+    }
+  },
+
   _showPluginUpdatePage: function BG__showPluginUpdatePage() {
     this._prefs.setBoolPref(PREF_PLUGINS_NOTIFYUSER, false);
 
     var formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].
                     getService(Ci.nsIURLFormatter);
     var updateUrl = formatter.formatURLPref(PREF_PLUGINS_UPDATEURL);
 
     var win = this.getMostRecentBrowserWindow();
new file mode 100644
--- /dev/null
+++ b/browser/components/test/Makefile.in
@@ -0,0 +1,48 @@
+#
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Robert Strong <robert.bugzilla@gmail.com> (Original Author)
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH     = ../../..
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+DIRS = browser
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/browser/components/test/browser/Makefile.in
@@ -0,0 +1,52 @@
+# ***** BEGIN LICENSE BLOCK *****
+# Version: MPL 1.1/GPL 2.0/LGPL 2.1
+#
+# The contents of this file are subject to the Mozilla Public License Version
+# 1.1 (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+# http://www.mozilla.org/MPL/
+#
+# Software distributed under the License is distributed on an "AS IS" basis,
+# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+# for the specific language governing rights and limitations under the
+# License.
+#
+# The Original Code is mozilla.org code.
+#
+# The Initial Developer of the Original Code is
+# the Mozilla Foundation.
+# Portions created by the Initial Developer are Copyright (C) 2010
+# the Initial Developer. All Rights Reserved.
+#
+# Contributor(s):
+#   Robert Strong <robert.bugzilla@gmail.com> (Original Author)
+#
+# Alternatively, the contents of this file may be used under the terms of
+# either of the GNU General Public License Version 2 or later (the "GPL"),
+# or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+# in which case the provisions of the GPL or the LGPL are applicable instead
+# of those above. If you wish to allow use of your version of this file only
+# under the terms of either the GPL or the LGPL, and not to allow others to
+# use your version of this file under the terms of the MPL, indicate your
+# decision by deleting the provisions above and replace them with the notice
+# and other provisions required by the GPL or the LGPL. If you do not delete
+# the provisions above, a recipient may use your version of this file under
+# the terms of any one of the MPL, the GPL or the LGPL.
+#
+# ***** END LICENSE BLOCK *****
+
+DEPTH     = ../../../..
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+relativesrcdir  = browser/components/test/browser
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_BROWSER_TEST_FILES = \
+  browser_bug538331.js \
+  $(NULL)
+
+libs:: $(_BROWSER_TEST_FILES)
+	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/components/test/browser/browser_bug538331.js
@@ -0,0 +1,373 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const PREF_POSTUPDATE = "app.update.postupdate";
+const PREF_MSTONE = "browser.startup.homepage_override.mstone";
+const PREF_OVERRIDE_URL = "startup.homepage_override_url";
+
+const DEFAULT_PREF_URL = "http://pref.example.com/";
+const DEFAULT_UPDATE_URL = "http://example.com/";
+
+const XML_EMPTY = "<?xml version=\"1.0\"?><updates xmlns=" +
+                  "\"http://www.mozilla.org/2005/app-update\"></updates>";
+
+const XML_PREFIX =  "<updates xmlns=\"http://www.mozilla.org/2005/app-update\"" +
+                    "><update appVersion=\"1.0\" buildID=\"20080811053724\" " +
+                    "channel=\"nightly\" displayVersion=\"Version 1.0\" " +
+                    "extensionVersion=\"1.0\" installDate=\"1238441400314\" " +
+                    "isCompleteUpdate=\"true\" name=\"Update Test 1.0\" " +
+                    "serviceURL=\"https://example.com/\" showNeverForVersion=" +
+                    "\"false\" showPrompt=\"false\" showSurvey=\"false\" type=" +
+                    "\"minor\" version=\"version 1.0\" detailsURL=" +
+                    "\"http://example.com/\" previousAppVersion=\"1.0\" " +
+                    "statusText=\"The Update was successfully installed\" " +
+                    "foregroundDownload=\"true\"";
+
+const XML_SUFFIX = "><patch type=\"complete\" URL=\"http://example.com/\" " +
+                   "hashFunction=\"MD5\" hashValue=" +
+                   "\"6232cd43a1c77e30191c53a329a3f99d\" size=\"775\" " +
+                   "selected=\"true\" state=\"succeeded\"/></update></updates>";
+
+// nsBrowserContentHandler.js defaultArgs tests
+const BCH_TESTS = [
+  {
+    description: "no mstone change and no update",
+    noPostUpdatePref: true,
+    noMstoneChange: true
+  }, {
+    description: "mstone changed and no update",
+    noPostUpdatePref: true,
+    prefURL: DEFAULT_PREF_URL
+  }, {
+    description: "no mstone change and update with 'showURL' for actions",
+    actions: "showURL",
+    noMstoneChange: true
+  }, {
+    description: "update without actions",
+    prefURL: DEFAULT_PREF_URL
+  }, {
+    description: "update with 'showURL' for actions",
+    actions: "showURL",
+    prefURL: DEFAULT_PREF_URL
+  }, {
+    description: "update with 'showURL' for actions and openURL",
+    actions: "showURL",
+    openURL: DEFAULT_UPDATE_URL
+    }, {
+    description: "update with 'showURL showAlert' for actions",
+    actions: "showAlert showURL",
+    prefURL: DEFAULT_PREF_URL
+  }, {
+    description: "update with 'showAlert showURL' for actions and openURL",
+    actions: "showAlert showURL",
+    openURL: DEFAULT_UPDATE_URL
+  }, {
+    description: "update with 'showURL showNotification' for actions",
+    actions: "showURL showNotification",
+    prefURL: DEFAULT_PREF_URL
+  }, {
+    description: "update with 'showNotification showURL' for actions and " +
+                 "openURL",
+    actions: "showNotification showURL",
+    openURL: DEFAULT_UPDATE_URL
+  }, {
+    description: "update with 'showAlert showURL showNotification' for actions",
+    actions: "showAlert showURL showNotification",
+    prefURL: DEFAULT_PREF_URL
+  }, {
+    description: "update with 'showNotification showURL showAlert' for " +
+                 "actions and openURL",
+    actions: "showNotification showURL showAlert",
+    openURL: DEFAULT_UPDATE_URL
+  }, {
+    description: "update with 'showAlert' for actions",
+    actions: "showAlert"
+  }, {
+    description: "update with 'showAlert showNotification' for actions",
+    actions: "showAlert showNotification"
+  }, {
+    description: "update with 'showNotification' for actions",
+    actions: "showNotification"
+  }, {
+    description: "update with 'showNotification showAlert' for actions",
+    actions: "showNotification showAlert"
+  }, {
+    description: "update with 'silent' for actions",
+    actions: "silent"
+  }, {
+    description: "update with 'silent showURL showAlert showNotification' " +
+                 "for actions and openURL",
+    actions: "silent showURL showAlert showNotification"
+  }
+];
+
+var gOriginalMStone;
+var gOriginalOverrideURL;
+
+__defineGetter__("gBG", function() {
+  delete this.gBG;
+  return this.gBG = Cc["@mozilla.org/browser/browserglue;1"].
+                    getService(Ci.nsIBrowserGlue).
+                    QueryInterface(Ci.nsIObserver);
+});
+
+function test()
+{
+  waitForExplicitFinish();
+
+  if (gPrefService.prefHasUserValue(PREF_MSTONE)) {
+    gOriginalMStone = gPrefService.getCharPref(PREF_MSTONE);
+  }
+
+  if (gPrefService.prefHasUserValue(PREF_OVERRIDE_URL)) {
+    gOriginalOverrideURL = gPrefService.getCharPref(PREF_OVERRIDE_URL);
+  }
+
+  testDefaultArgs();
+}
+
+function finish_test()
+{
+  // Reset browser.startup.homepage_override.mstone to the original value or
+  // clear it if it didn't exist.
+  if (gOriginalMStone) {
+    gPrefService.setCharPref(PREF_MSTONE, gOriginalMStone);
+  } else if (gPrefService.prefHasUserValue(PREF_MSTONE)) {
+    gPrefService.clearUserPref(PREF_MSTONE);
+  }
+
+  // Reset startup.homepage_override_url to the original value or clear it if
+  // it didn't exist.
+  if (gOriginalOverrideURL) {
+    gPrefService.setCharPref(PREF_OVERRIDE_URL, gOriginalOverrideURL);
+  } else if (gPrefService.prefHasUserValue(PREF_OVERRIDE_URL)) {
+    gPrefService.clearUserPref(PREF_OVERRIDE_URL);
+  }
+
+  writeUpdatesToXMLFile(XML_EMPTY);
+  reloadUpdateManagerData();
+
+  finish();
+}
+
+// Test the defaultArgs returned by nsBrowserContentHandler after an update
+function testDefaultArgs()
+{
+  // Clear any pre-existing override in defaultArgs that are hanging around.
+  // This will also set the browser.startup.homepage_override.mstone preference
+  // if it isn't already set.
+  Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs;
+
+  let originalMstone = gPrefService.getCharPref(PREF_MSTONE);
+
+  gPrefService.setCharPref(PREF_OVERRIDE_URL, DEFAULT_PREF_URL);
+
+  writeUpdatesToXMLFile(XML_EMPTY);
+  reloadUpdateManagerData();
+
+  for (let i = 0; i < BCH_TESTS.length; i++) {
+    let test = BCH_TESTS[i];
+    ok(true, "Test nsBrowserContentHandler " + (i + 1) + ": " + test.description);
+
+    if (test.actions) {
+      let actionsXML = " actions=\"" + test.actions + "\"";
+      if (test.openURL) {
+        actionsXML += " openURL=\"" + test.openURL + "\"";
+      }
+      writeUpdatesToXMLFile(XML_PREFIX + actionsXML + XML_SUFFIX);
+    } else {
+      writeUpdatesToXMLFile(XML_EMPTY);
+    }
+
+    reloadUpdateManagerData();
+
+    let noOverrideArgs = Cc["@mozilla.org/browser/clh;1"].
+                         getService(Ci.nsIBrowserHandler).defaultArgs;
+
+    let overrideArgs = "";
+    if (test.prefURL) {
+      overrideArgs = test.prefURL;
+    } else if (test.openURL) {
+      overrideArgs = test.openURL;
+    }
+
+    if (overrideArgs == "" && noOverrideArgs) {
+      overrideArgs = noOverrideArgs;
+    } else if (noOverrideArgs) {
+      overrideArgs += "|" + noOverrideArgs;
+    }
+
+    if (test.noMstoneChange === undefined) {
+      gPrefService.setCharPref(PREF_MSTONE, "PreviousMilestone");
+    }
+
+    if (test.noPostUpdatePref == undefined) {
+      gPrefService.setBoolPref(PREF_POSTUPDATE, true);
+    }
+
+    let defaultArgs = Cc["@mozilla.org/browser/clh;1"].
+                      getService(Ci.nsIBrowserHandler).defaultArgs;
+    is(defaultArgs, overrideArgs, "correct value returned by defaultArgs");
+
+    if (test.noMstoneChange === undefined || test.noMstoneChange != true) {
+      let newMstone = gPrefService.getCharPref(PREF_MSTONE);
+      is(originalMstone, newMstone, "preference " + PREF_MSTONE +
+         " should have been updated");
+    }
+
+    if (gPrefService.prefHasUserValue(PREF_POSTUPDATE)) {
+      gPrefService.clearUserPref(PREF_POSTUPDATE);
+    }
+  }
+
+  testShowNotification();
+}
+
+// nsBrowserGlue.js _showUpdateNotification notification tests
+const BG_NOTIFY_TESTS = [
+  {
+    description: "'silent showNotification' actions should not display a notification",
+    actions: "silent showNotification"
+  }, {
+    description: "'showNotification' for actions should display a notification",
+    actions: "showNotification"
+  }, {
+    description: "no actions and empty updates.xml",
+  }, {
+    description: "'showAlert' for actions should not display a notification",
+    actions: "showAlert"
+  }, {
+    // This test MUST be the last test in the array to test opening the url
+    // provided by the updates.xml.
+    description: "'showNotification' for actions with custom notification " +
+                 "attributes should display a notification",
+    actions: "showNotification",
+    notificationText: "notification text",
+    notificationURL: DEFAULT_UPDATE_URL,
+    notificationButtonLabel: "button label",
+    notificationButtonAccessKey: "b"
+  }
+];
+
+// Test showing a notification after an update
+// _showUpdateNotification in nsBrowserGlue.js
+function testShowNotification()
+{
+  let gTestBrowser = gBrowser.selectedBrowser;
+  let notifyBox = gBrowser.getNotificationBox(gTestBrowser);
+
+  for (let i = 0; i < BG_NOTIFY_TESTS.length; i++) {
+    let test = BG_NOTIFY_TESTS[i];
+    ok(true, "Test showNotification " + (i + 1) + ": " + test.description);
+
+    if (test.actions) {
+      let actionsXML = " actions=\"" + test.actions + "\"";
+      if (test.notificationText) {
+        actionsXML += " notificationText=\"" + test.notificationText + "\"";
+      }
+      if (test.notificationURL) {
+        actionsXML += " notificationURL=\"" + test.notificationURL + "\"";
+      }
+      if (test.notificationButtonLabel) {
+        actionsXML += " notificationButtonLabel=\"" + test.notificationButtonLabel + "\"";
+      }
+      if (test.notificationButtonAccessKey) {
+        actionsXML += " notificationButtonAccessKey=\"" + test.notificationButtonAccessKey + "\"";
+      }
+      writeUpdatesToXMLFile(XML_PREFIX + actionsXML + XML_SUFFIX);
+    } else {
+      writeUpdatesToXMLFile(XML_EMPTY);
+    }
+
+    reloadUpdateManagerData();
+    gPrefService.setBoolPref(PREF_POSTUPDATE, true);
+
+    gBG.observe(null, "browser-glue-test", "post-update-notification");
+
+    let updateBox = notifyBox.getNotificationWithValue("post-update-notification");
+    if (test.actions && test.actions.indexOf("showNotification") != -1 &&
+        test.actions.indexOf("silent") == -1) {
+      ok(updateBox, "Update notification box should have been displayed");
+      if (updateBox) {
+        if (test.notificationText) {
+          is(updateBox.label, test.notificationText, "Update notification box " +
+             "should have the label provided by the update");
+        }
+        if (test.notificationButtonLabel) {
+          var button = updateBox.getElementsByTagName("button").item(0);
+          is(button.label, test.notificationButtonLabel, "Update notification " +
+             "box button should have the label provided by the update");
+          if (test.notificationButtonAccessKey) {
+            let accessKey = button.getAttribute("accesskey");
+            is(accessKey, test.notificationButtonAccessKey, "Update " +
+               "notification box button should have the accesskey " +
+               "provided by the update");
+          }
+        }
+        // The last test opens an url and verifies the url from the updates.xml
+        // is correct.
+        if (i == (BG_NOTIFY_TESTS.length - 1)) {
+          button.click();
+          gBrowser.selectedBrowser.addEventListener("load", testNotificationURL, true);
+        }
+      } else if (i == (BG_NOTIFY_TESTS.length - 1)) {
+        // If updateBox is null the test has already reported errors so bail
+        finish_test();
+      }
+      notifyBox.removeAllNotifications(true);
+    } else {
+      ok(!updateBox, "Update notification box should not have been displayed");
+    }
+
+    let prefHasUserValue = gPrefService.prefHasUserValue(PREF_POSTUPDATE);
+    is(prefHasUserValue, false, "preference " + PREF_POSTUPDATE +
+       " shouldn't have a user value");
+  }
+}
+
+// Test opening the url provided by the updates.xml in the last test
+function testNotificationURL()
+{
+  ok(true, "Test testNotificationURL: clicking the notification button " +
+           "opened the url specified by the update");
+  let href = gBrowser.selectedBrowser.contentWindow.location.href;
+  let expectedURL = BG_NOTIFY_TESTS[BG_NOTIFY_TESTS.length - 1].notificationURL;
+  is(href, expectedURL, "The url opened from the notification should be the " +
+     "url provided by the update");
+  gBrowser.removeCurrentTab();
+  window.focus();
+  finish_test();
+}
+
+/* Reloads the update metadata from disk */
+function reloadUpdateManagerData()
+{
+  Cc["@mozilla.org/updates/update-manager;1"].getService(Ci.nsIUpdateManager).
+  QueryInterface(Ci.nsIObserver).observe(null, "um-reload-update-data", "");
+}
+
+
+function writeUpdatesToXMLFile(aText)
+{
+  const PERMS_FILE = 0644;
+
+  const MODE_WRONLY   = 0x02;
+  const MODE_CREATE   = 0x08;
+  const MODE_TRUNCATE = 0x20;
+
+  let file = Cc["@mozilla.org/file/directory_service;1"].
+             getService(Ci.nsIProperties).
+             get("XCurProcD", Ci.nsIFile);
+  file.append("updates.xml");
+  let fos = Cc["@mozilla.org/network/file-output-stream;1"].
+            createInstance(Ci.nsIFileOutputStream);
+  if (!file.exists()) {
+    file.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
+  }
+  fos.init(file, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, PERMS_FILE, 0);
+  fos.write(aText, aText.length);
+  fos.close();
+}
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -186,16 +186,24 @@ editBookmarkPanel.editBookmarkTitle=Edit
 
 # LOCALIZATION NOTE (editBookmark.removeBookmarks.label)
 # Semi-colon list of plural forms. Replacement for #1 is
 # the number of bookmarks to be removed.
 # If this causes problems with localization you can also do "Remove Bookmarks (#1)"
 # instead of "Remove #1 Bookmarks".
 editBookmark.removeBookmarks.label=Remove Bookmark;Remove #1 Bookmarks
 
+# Post Update Notifications
+pu.notifyButton.label=Details…
+pu.notifyButton.accesskey=D
+# LOCALIZATION NOTE %S will be replaced by the short name of the application.
+puNotifyText=%S has been updated
+puAlertTitle=%S Updated
+puAlertText=Click here for details
+
 # Geolocation UI
 
 # LOCALIZATION NOTE (geolocation.shareLocation geolocation.dontShareLocation): 
 #If you're having trouble with the word Share, please use Allow and Block in your language.
 geolocation.shareLocation=Share Location
 geolocation.shareLocation.accesskey=a
 geolocation.dontShareLocation=Don't Share
 geolocation.dontShareLocation.accesskey=o